128

TL;DR - I'm looking for xUnit's equivalent of MSTest's AssemblyInitialize (aka the ONE feature it has that I like).

Specifically I'm looking for it because I have some Selenium smoke tests which I would like to be able to run with no other dependencies. I have a Fixture that will launch IisExpress for me and kill it on disposal. But doing this before every test hugely bloats runtime.

I would like to trigger this code once at the start of testing, and dispose of it (shutting down the process) at the end. How could I go about doing that?

Even if I can get programmatic access to something like "how many tests are currently being run" I can figure something out.

abatishchev
  • 98,240
  • 88
  • 296
  • 433
George Mauer
  • 117,483
  • 131
  • 382
  • 612
  • Do you mean xUnit as "the generic group of language-specific unit testing tools like JUnit, NUnit, etc." or xUnit as "xUnit.net, the .Net unit testing tool"? – Andy Tinkham Dec 17 '12 at 16:18
  • 3
    Based on this table http://xunit.codeplex.com/wikipage?title=Comparisons&referringTitle=Home I dont think there is an equivalent. A workaround would be to move your assembly initalization into a singleton and invoke it from each of your constructors. – allen Dec 18 '12 at 07:11
  • @allen - That is similar to what I'm doing but it gives me an assembly initializer without not an assembly teardown. This is why I was asking about the test count. – George Mauer Dec 19 '12 at 00:05

11 Answers11

92

As of Nov 2015 xUnit 2 is out, so there is a canonical way to share features between tests. It is documented here.

Basically you'll need to create a class doing the fixture:

    public class DatabaseFixture : IDisposable
    {
        public DatabaseFixture()
        {
            Db = new SqlConnection("MyConnectionString");

            // ... initialize data in the test database ...
        }

        public void Dispose()
        {
            // ... clean up test data from the database ...
        }

        public SqlConnection Db { get; private set; }
    }

A dummy class bearing the CollectionDefinition attribute. This class allows Xunit to create a test collection, and will use the given fixture for all test classes of the collection.

    [CollectionDefinition("Database collection")]
    public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
    {
        // This class has no code, and is never created. Its purpose is simply
        // to be the place to apply [CollectionDefinition] and all the
        // ICollectionFixture<> interfaces.
    }

Then you need to add the collection name over all your test classes. The test classes can receive the fixture through the constructor.

    [Collection("Database collection")]
    public class DatabaseTestClass1
    {
        DatabaseFixture fixture;

        public DatabaseTestClass1(DatabaseFixture fixture)
        {
            this.fixture = fixture;
        }
    }

It's a bit more verbose than MsTests AssemblyInitialize since you have to declare on each test class which test collection it belongs, but it's also more modulable (and with MsTests you still need to put a TestClass on your classes)

Note: the samples have been taken from the documentation.

Community
  • 1
  • 1
