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