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

参考:
https://www.cnblogs.com/AlexanderZhao/p/12369732.html

posted @ 2025-07-19 10:03  .Neterr  阅读(164)  评论(0)    收藏  举报