gwenzek
  • 2,816
  • 21
  • 21
  • 20
    When I read this:"...// This class has no code, and is never created..." then I really prefer Microsofts implementation of a AssemblyInitialize. Much more elegant. – Elisabeth Dec 29 '15 at 12:29
  • I agree it's a bit weird – gwenzek Dec 30 '15 at 13:26
  • There's no reason that xunit couldn't also support an [AssemblyInitialize] attribute. I agree that the method described above is elegant, but for certain things it's really overkill, and conflicts with their desire to be simple. xUnit should really support both ways. – Ken Smith Dec 31 '15 at 20:28
  • 9
    @Elisabeth I've just used this and added both the `[CollectionDefinition("Database collection")]` attribute and `ICollectionFixture` interface to the `DatabaseFixture` class and everything works. It removes an empty class and seems cleaner to me! – Christian Droulers Sep 27 '16 at 20:06
  • 23
    The [Collection] attribute prevents tests from running in parallel. Are there any other approach for global initialization/tear-down in xUnit that allows running tests in parallel? – IT Hit WebDAV Feb 01 '18 at 07:03
  • 1
    I know this is old, but how do you have multiple Collections on a single test class? I want it to run the database setup code as well as my Mapping code. – rball Nov 15 '18 at 00:47
  • awesome thankyou it has worked for me. basically before i run all my tests i restore a snapshot of my database and i wanted to kill and restore my dummy db which is a replica of my production db. this code here helped me with running the snapshot kill/restore and all my tests passed. before this script they were not as i was killing the connection while tests were running. tkx – Costas Aletrari Jan 01 '20 at 09:39
  • 1
    @ITHitWebDAV you can use [Shimmy's Answer](https://stackoverflow.com/questions/13829737/run-code-once-before-and-after-all-tests-in-xunit-net#answer-44534216) – manjeet lama Jan 31 '20 at 19:28
  • But we cannot run async code inside a constructor. I was thinking of spinning of a docker container using dotnetdocker library but the async code cannot be placed inside the constructor. – Navjot Singh May 01 '21 at 21:29
  • Links are dead, i believe these are the new links: https://xunit.net/docs/shared-context – badsyntax Sep 21 '21 at 14:52
  • @NavjotSingh, you can implement IAsyncLifetime instead of IDisposable if that's the case. – Brecht De Rooms Mar 21 '22 at 18:13
  • You can implement both IAsyncLifetime and IDisposable if you need to. I am in some cases. – schmiddy98 Aug 01 '22 at 19:50
49

To execute code on assembly initialize, then one can do this (Tested with xUnit 2.3.1)

using Xunit.Abstractions;
using Xunit.Sdk;

[assembly: Xunit.TestFramework("MyNamespace.MyClassName", "MyAssemblyName")]

namespace MyNamespace
{   
   public sealed class MyClassName : XunitTestFramework, IDisposable
   {
      public MyClassName(IMessageSink messageSink)
        :base(messageSink)
      {
        // Place initialization code here
      }

      public new void Dispose()
      {
        // Place tear down code here
        base.Dispose();
      }
   }
}

See also https://github.com/xunit/samples.xunit/tree/master/AssemblyFixtureExample

Rolf Kristensen
  • 17,785
  • 1
  • 51
  • 70
  • The constructor is called but the dispose is not called after the test case . ( xUnit 2.4.1) – TonyQ Dec 24 '20 at 01:27
  • 5
    @TonyQ I added IDisposable and after that Dispose start getting called ( MyClassName : XunitTestFramework, IDisposable). Also, in case anyone doesn't know "MyAssemblyName" is the name of your test project, I didn't know that. – manjeet lama Mar 12 '21 at 01:32
  • Not a big fan of using "new" here, but, oh well... – schmiddy98 Aug 01 '22 at 22:39
  • Think this should probably be the accepted answer and definitely was the one that I wound up using. Didn't require decorating all my test classes with attributes. – lamont Apr 30 '23 at 20:18
30

Create a static field and implement a finalizer.

You can use the fact that xUnit creates an AppDomain to run your test assembly and unloads it when it's finished. Unloading the app domain will cause the finalizer to run.

I am using this method to start and stop IISExpress.

public sealed class ExampleFixture
{
    public static ExampleFixture Current = new ExampleFixture();

    private ExampleFixture()
    {
        // Run at start
    }

    ~ExampleFixture()
    {
        Dispose();
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);

        // Run at end
    }        
}

Edit: Access the fixture using ExampleFixture.Current in your tests.

Community
  • 1
  • 1
