Xunit-介绍
xUnit是.NET生态中最流行的单元测试框架之一,以其简洁的设计、强大的功能和灵活的扩展性著称。
测试项目需引用被测试项目从而对其进行测试,测试项目同时需要 引用xUnit库。测试编写好后,用Test Runner来运行测试。Test Runner可以读取测试代码,并且会知道我们所使用的测试框架, 然后执行,并显示结果。目前可用的Test Runner包括vs自带的 Test Explorer,或者dotnet core命令行dotnet test,以及第三方工具,例如 resharper等等。
测试发现机制:xUnit通过反射扫描程序集中的公共类,查找带有[Fact]或[Theory]特性的方法作为测试用例。测试类不需要继承任何基类或实现特定接口,保持轻量级设计。
入门
1、创建新项目,选择项目模板'xunit'
2、
public class CalculatorTests
{
[Fact]
public void ShouldAddEquals5() //注意命名规范
{
//Arrange
var sut = new Calculator();
var result = sut.Add(3, 2);
//Assert
Assert.Equal(5, result);
}
}
public class Calculator
{
public int Add(int x, int y)
{
return x + y;
}
}
3、运行单元测试
测试的三个阶段:AAA
- Arrange: 在这里做一些先决的设定。例如创建对象实例,数据,输入等。
- Act: 在这里执行生产代码并返回结果。例如调用方法或者设置属性。
- Assert:在这里检查结果,会产生测试通过或者失败两种结果。
[Fact]
public void ShouldAddEquals5() //注意命名规范
{
//Arrange
var sut = new Calculator(); //sut-system under test,通用命名
//Act
var result = sut.Add(3, 2);
//Assert
Assert.Equal(5, result);
}
Assert
Assert基于代码的返回值、对象的最终状态、事件是否发生等情况来评估测试的结果
Assert的结果可能是Pass或者Fail
如果所有的asserts都通过了,那么整个测试就通过了。
如果任何assert 失败了,那么结果就失败了。
一个test里可以有一个或多个asserts
字符串结果测试
[Fact]
public void HaveCorrectFullName()
{
//var patient = new Patient();
_patient.FirstName = "Nick";
_patient.LastName = "Carter";
var fullName = _patient.FullName;
Assert.Equal("Nick Carter", fullName); //相等
Assert.StartsWith("Nick", fullName);//以开头
Assert.EndsWith("Carter", fullName);//以结尾
Assert.Contains("Carter", fullName);//包含
Assert.Contains("Car", fullName);
Assert.NotEqual("CAR", fullName);//不相等
Assert.Matches(@"^[A-Z][a-z]*\s[A-Z][a-z]*", fullName);//正则表达式
}
数字结果测试
[Fact]
[Trait("Category", "New")]
public void HaveDefaultBloodSugarWhenCreated()
{
var p = new Patient();
var bloodSugar = p.BloodSugar;
Assert.Equal(4.9f, bloodSugar,5); //判断是否相等
Assert.InRange(bloodSugar, 3.9, 6.1);//判断是否在某一范围内
}
判断null,not null
[Fact]
public void HaveNoNameWhenCreated()
{
var p = new Patient();
Assert.Null(p.FirstName);
Assert.NotNull(_patient);
}
集合测试
[Fact]
public void HaveHadAColdBefore()
{
//Arrange
var _patient = new Patient();
//Act
var diseases = new List<string>
{
"感冒",
"发烧",
"水痘",
"腹泻"
};
_patient.History.Add("发烧");
_patient.History.Add("感冒");
_patient.History.Add("水痘");
_patient.History.Add("腹泻");
//Assert
//判断集合是否含有或者不含有某个元素
Assert.Contains("感冒",_patient.History);
Assert.DoesNotContain("心脏病", _patient.History);
//判断p.History至少有一个元素,该元素以水开头
Assert.Contains(_patient.History, x => x.StartsWith("水"));
//判断集合的长度
Assert.All(_patient.History, x => Assert.True(x.Length >= 2));
//判断集合是否相等,这里测试通过,说明是比较集合元素的值,而不是比较引用
Assert.Equal(diseases, _patient.History);
}
测试对象
/// <summary>
/// 测试Object
/// </summary>
[Fact]
public void BeAPerson()
{
var p = new Patient();
var p2 = new Patient();
Assert.IsNotType<Person>(p); //测试对象是否相等,注意这里为false
Assert.IsType<Patient>(p);
Assert.IsAssignableFrom<Person>(p);//判断对象是否继承自Person,true
//判断是否为同一个实例
Assert.NotSame(p, p2);
//Assert.Same(p, p2);
}
判断是否发生异常
/// <summary>
/// 判断是否发生异常
/// </summary>
[Fact]
public void ThrowException() //注意不能使用ctrl+R,T快捷键,因为会中断测试,抛出异常
{
var p = new Patient();
//判断是否返回指定类型的异常
var ex = Assert.Throws<InvalidOperationException>(()=> { p.NotAllowed(); });
//判断异常信息是否相等
Assert.Equal("not able to create", ex.Message);
}
判断是否触发事件
/// <summary>
/// 判断是否触发事件
/// </summary>
[Fact]
public void RaizeSleepEvent()
{
var p = new Patient();
Assert.Raises<EventArgs>(
handler=>p.PatientSlept+=handler,
handler=>p.PatientSlept -= handler,
() => p.Sleep());
}
判断属性改变是否触发事件
/// <summary>
/// 测试属性改变事件是否触发
/// </summary>
[Fact]
public void RaisePropertyChangedEvent()
{
var p = new Patient();
Assert.PropertyChanged(p, nameof(p.HeartBeatRate),
() => p.IncreaseHeartBeatRate());
}
分组
使用trait特性,对测试进行分组:[Trait("Name","Value")] 可以作用于方法级和Class级别。可以在运行测试时选择测试哪些分组
[Fact]
[Trait("Category", "New")]
public void ShouldAddEquals5()
{
//Arrange
var sut = new Calculator();
var result = sut.Add(3, 2);
//Assert
Assert.Equal(5, result);
}
[Fact]
[Trait("Category", "New2")]
public void ShouldAddEquals6()
{
//Arrange
var sut = new Calculator();
var result = sut.Add(3, 2);
//Assert
Assert.Equal(5, result);
}
在dotnet cli中分组测试:
dotnew test --filter “Category=New” //运行单个分类测试
dotnew test --filter “Category=New|Category=New2”//运行多个分类测试
自定义测试输出内容
忽略测试
使用特性:[Fact(Skip="不跑这个测试")],可以忽略测试,忽略测试图标为黄色警告
测试类型
[Fact]:用于不需要参数的测试
[Theory]:用于参数化测试,可以接受多组输入数据
数据驱动测试
| 特性 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| InlineData | 简单、静态数据 | 简单直接,代码量少 | 不支持复杂对象,数据量大时代码冗长 |
| MemberData | 复杂或动态数据 | 支持复杂对象,数据可动态生成 | 需要额外的方法或属性来提供数据 |
| ClassData | 复杂对象和复用数据 | 支持复杂对象,可复用数据提供逻辑 | 需要创建单独的类 |
| 自定义DataAttribute | 需要完全控制数据生成逻辑的场景 | 无限灵活性,可访问测试上下文 | 实现复杂度较高 |
InlineData
InlineData是xUnit中用于参数化测试的特性之一,它与[Theory]属性配合使用,可以为测试方法提供多组输入数据,xUnit会为每组数据生成一个独立的测试用例
InlineData适用于简单、静态的测试数据场景
InlineData不能直接传递复杂对象作为参数,因为它的参数必须是编译时常量
[Theory]
[InlineData(1,2)]
[InlineData(1, 3)]
[InlineData(1, 4)]
[InlineData(2, 3)]
public void ShouldAddEquals6(int num1,int num2)
{
//Arrange
var sut = new Calculator();
var result = sut.Add(num1, num2);
//Assert
Assert.Equal(5, result);
}
MemberData
MemberData允许从测试类的静态成员(字段、属性或方法)获取测试数据。数据提供者必须返回IEnumerable<object[]>类型的数据。
public class CalculatorTests
{
// 静态方法作为数据源,也可以是非静态的
public static IEnumerable<object[]> GetTestData()
{
yield return new object[] { 1, 2, 3 };
yield return new object[] { -1, 1, 0 };
yield return new object[] { 0, 0, 0 };
}
[Theory]
[MemberData(nameof(GetTestData))]
public void Add_TwoNumbers_ReturnsSum(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.Equal(expected, result);
}
}
ClassData详解
ClassData通过专门的类来提供测试数据,该类必须实现IEnumerable<object[]>接口。
public class PersonTests
{
[Theory]
[ClassData(typeof(PersonTestData))]
public void HasMobile_ReturnsCorrectResult(Person person, bool expected)
{
var result = !string.IsNullOrWhiteSpace(person.Mobile);
Assert.Equal(expected, result);
}
}
public class PersonTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[] { new Person { Id = 1, Name = "张三" }, false };
yield return new object[] { new Person { Id = 2, Name = "李四", Mobile = "139XXXXXXXX" }, true };
yield return new object[] { new Person { Id = 3, Name = "王五", Mobile = "" }, false };
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
自定义DataAttribute的基本实现
通过继承DataAttribute类并重写GetData方法,可以创建完全定制的数据源特性。这种方式比MemberData/ClassData更灵活,允许在运行时动态生成测试数据
public class CustomDataAttribute : DataAttribute
{
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
yield return new object[] { 1, 2, 3 };
yield return new object[] { -1, -2, -3 };
yield return new object[] { int.MaxValue, 1, int.MinValue }; // 溢出用例
}
}
[Theory]
[CustomData]
public void Add_WithCustomAttribute(int a, int b, int expected)
{
Assert.Equal(expected, new Calculator().Add(a, b));
}

浙公网安备 33010602011771号