Skip to content

xUnit

简单使用

using Xunit;

public class UnitTest1
{
    [Fact(DisplayName="这是一个测试", Skip="测试了这个值就不测试")]
    public void Test1()
    {
        var sum = 18 + 24;
        Assert.Equal(42, sum);
    }
}

断言

// Any values
int value = 0;
Assert.Equal(42, value);
Assert.NotEqual(42, value);

// Boolean
bool b = true;
Assert.True(b, "b should be true");
Assert.False(b, "b should be false");

// Strings
var str = "";
Assert.Equal("", str, ignoreCase: false, ignoreLineEndingDifferences: false, ignoreWhiteSpaceDifferences: false);
Assert.StartsWith("prefix", str, StringComparison.Ordinal);
Assert.EndsWith("suffix", str, StringComparison.Ordinal);
Assert.Matches("[0-9]+", str);

// Collections
var collection = new [] { 1, 2, 3 };
Assert.Empty(collection);
Assert.NotEmpty(collection);
Assert.Single(collection); // Contains only 1 item
Assert.Single(collection, item => item == 1); // Contains only 1 item
Assert.Equal(new int[] { 1, 2, 3 }, collection);
Assert.Contains(0, collection);
Assert.Contains(collection, item => item == 1);

// Assert each items of the collection match the condition
Assert.All(collection, item => Assert.InRange(item, low: 0, high: 10));

// Assert the collection contains 3 items and the items match the conditions (in the declared order)
Assert.Collection(collection,
                item => Assert.Equal(1, item),
                item => Assert.Equal(2, item),
                item => Assert.Equal(3, item));

// Exceptions
var ex1 = Assert.Throws<Exception>(() => Console.WriteLine()); // Return the thrown exception
var ex2 = await Assert.ThrowsAsync<Exception>(() => Task.FromResult(1)); // Return the thrown exception
Assert.Equal("message", ex1.Message);

// Events
var test = new Test();
Assert.Raises<EventArgs>(
    handler => test.MyEvent += handler,
    handler => test.MyEvent -= handler,
    () => test.RaiseEvent());

参数化测试

// 使用 Theory 指定会进行多次测试,使用属性旨定测试的数据

  • InlineData 直接定义数据
  • MemberData 使用静态函数
  • ClassData 实现 IEnumerable 接口
  • CustomDataAttribute 实现 DataAttribute 子类
public class UnitTest1
{
    // You can mix multiple data sources
    // xUnit provides an analyzer to ensure the data sources are valid for the test
    [Theory]
    [InlineData(1, 2, 3)]              // InlineData works for constant values
    [MemberData(nameof(Test1Data))]    // MemberData can be a public static method or property
    [ClassData(typeof(TestDataClass))] // TestDataClass must implement IEnumerable<object[]>
    [CustomDataAttribute]              // Any attribute that inherits from DataAttribute
    public void Test1(int a, int b, int expected)
    {
        Assert.Equal(expected, a + b);
    }

    // The method can return IEnumerable<object[]>
    // You can use TheoryData to strongly type the result
    public static TheoryData<int, int, int> Test1Data()
    {
        var data = new TheoryData<int, int, int>();
        data.Add(18, 24, 42);
        data.Add(6, 7, 13);
        return data;
    }

    public class TestDataClass : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] { 9, 1, 10 };
            yield return new object[] { 9, 10, 19 };
        }

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

    private class CustomDataAttribute : DataAttribute
    {
        public override IEnumerable<object[]> GetData(MethodInfo testMethod)
        {
            yield return new object[] { 2, 3, 4 };
        }
    }
}

初始化、释放

// 使用构造函数进行初始化, 使用 IDisposable 进行释放 // 实现 IAsyncLifetime 进行异步初始化及释放

// The class is instantiated once per test
// This means the constructor/Dispose are called once per test
public class UnitTest1 : IDisposable, IAsyncLifetime
{
    public UnitTest1()
    {
        // Set up (called once per test)
    }

    public void Dispose()
    {
        // Tear down (called once per test)
    }

    public Task InitializeAsync()
    {
        // Async set up (called once per test)
    }

    public Task DisposeAsync()
    {
        // Async Tear down (called once per test)
    }

    [Fact]
    public void Test1()
    {
        // Test
    }