Jared Kells
  • 6,771
  • 4
  • 38
  • 43
  • Interesting - does the appdomain get disposed immediately or does it take a while? In other words, what if I have two test runs back-to-back? – George Mauer Feb 19 '13 at 06:42
  • The AppDomain is unloaded immediately after the last test in your assembly finishes running. If you have 100 tests running back to back in the same test assembly it will be unloaded after the 100'th test completes. – Jared Kells Feb 19 '13 at 06:47
  • 1
    I am using this method to start IISExpress at the start of my tests and stop it after they have all completed. It's working fine in ReSharper, and MSBuild on Teamcity. – Jared Kells Feb 19 '13 at 06:49
  • Check out Brad's comment on the accepted answer. Not saying that your approach - obviously it does for you - but the fact that the finalizer is not guaranteed to run at all is an interesting wrinkle – George Mauer Mar 14 '13 at 15:04
  • 1
    @GeorgeMauer and Jared, perhaps the `AppDomain` `Unload` event might be more useful ? (Of course during shutdown all bets are off, but it may just be that bit more reliable from memory) – Ruben Bartelink Mar 14 '13 at 20:39
  • AppDomain Unload worked for me too, that's what I had to start with. I think I switched to a finalizer because I read somewhere it was more reliable lol. – Jared Kells Mar 14 '13 at 23:48
  • 2
    This is just anecdotal but since posting this answer, my build server has run for a month and done 300 builds using this method and it's working fine. – Jared Kells Mar 14 '13 at 23:51
  • I missed the event at first look, it's named DomainUnload: http://msdn.microsoft.com/en-us/library/system.appdomain.domainunload(v=vs.110).aspx – Karsten Sep 05 '14 at 08:59
  • 1
    I know this is old, but just to clarify... do I need to call `ExampleFixture.Current` from every test case to ensure that it gets instantiated? – Peet Brits Dec 30 '20 at 08:53
19

It's not possible to do in the framework today. This is a feature planned for 2.0.

In order to make this work before 2.0, it would require you to perform significant re-architecture on the framework, or write your own runners that recognized your own special attributes.

Brad Wilson
  • 67,914
  • 9
  • 74
  • 83
  • Thanks Brad, as long as I have you here, any idea about my most recent question on the VS test runner? http://visualstudiogallery.msdn.microsoft.com/463c5987-f82b-46c8-a97e-b1cde42b9099/view/Discussions – George Mauer Dec 19 '12 at 16:01
  • Sorry, I don't have any experience with FsUnit. In general, since the VS2012 runner runs based on compiled DLLs, anything which can be run with another runner (like the console or MSBuild runner) should be able to be run inside VS2012. I'll take a look this weekend and see if I can detect anything F# specific that's the cause of the issue. – Brad Wilson Dec 19 '12 at 18:12
  • My question isn't the FsUnit one, mine is about how to get static assets to copy to the test directory since the test runner doesn't run it in-place. – George Mauer Dec 19 '12 at 19:58
  • Everything in your bin\Debug (or Release) folder will be copied (but nothing outside of it), so the correct answer is to mark all the files as Content using Visual Studio's Properties window, and then expect their path to be relative to the DLL, not relative to the project file. – Brad Wilson Dec 19 '12 at 23:29
  • 1
    Hey Brad - check out the answer from @JaredKells below, do you forsee any issues with that approach? – George Mauer Feb 19 '13 at 15:06
  • 1
    According to the .NET framework, there is no guarantee when a finalizer will run, or if it will run at all. If you're comfortable with that, then I guess his suggestion is fine. :) – Brad Wilson Mar 14 '13 at 13:35
  • Where is that documented Brad? from what I can see the finalizer is guaranteed to run except under a few exceptional circumstances. 1. Another finalizer blocks indefinitely. 2. The process terminates without giving the runtime a chance to clean up. 3. The runtime continues to Finalize objects during shutdown only while the number of finalizable objects continues to decrease. – Jared Kells Mar 14 '13 at 23:44
  • I don't think it's possible to call AppDomain.Unload and not run all the finalizers without a CannotUnloadAppDomainException being raised crashing the runner. – Jared Kells Mar 14 '13 at 23:46
  • 7
    So, now that 2.0 is released, the solution that does NOT involve you having to have a common base class for all of your tests (or otherwise call some common code) is to implement your own subclass of XunitTestFramework, do your initialization in the constructor and finalizer and then mark the assembly with a TestFrameworkAttribute. An alternative to the finalizer, if you're uncomfortable with that, is to add an object to the protected DisposalTracker property of the base class TestFramework that does the cleanup. – Avi Cherry Apr 27 '15 at 22:25
  • 1
    @AviCherry, that should be the answer to the original question now. Worked great for me – dcstraw Sep 22 '16 at 21:34
5

I use AssemblyFixture (NuGet).

What it does is it provides an IAssemblyFixture<T> interface that is replacing any IClassFixture<T> where you want the object's lifetime to be as the testing assembly.

