在xUnit.net中进行所有测试之前和之后运行一次代码


78

TL; DR-我正在寻找xUnit等同于MSTest AssemblyInitialize(也就是我喜欢的ONE功能)。

特别是我正在寻找它,因为我有一些Selenium烟雾测试,希望能够在没有其他依赖项的情况下运行。我有一个Fixture,它将为我启动IisExpress并销毁它。但是,在每次测试前都要这样做会极大地膨胀运行时。

我想在测试开始时触发一次此代码,然后在结束时将其处理(关闭过程)。我该怎么做呢?

即使我可以通过编程方式访问“当前正在运行多少个测试”之类的东西,我也可以弄清楚。


您是否将xUnit称为“特定于语言的单元测试工具的通用组,例如JUnit,NUnit等”。还是xUnit作为“ xUnit.net,.Net单元测试工具”?
Andy Tinkham

3
基于此表xunit.codeplex.com/… 我认为没有等效项。一种解决方法是将程序集初始化成一个单例并从每个构造函数中调用它。
艾伦

@allen-与我正在执行的操作类似,但是它为我提供了一个程序集初始化程序,而没有程序集拆卸。这就是为什么我要询问测试计数的原因。
George Mauer 2012年

Answers:


57

截至2015年11月,xUnit 2已发布,因此有一种规范的方式可以在测试之间共享功能。它记录在这里

基本上,您需要创建一个执行夹具的类:

    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; }
    }

带有CollectionDefinition属性的伪类。该类允许Xunit创建测试集合,并将给定的夹具用于集合的所有测试类。

    [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.
    }

然后,您需要在所有测试类上添加集合名称。测试类可以通过构造函数接收灯具。

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

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

它比MsTests更为冗长,AssemblyInitialize因为您必须在每个测试类上声明它属于哪个测试集合,但是它也更具可调整性(并且对于MsTests,您仍然需要在类上放置一个TestClass)

注意:样本取自文档


10
当我读到以下内容时:“ ... //此类没有代码,并且永远不会创建...”,那么我真的更喜欢Microsoft对AssemblyInitialize的实现。更加优雅。
伊丽莎白2015年

我同意这
有点奇怪

8
@Elisabeth我刚刚使用了它,并向类添加了[CollectionDefinition("Database collection")]属性和ICollectionFixture<DatabaseFixture>接口,DatabaseFixture并且一切正常。它消除了一个空的类,对我来说似乎更干净!
Christian Droulers

11
[Collection]属性可防止测试并行运行。在xUnit中是否还有其他用于全局初始化/拆卸的方法可以并行运行测试?
IT Hit WebDAV '18

1
我知道这很老了,但是如何在一个测试类上有多个Collections?我希望它运行数据库设置代码以及我的映射代码。
rball

24

创建一个静态字段并实现一个终结器。

您可以使用xUnit创建一个AppDomain来运行测试程序集并在完成时将其卸载的事实。卸载应用程序域将导致终结器运行。

我正在使用此方法启动和停止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
    }        
}

编辑:ExampleFixture.Current在测试中使用夹具。


有趣-appdomain是立即处置还是需要一段时间?换句话说,如果我有两个背对背的测试怎么办?
2013年

程序集中的最后一个测试完成运行后,将立即卸载AppDomain。如果在同一个测试程序集中有100个测试在运行,则第100个测试完成后将被卸载。
贾里德·凯尔斯

1
我正在使用这种方法在测试开始时启动IISExpress,并在测试全部完成后将其停止。在ReSharper和TeamBuild上的MSBuild上运行正常。
贾里德·凯尔斯

1
@GeorgeMauer和Jared,也许该AppDomain Unload活动可能会更有用?(当然,在关机期间所有赌注都是关闭的,但是从内存来看可能更可靠)
Ruben Bartelink 2013年

2
这只是轶事,但是自发布此答案以来,我的构建服务器已经运行了一个月,并且使用此方法完成了300次构建,并且运行良好。
Jared Kells

21

要在程序集初始化上执行代码,则可以执行此操作(已通过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();
      }
   }
}

另请参见https://github.com/xunit/samples.xunit/tree/master/AssemblyFixtureExample


您好,这看起来很有趣:我们可以提供更多信息吗?
乔纳莎·安托万(Jonatha ANTOINE),

1
@JonathaANTOINE认为您应该创建一个新的stackoverflow问题,而不是从一个有10000个可能答案的开放性问题开始。
罗尔夫·克里斯滕森

好的,我已经找到了解决方法,并且收到了一篇博客文章:)
Jonatha ANTOINE