    [Fact]
    public void Test2()
    {
        // Test
    }
}

测试共享数据

实现 IClassFixture 的测试,会在构造函数中注入 T 的实例,并在共享相同的 T 实例


public class UnitTest1 : IClassFixture<MyFixture>
{
    private readonly MyFixture _fixture;

    // All the tests share the same instance of MyFixture
    public UnitTest1(MyFixture fixture)
    {
        _fixture = fixture;
    }

    [Fact]
    public void Test1()
    {
    }

    [Fact]
    public void Test2()
    {
    }
}

使用 Collection 共享数据

定义一个 ICollectionFixture 并定义名称

为指定的测试指定 Collection 名称,这些测试则使用同一个数据

// Define the collection with the fixture
[CollectionDefinition("Collection #1")]
public class Collection1Class : ICollectionFixture<MyFixture> { }

// Declare test in the defined collection
[Collection("Collection #1")]
public class UnitTest1 {}

[Collection("Collection #1")]
public class UnitTest2 {}

并行执行

xUnit 每个类之间并行 ,每个类属于一个测试集合。 而每个测试集合中的测试是串行进行

可以使用 Collection("name") 并不同的类测试进行串行

// Test1 Test2, Test3, Test4 run sequentially because there are in the same collection
// => Runs in 20s , 进行了串行, 一共用了 20 秒
[Collection("Collection #1")]
public class UnitTest1
{
    [Fact]
    public async Task Test1() => await Task.Delay(5000);

    [Fact]
    public async Task Test2() => await Task.Delay(5000);
}

[Collection("Collection #1")]
public class UnitTest2
{
    [Fact]
    public void Test3() => Thread.Sleep(5000);

    [Fact]
    public void Test4() => Thread.Sleep(5000);
}

并行控制编译参数

[assembly: Xunit.CollectionBehaviorAttribute(
                    CollectionBehavior.CollectionPerAssembly,
                    DisableTestParallelization = false,
                    MaxParallelThreads = 4)]

Collection 控制是否并行

在 CollectionDefinition 中使用 DisableParallelization 控制是否并行进行

[CollectionDefinition("Collection #1", DisableParallelization = true)]
public class Collection1Class { }

测试分类

给测试定义一些特性值,然后可以指定运行某些特性值的测试

可以使用 Trait 或是 ITraitAttribute 子类来指定

比如使用 dotnet test --filter "Area=API&Category!=Integration"

// 使用 Trait 属性 
[Trait("Area", "API")] // 指定 Area=API
public class UnitTest1
{
    [Fact]
    [Trait("Category", "Integration")] // 指定 Category=Integration
    [Trait("Issue", "123")] // 指定 Issue=123 
    public void Test1()
    {
    }
}

// 自定义属性
// 1. 实现 ITraitAttribute 并使用 TraitDiscoverer 指定特性值指定类
[TraitDiscoverer("Sample.FunctionalTestDiscoverer", "XUnitTestProject1")]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class FunctionalTestAttribute : Attribute, ITraitAttribute
{
}

// 2. 实现特性值指定类, 通过 GetTraits 返回特性
public sealed class FunctionalTestDiscoverer : ITraitDiscoverer
{
    public IEnumerable<KeyValuePair<string, string>> GetTraits(IAttributeInfo traitAttribute)
    {
        yield return new KeyValuePair<string, string>("Category", "Integration");
    }
}

public class UnitTest1
{
    // 3. 使用特性
    [Fact]
    [FunctionalTest] // Equivalent of [Trait("Category", "Integration")], 相当于 Category=Integration
    public void Test1()
    {
    }
}

输出日志

通过注入 ITestOutputHelper , 实现日志的输出

public class UnitTest1
{
    private readonly ITestOutputHelper _output;

    public UnitTest1(ITestOutputHelper testOutputHelper)
    {
        _output = testOutputHelper;
    }

    [Fact]
    public void Test1()
    {
        _output.WriteLine("This is a log from the test output helper");
    }
}

跳过测试

使用 Skip 并写入原因

[Fact(Skip = "This test is disabled")]
public void Test1() {}

自定义属性

可能可以动态定义

// 1. 实现一个 FactAttribute 子类。根据需要动态定义 Skip 属性  
public sealed class IgnoreOnWindowsFactAttribute : FactAttribute
{
    public IgnoreOnWindowsFactAttribute()
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            Skip = "Ignored on Windows";
        }
    }
}