Example:

public class Singleton { }

public class TestClass1 : IAssemblyFixture<Singleton>
{
  readonly Singletone _Singletone;
  public TestClass1(Singleton singleton)
  {
    _Singleton = singleton;
  }

  [Fact]
  public void Test1()
  {
     //use singleton  
  }
}

public class TestClass2 : IAssemblyFixture<Singleton>
{
  readonly Singletone _Singletone;
  public TestClass2(Singleton singleton)
  {
    //same singleton instance of TestClass1
    _Singleton = singleton;
  }

  [Fact]
  public void Test2()
  {
     //use singleton  
  }
}
Shimmy Weitzhandler
  • 101,809
  • 122
  • 424
  • 632
  • Correct me if I'm wrong but its worth mentioning that this is xUnit 2+ – George Mauer Jun 14 '17 at 03:50
  • It doesn't work unless you add `[assembly: TestFramework("Xunit.Extensions.Ordering.TestFramework", "Xunit.Extensions.Ordering")]` to one of the project's class. [Reference](https://github.com/tomaszeman/Xunit.Extensions.Ordering/issues/3) – manjeet lama Jan 31 '20 at 19:16
3

I was quite annoyed for not having the option to execute things at the end of all the xUnit tests. Some of the options here are not as great, as they involve changing all your tests or putting them under one collection (meaning they get executed synchronously). But Rolf Kristensen's answer gave me the needed information to get to this code. It's a bit long, but you only need to add it into your test project, no other code changes necessary:

using Siderite.Tests;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

[assembly: TestFramework(
    SideriteTestFramework.TypeName,
    SideriteTestFramework.AssemblyName)]

namespace Siderite.Tests
{
    public class SideriteTestFramework : ITestFramework
    {
        public const string TypeName = "Siderite.Tests.SideriteTestFramework";
        public const string AssemblyName = "Siderite.Tests";
        private readonly XunitTestFramework _innerFramework;

        public SideriteTestFramework(IMessageSink messageSink)
        {
            _innerFramework = new XunitTestFramework(messageSink);
        }

        public ISourceInformationProvider SourceInformationProvider
        {
            set
            {
                _innerFramework.SourceInformationProvider = value;
            }
        }

        public void Dispose()
        {
            _innerFramework.Dispose();
        }

        public ITestFrameworkDiscoverer GetDiscoverer(IAssemblyInfo assembly)
        {
            return _innerFramework.GetDiscoverer(assembly);
        }

        public ITestFrameworkExecutor GetExecutor(AssemblyName assemblyName)
        {
            var executor = _innerFramework.GetExecutor(assemblyName);
            return new SideriteTestExecutor(executor);
        }

        private class SideriteTestExecutor : ITestFrameworkExecutor
        {
            private readonly ITestFrameworkExecutor _executor;
            private IEnumerable<ITestCase> _testCases;

            public SideriteTestExecutor(ITestFrameworkExecutor executor)
            {
                this._executor = executor;
            }

            public ITestCase Deserialize(string value)
            {
                return _executor.Deserialize(value);
            }

            public void Dispose()
            {
                _executor.Dispose();
            }

            public void RunAll(IMessageSink executionMessageSink, ITestFrameworkDiscoveryOptions discoveryOptions, ITestFrameworkExecutionOptions executionOptions)
            {
                _executor.RunAll(executionMessageSink, discoveryOptions, executionOptions);
            }

            public void RunTests(IEnumerable<ITestCase> testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
            {
                _testCases = testCases;
                _executor.RunTests(testCases, new SpySink(executionMessageSink, this), executionOptions);
            }

            internal void Finished(TestAssemblyFinished executionFinished)
            {
                // do something with the run test cases in _testcases and the number of failed and skipped tests in executionFinished
            }
        }


        private class SpySink : IMessageSink
        {
            private readonly IMessageSink _executionMessageSink;
            private readonly SideriteTestExecutor _testExecutor;

            public SpySink(IMessageSink executionMessageSink, SideriteTestExecutor testExecutor)
            {
                this._executionMessageSink = executionMessageSink;
                _testExecutor = testExecutor;
            }

            public bool OnMessage(IMessageSinkMessage message)
            {
                var result = _executionMessageSink.OnMessage(message);
                if (message is TestAssemblyFinished executionFinished)
                {
                    _testExecutor.Finished(executionFinished);
                }
                return result;
            }
        }
    }
}

The highlights:

  • assembly: TestFramework instructs xUnit to use your framework, which proxies to the default one
  • SideriteTestFramework also wraps the executor into a custom class that then wraps the message sink
  • in the end, the Finished method is executed, with the list of tests run and the result from the xUnit message

More work could be done here. If you want to execute stuff without caring about the tests run, you could inherit from XunitTestFramework and just wrap the message sink.

Siderite Zackwehdex
  • 6,293
  • 3
  • 30
  • 46
2

As of now, xUnit has addressed this officially within their documentation. That is to use Class Fixture.

public class DatabaseFixture : IDisposable
{
    public DatabaseFixture()
    {
        Db = new SqlConnection("MyConnectionString");

        // ... initialize data in the test database ...
    }

    public void Dispose()
    {
        // ... clean up test data from the database ...
    }

    public SqlConnection Db { get; private set; }
}

public class MyDatabaseTests : IClassFixture<DatabaseFixture>
{
    DatabaseFixture fixture;

    public MyDatabaseTests(DatabaseFixture fixture)
    {
        this.fixture = fixture;
    }

    // ... write tests, using fixture.Db to get access to the SQL Server ...
}

More: https://xunit.net/docs/shared-context#class-fixture

1

You can use IUseFixture interface to make this happen. Also all of your test must inherit TestBase class. You can also use OneTimeFixture directly from your test.

public class TestBase : IUseFixture<OneTimeFixture<ApplicationFixture>>
{
    protected ApplicationFixture Application;

    public void SetFixture(OneTimeFixture<ApplicationFixture> data)
    {
        this.Application = data.Fixture;
    }
}

public class ApplicationFixture : IDisposable
{
    public ApplicationFixture()
    {
        // This code run only one time
    }

    public void Dispose()
    {
        // Here is run only one time too
    }
}

public class OneTimeFixture<TFixture> where TFixture : new()
{
    // This value does not share between each generic type
    private static readonly TFixture sharedFixture;

    static OneTimeFixture()
    {
        // Constructor will call one time for each generic type
        sharedFixture = new TFixture();
        var disposable = sharedFixture as IDisposable;
        if (disposable != null)
        {
            AppDomain.CurrentDomain.DomainUnload += (sender, args) => disposable.Dispose();
        }
    }

    public OneTimeFixture()
    {
        this.Fixture = sharedFixture;
    }

    public TFixture Fixture { get; private set; }
}

EDIT: Fix the problem that new fixture create for each test class.

Khoa Le
  • 41
  • 3
  • 1
    This is both wrong and bad advice. The methods of `ApplicationFixture` will run before and after each test, not just once. It is also bad advice to always inherit from the same `TestBase` as that uses up your one inheritance link and you can no longer use it to share common methods between a group of related classes. In fact, that is exactly why `IUseFixture` was invented, to not have to rely on inheritance. Finally, you will note that the creator of xUnit already has the accepted answer to the question that until 2.0 ships this is impossible to do properly. – George Mauer Feb 01 '15 at 17:36
  • Well actually it not run before each test. When you use IUseFixture the Fixture will create only one time for each type of test class. So if you put your code in the Fixture constructor it will execute only one time. The bad is it run one time for each type of test class and I just know that too. I thought that is will create one instance for each test session but it's not. I just modify sample code to solve that issue. Use static variable to store the Fixture instance to make sure that it will create only one time and AppDomain Unload event to dispose Fixture. – Khoa Le Feb 02 '15 at 05:24
0

Does your build tool provide such a feature?

In the Java world, when using Maven as a build tool, we use the appropriate phases of the build lifecycle. E.g. in your case (acceptance tests with Selenium-like tools), one can make good use of the pre-integration-test and post-integration-test phases to start/stop a webapp before/after one's integration-tests.

I'm pretty sure the same mechanism can be set up in your environment.

Vincent
  • 1,035
  • 6
  • 14
  • With a full blown build system anything is possible of course. I can set this up with psake or grunt fairly easily. The problem is that Visual Studio-integrated test runners don't use build systems simply to run their tests, from what I've seen of their codebases they are invoked by the IDE directly and themselves run any dlls directly. – George Mauer Dec 19 '12 at 20:01
0

The method described by Jared Kells does not work under Net Core, because, well it is not guaranteed that finalizers will be called. And, in fact, it is not called for the code above. Please, see:

Why does the Finalize/Destructor example not work in .NET Core?

https://github.com/dotnet/runtime/issues/16028

https://github.com/dotnet/runtime/issues/17836

https://github.com/dotnet/runtime/issues/24623

So, based on the great answer above, here is what I ended up doing (replace saving to file as necessary):

public class DatabaseCommandInterceptor : IDbCommandInterceptor
{
    private static ConcurrentDictionary<DbCommand, DateTime> StartTime { get; } = new();

    public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) => Log(command, interceptionContext);

    public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) => Log(command, interceptionContext);

    public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) => Log(command, interceptionContext);

    private static void Log<T>(DbCommand command, DbCommandInterceptionContext<T> interceptionContext)
    {
        var parameters = new StringBuilder();

        foreach (DbParameter param in command.Parameters)
        {
            if (parameters.Length > 0) parameters.Append(", ");
            parameters.Append($"{param.ParameterName}:{param.DbType} = {param.Value}");
        }

        var data = new DatabaseCommandInterceptorData
        {
            CommandText = command.CommandText,
            CommandType = $"{command.CommandType}",
            Parameters = $"{parameters}",
            Duration = StartTime.TryRemove(command, out var startTime) ? DateTime.Now - startTime : TimeSpan.Zero,
            Exception = interceptionContext.Exception,
        };

        DbInterceptorFixture.Current.LogDatabaseCall(data);
    }

    public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) => OnStart(command);
    public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) => OnStart(command);
    public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) => OnStart(command);

    private static void OnStart(DbCommand command) => StartTime.TryAdd(command, DateTime.Now);
}