16

今天在该框架中是不可能的。这是针对2.0计划的功能。

为了在2.0之前完成这项工作,需要您在框架上进行重大的重新架构,或者编写自己的识别出自己特殊属性的运行程序。


谢谢布拉德,只要我在这里,对我最近在VS测试跑步机上的问题有任何想法吗?visualstudiogallery.msdn.microsoft.com/…–
George Mauer

1
嘿,布拉德-请查看下面@JaredKells的答案,您认为这种方法有任何问题吗?
乔治·莫尔

1
根据.NET框架,无法保证终结器何时运行或完全运行。如果您对此感到满意,那么我想他的建议很好。:)
布拉德·威尔逊

5
因此,既然2.0版已经发布,不涉及您必须为所有测试拥有通用基类(或者调用某些通用代码)的解决方案是实现您自己的XunitTestFramework子类,并在构造函数中进行初始化和终结器,然后用TestFrameworkAttribute标记程序集。如果您不满意终结器,则可以使用另一种方法,即将对象添加到进行清理的基类TestFramework的受保护的DisposalTracker属性中。
阿维·切里

1
@AviCherry,那应该是现在原始问题的答案。对我来说很棒
dcstraw

3

我使用AssemblyFixtureNuGet)。

它的作用是提供一个IAssemblyFixture<T>接口,该接口可以替换IClassFixture<T>您希望将对象的寿命用作测试组件的任何位置。

例:

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  
  }
}

如果我错了,请纠正我,但值得一提的是,这是xUnit 2+
George Mauer

除非您添加[assembly: TestFramework("Xunit.Extensions.Ordering.TestFramework", "Xunit.Extensions.Ordering")]到项目的一个类中,否则它将不起作用。参考
manjeet lama

2

我很生气,因为没有选择在所有xUnit测试结束时执行事情的选项。这里的一些选项不是那么好,因为它们涉及更改所有测试或将它们置于一个集合中(这意味着它们将被同步执行)。但是Rolf Kristensen的回答为我提供了获取此代码所需的信息。它有点长,但是您只需要将其添加到测试项目中,无需更改其他代码:

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;
            }
        }
    }
}

亮点:

  • 程序集:TestFramework指示xUnit使用您的框架,该框架替代默认框架
  • SideriteTestFramework还将执行程序包装到一个自定义类中,然后该类包装消息接收器
  • 最后,执行Finished方法,并运行测试列表和xUnit消息的结果

这里可以做更多的工作。如果您想执行某些任务而不关心测试的运行,则可以继承XunitTestFramework并包装消息接收器。


1

您可以使用IUseFixture界面实现此目的。同样,所有测试都必须继承TestBase类。您也可以直接从测试中使用OneTimeFixture。

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; }
}

编辑:修复新夹具为每个测试类创建的问题。


这既是错误的也是不好的建议。的方法ApplicationFixture将在每次测试之前和之后运行,而不仅仅是一次。始终从与TestBase用完一个继承链接相同的那个继承,也是坏建议,您不能再使用它在一组相关类之间共享通用方法。实际上,这正是IUseFixture发明而不依赖于继承的原因。最后,您将注意到xUnit的创建者已经接受了以下问题的答案:在2.0发布之前,这是不可能正常进行的。
George Mauer

好吧,实际上它不是在每次测试之前都运行。当您使用IUseFixture时,夹具将为每种类型的测试类仅创建一次。因此,如果将代码放入Fixture构造函数中,它将仅执行一次。不好的是,每种类型的测试类只能运行一次,我也知道这一点。我认为这将为每个测试会话创建一个实例,但事实并非如此。我只是修改示例代码来解决该问题。使用静态变量存储Fixture实例,以确保它仅创建一次,并使用AppDomain Unload事件来处置Fixture。
Khoa Le

0

您的构建工具是否提供了这样的功能?

在Java世界中,当使用Maven作为构建工具时,我们使用了构建生命周期的相应阶段。例如,在您的情况下(使用类似Selenium的工具进行验收测试),您可以很好地利用pre-integration-testpost-integration-test阶段在integration-tests之前/之后启动/停止webapp 。

我很确定可以在您的环境中设置相同的机制。


有了完整的构建系统,任何事情当然都是可能的。我可以很容易地用psake或grunt进行设置。问题在于,与Visual Studio集成的测试运行程序不会仅仅使用构建系统来运行测试,从我所看到的代码库来看,它们直接由IDE调用,而它们本身直接运行任何dll。
George Mauer 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.