public class UnitTest1
{
    // 使用属性
    [IgnoreOnWindowsFactAttribute]
    public void Test1() {}
}

针对不同框架进行测试

dotnet test

dotnet test --framework net461

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net461;netcoreapp3.1;net5.0</TargetFrameworks>
  </PropertyGroup>
</Project>

测试名称

使用 DisplayName 指定名称, Fact/Theory 中可以指定

[Fact(DisplayName = "1 + 1 = 2")]
public void Test_that_1_plus_1_eq_2()
{
    Assert.Equal(2, 1 + 1);
}

定义测试名

在根目录下使用测试配置文件 xunit.runner.json

比上面的例子会将 Test_that_1_X2B_1_eq_3_U263A``Test that 1 + 1 = 3 ☺

{
  "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
  "methodDisplay": "method",
  "methodDisplayOptions":   "replaceUnderscoreWithSpace,useOperatorMonikers,useEscapeSequences,replacePeriodWithComma"
}

执行次序

按方法名

// 1. 实现一个 ITestCaseOrderer ,在 OrderTestCases 中定义一个按名称排序的规则     
public class AlphabeticalOrderer : ITestCaseOrderer
    {
        public IEnumerable<TTestCase> OrderTestCases<TTestCase>(
            IEnumerable<TTestCase> testCases) where TTestCase : ITestCase =>
            testCases.OrderBy(testCase => testCase.TestMethod.Method.Name);
    }

// 2. 在测试中使用定义使用 AlphabeticalOrderer 进行排序 
    [TestCaseOrderer("XUnit.Project.Orderers.AlphabeticalOrderer", "XUnit.Project")]
    public class ByAlphabeticalOrder
    {
        [Fact]
        public void Test1() {}

        [Fact]
        public void Test2() {}

        [Fact]
        public void Test3() {}
    }

按自定义属性

定义一个属性,可以填写数值

//  1. 定义一个属性,并可以设置优先级
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class TestPriorityAttribute : Attribute
{
    public int Priority { get; private set; }
    public TestPriorityAttribute(int priority) => Priority = priority;
}

// 2. 实现一个 ITestCaseOrderer, 读出方法属性里的 TestPriorityAttribute.Priority 值,并进行排序
    public class PriorityOrderer : ITestCaseOrderer
    {
        public IEnumerable<TTestCase> OrderTestCases<TTestCase>(
            IEnumerable<TTestCase> testCases) where TTestCase : ITestCase
        {
            // 从 testCases 中读出方法有没有 TestPriority 属性,并读出 Priority 值,进行排序 
        }
    }

// 3. 测试中使用 TestCaseOrderer 指定 PriorityOrderer 定义顺序
[TestCaseOrderer("XUnit.Project.Orderers.PriorityOrderer", "XUnit.Project")]
public class ByPriorityOrder
{
    [Fact, TestPriority(5)] public void Test3() {}
    [Fact, TestPriority(0)] public void Test2B() {}
    [Fact] public void Test2A() {}
    [Fact, TestPriority(-5)] public void Test1() {}
}

按集合名称进行

实现 ITestCollectionOrderer

// 1. 实现 ITestCollectionOrderer, 并实现按名称进行排序
public class DisplayNameOrderer : ITestCollectionOrderer
{
        public IEnumerable<ITestCollection> OrderTestCollections(
            IEnumerable<ITestCollection> testCollections) =>
            testCollections.OrderBy(collection => collection.DisplayName);
}

// 2. 使用编译开关 TestCollectionOrderer 指定排序类进行排序 
// 注意集合默认并行, 所以要强制为串行, 不然指定顺序没有意义
// Need to turn off test parallelization so we can validate the run order
[assembly: CollectionBehavior(DisableTestParallelization = true)]
[assembly: TestCollectionOrderer("XUnit.Project.Orderers.DisplayNameOrderer", "XUnit.Project")]

namespace XUnit.Project
{
    [Collection("Xzy Test Collection")] public class TestsInCollection1 { }
    [Collection("Abc Test Collection")] public class TestsInCollection2 { }
    [Collection("Mno Test Collection")] public class TestsInCollection3 { }
}