public class DatabaseCommandInterceptorData
{
    public string CommandText { get; set; }
    public string CommandType { get; set; }
    public string Parameters { get; set; }
    public TimeSpan Duration { get; set; }
    public Exception Exception { get; set; }
}

/// <summary>
/// All times are in milliseconds.
/// </summary>
public record DatabaseCommandStatisticalData
{
    public string CommandText { get; }
    public int CallCount { get; init; }
    public int ExceptionCount { get; init; }
    public double Min { get; init; }
    public double Max { get; init; }
    public double Mean { get; init; }
    public double StdDev { get; init; }

    public DatabaseCommandStatisticalData(string commandText)
    {
        CommandText = commandText;
        CallCount = 0;
        ExceptionCount = 0;
        Min = 0;
        Max = 0;
        Mean = 0;
        StdDev = 0;
    }

    /// <summary>
    /// Calculates k-th moment for n + 1 values: M_k(n + 1)
    /// based on the values of k, n, mkn = M_k(N), and x(n + 1).
    /// The sample adjustment (replacement of n -> (n - 1)) is NOT performed here
    /// because it is not needed for this function.
    /// Note that k-th moment for a vector x will be calculated in Wolfram as follows:
    ///     Sum[x[[i]]^k, {i, 1, n}] / n
    /// </summary>
    private static double MknPlus1(int k, int n, double mkn, double xnp1) =>
        (n / (n + 1.0)) * (mkn + (1.0 / n) * Math.Pow(xnp1, k));

