Run code once before and after ALL tests in xUnit.net

后端 未结 8 820
野趣味
野趣味 2020-12-02 15:15

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

相关标签:
8条回答
  • 2020-12-02 15:41

    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.

    0 讨论(0)
  • 2020-12-02 15:46

    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.

    0 讨论(0)
  • 2020-12-02 15:46

    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.

    0 讨论(0)
  • 2020-12-02 15: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 class MyClassName : XunitTestFramework
       {
          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

    0 讨论(0)
  • 2020-12-02 15:52

    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.

    0 讨论(0)
  • 2020-12-02 15:54

    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.

    0 讨论(0)
提交回复
热议问题