    public DatabaseCommandStatisticalData Updated(DatabaseCommandInterceptorData data) =>
        CallCount == 0
            ? this with
            {
                CallCount = 1,
                ExceptionCount = data.Exception == null ? 0 : 1,
                Min = data.Duration.TotalMilliseconds,
                Max = data.Duration.TotalMilliseconds,
                Mean = data.Duration.TotalMilliseconds,
                StdDev = 0.0,
            }
            : this with
            {
                CallCount = CallCount + 1,
                ExceptionCount = ExceptionCount + (data.Exception == null ? 0 : 1),
                Min = Math.Min(Min, data.Duration.TotalMilliseconds),
                Max = Math.Max(Max, data.Duration.TotalMilliseconds),
                Mean = MknPlus1(1, CallCount, Mean, data.Duration.TotalMilliseconds),
                StdDev = Math.Sqrt(
                    MknPlus1(2, CallCount, Math.Pow(StdDev, 2) + Math.Pow(Mean, 2), data.Duration.TotalMilliseconds)
                    - Math.Pow(MknPlus1(1, CallCount, Mean, data.Duration.TotalMilliseconds), 2)),
            };

    public static string Header { get; } =
        string.Join(TextDelimiter.VerticalBarDelimiter.Key,
            new[]
            {
                nameof(CommandText),
                nameof(CallCount),
                nameof(ExceptionCount),
                nameof(Min),
                nameof(Max),
                nameof(Mean),
                nameof(StdDev),
            });

    public override string ToString() =>
        string.Join(TextDelimiter.VerticalBarDelimiter.Key,
            new[]
            {
                $"\"{CommandText.Replace("\"", "\"\"")}\"",
                $"{CallCount}",
                $"{ExceptionCount}",
                $"{Min}",
                $"{Max}",
                $"{Mean}",
                $"{StdDev}",
            });
}

public class DbInterceptorFixture
{
    public static readonly DbInterceptorFixture Current = new();
    private bool _disposedValue;
    private ConcurrentDictionary<string, DatabaseCommandStatisticalData> DatabaseCommandData { get; } = new();
    private static IMasterLogger Logger { get; } = new MasterLogger(typeof(DbInterceptorFixture));

    /// <summary>
    /// Will run once at start up.
    /// </summary>
    private DbInterceptorFixture()
    {
        AssemblyLoadContext.Default.Unloading += Unloading;
    }

    /// <summary>
    /// A dummy method to call in order to ensure that static constructor is called
    /// at some more or less controlled time.
    /// </summary>
    public void Ping()
    {
    }

    public void LogDatabaseCall(DatabaseCommandInterceptorData data) =>
        DatabaseCommandData.AddOrUpdate(
            data.CommandText,
            _ => new DatabaseCommandStatisticalData(data.CommandText).Updated(data),
            (_, d) => d.Updated(data));

    private void Unloading(AssemblyLoadContext context)
    {
        if (_disposedValue) return;
        GC.SuppressFinalize(this);
        _disposedValue = true;
        SaveData();
    }

    private void SaveData()
    {
        try
        {
            File.WriteAllLines(
                @"C:\Temp\Test.txt",
                DatabaseCommandData
                    .Select(e => $"{e.Value}")
                    .Prepend(DatabaseCommandStatisticalData.Header));
        }
        catch (Exception e)
        {
            Logger.LogError(e);
        }
    }
}

and then register DatabaseCommandInterceptor once somewhere in the tests:

DbInterception.Add(new DatabaseCommandInterceptor());

I also prefer calling DbInterceptorFixture.Current.Ping() in the base test class, though I don't think that this is needed.

The interface IMasterLogger is just a strongly typed wrapper around log4net, so just replace it with your favorite one.

The value of TextDelimiter.VerticalBarDelimiter.Key is just '|' and it sits in what we call a closed set.

PS If I screwed up with statistics, please, comment and I will update the answer.

  • 1
    I suspect you were downvoted because 90+% of the code you posted isn't relevant to the question (which asks nothing about databases), and while you seem offer some useful insight (vis-a-vis finalisers in .NET Core - I didn't know about that), it's unclear from your answer what to do with that information. – Dan Parsonson Apr 20 '22 at 09:29
  • There is a bit of a low Signal/Noise ratio when trying to relate to the original post, so I support the theory on the downvote (non casted by me, btw) ;) – oligofren Aug 18 '22 at 15:19
0

Just use the static constructor, that's all you need to do, it runs just once.

Movsar Bekaev
  • 888
  • 1
  • 12
  • 30