.net、C#单元测试xUnit
创始人
2024-03-08 00:15:58

xUnit单元测试

测试的分类

  • 单元测试:对某个类或者某个方法进行测试

  • 集成测试:可使用Web资源、数据库数据进行测试

  • 皮下测试:在Web中对controller下的节点测试

  • UI测试:对界面的功能进行测试

    程序员主要关注单元测试集成测试

xUnit

xUnit是一个可对.net进行测试的框架,支持.Net Framework、.Net Core、.Net Standard、UWP、Xamarin。

  • 支持多平台
  • 并行测试
  • 数据驱动测试
  • 可扩展

测试一般针对Public方法进行测试,如果是私有方法需要改变修饰符才能进行测试。同时测试项目需添加对被测项目的引用,同时测试项目需要引入xUnit框架库。

最简单的测试

  1. 创建一个.net core类库:Demo,添加一个Calculator类

    namespace Demo
    {public class Calculator{public int Add(int x,int y){return x + y;}}
    }
    
  2. 在同一解决方案,创建一个xUnit测试项目:DemoTest

    命名规则:一般是项目名+Test命名测试项目。创建一个类:CalculatorTests:

    public class CalculatorTests
    {[Fact]public void ShouldAddEquals5() //注意命名规范{//Arrange var sut = new Calculator(); //sut-system under test,通用命名//Actvar result = sut.Add(3, 2);//AssertAssert.Equal(5, result);}
    }
    
  3. 运行测试,s自带的测试资源管理器,找到测试项目,选择运行

测试的三个阶段

  • Arrange: 在这里做一些准备。例如创建对象实例,数据,输入等。
  • Act: 在这里执行生产代码并返回结果。例如调用方法或者设置属性。
  • Assert:在这里检查结果,会产生测试通过或者失败两种结果。

Assert详解

xUnit提供了以下类型的Assert

类型行为
boolTrue/False
string是否相等、空、以什么开始/结束、是否包含、是否匹配正则
数值是否相等、是否在范围内、浮点的精度
集合内容是否相等、是否包含、是否包含某种条件的元素、每个元素是否满足条件
事件自定义事件、.net事件
Object是否为某种类型、是否继承某类型

实例

创建一个类库

 public class Patient : Person, INotifyPropertyChanged{public Patient(){IsNew = true;BloodSugar = 4.900003f;History = new List();//throw new InvalidOperationException("not able to create"); 测试异常使用}public string FullName => $"{FirstName} {LastName}";public int HeartBeatRate { get; set; }public bool IsNew { get; set; }public float BloodSugar { get; set; }public List History { get; set; }/// 事件public event EventHandler PatientSlept;public void OnPatientSleep(){PatientSlept?.Invoke(this, EventArgs.Empty);}public void Sleep(){OnPatientSleep();}public void IncreaseHeartBeatRate(){HeartBeatRate = CalculateHeartBeatRate() + 2;OnPropertyChanged(nameof(HeartBeatRate));}private int CalculateHeartBeatRate(){var random = new Random();return random.Next(1, 100);}public event PropertyChangedEventHandler PropertyChanged;protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}}

Bool类型测试

[Fact] //必须有这个特性
public void BeNewWhenCreated()
{// Arrangevar patient = new Patient();// Actvar result = patient.IsNew;// AssertAssert.True(result);
}

String测试

[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]
public void HaveDefaultBloodSugarWhenCreated()
{var p = new Patient();var bloodSugar = p.BloodSugar;Assert.Equal(4.9f, bloodSugar,5); //判断是否相等,最后一个是精度,很重要Assert.InRange(bloodSugar, 3.9, 6.1);//判断是否在某一范围内
}

空值判断

[Fact]
public void HaveNoNameWhenCreated()
{var p = new Patient();Assert.Null(p.FirstName);Assert.NotNull(_patient);
}

集合测试

[Fact]
public void HaveHadAColdBefore()
{//Arrangevar _patient = new Patient();//Actvar diseases = new List{"感冒","发烧","水痘","腹泻"};_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);}

object测试

[Fact]
public void BeAPerson()
{var p = new Patient();var p2 = new Patient();Assert.IsNotType(p); //测试对象是否相等,注意这里为falseAssert.IsType(p);Assert.IsAssignableFrom(p);//判断对象是否继承自Person,true//判断是否为同一个实例Assert.NotSame(p, p2);//Assert.Same(p, p2);}

异常测试

[Fact]
public void ThrowException() 
{var p = new Patient();//判断是否返回指定类型的异常var ex = Assert.Throws(()=> { p.NotAllowed(); });//判断异常信息是否相等Assert.Equal("not able to create", ex.Message);
}

判断是否触发事件

[Fact]
public void RaizeSleepEvent()
{var p = new Patient();Assert.Raises(handler=>p.PatientSlept+=handler,handler=>p.PatientSlept -= handler,() => p.Sleep());
}

判断属性改变是否触发事件

[Fact]
public void RaisePropertyChangedEvent()
{var p = new Patient();Assert.PropertyChanged(p, nameof(p.HeartBeatRate),() => p.IncreaseHeartBeatRate());
}

分组

使用trait特性,对测试进行分组:[Trait(“GroupName”,“Name”)] 可以作用于方法级和Class级别。相同的分组使用相同的特性。

[Fact]
[Trait("Category","New")]//凡是使用这个特性且组名一样,则分到一个组中
public void BeNewWhenCreated()
{...}

忽略测试

在测试方法上加上特性[Fact(Skip="不跑这个测试")]

自定义输出内容

使用ITestOutputHelper可以自定义在测试时的输出内容

public class PatientShould:IClassFixture,IDisposable
{private readonly ITestOutputHelper _output;public PatientShould(ITestOutputHelper output,LongTimeFixture fixture){this._output = output;}[Fact]public void BeNewWhenCreated(){_output.WriteLine("第一个测试");}
}

常用技巧

  • 减少new对象,减少new对象,可以在构造函数中new,在方法中使用。
  • 测试类实现IDispose接口,测试完释放资源,注意每个测试结束后都会调用Dispose方法。

共享上下文

某个方法需要执行很长时间,而在构造函数中new时,每个测试跑的时候都会new对象或者执行方法,这是导致测试很慢。解决方法:

模拟运行长时间的任务

public class LongTimeTask
{public LongTimeTask(){Thread.Sleep(2000);}
}

相同测试类

  1. 创建一个类:
public class LongTimeFixture{public LongTimeTask Task { get; }public LongTimeFixture(){Task = new LongTimeTask();}}
}
  1. 测试类实现IClassFixture接口,并在构造函数中使用依赖注入的方式获取方法
public class PatientShould:IClassFixture,IDisposable
{private readonly Patient _patient;private readonly LongTimeTask _task;public PatientShould(ITestOutputHelper output,LongTimeFixture fixture){this._output = output;_task = fixture.Task;//获取方法}
}
//这样的话其实只有一个LongTimeTask实例,所以要保证该实例不能有副作用

不同测试类

如果多个测试类都要用到相同的耗时任务,则可以这样用

  1. 添加一个类
    [CollectionDefinition("Lone Time Task Collection")]public class TaskCollection:ICollectionFixture{}
  1. 在使用的类上加上[CollectionDefinition("Lone Time Task Collection")]注意里面的字符串要相同
[Collection("Lone Time Task Collection")]
public class AAAShould:IClassFixture,IDisposable
{private readonly LongTimeTask _task;public PatientShould(LongTimeFixture fixture){_task = fixture.Task;//获取方法}
}
[Collection("Lone Time Task Collection")]
public class BBBShould:IClassFixture,IDisposable
{private readonly LongTimeTask _task;public BBBShould(LongTimeFixture fixture){_task = fixture.Task;//获取方法}
}
//此时这两个类中测试方法都会共享一个LongTimeFixture实例

数据共享

1.[Theory]

可以写有构造参数的测试方法,使用InlineData传递数据

[Theory]
[InlineData(1,2,3)]
[InlineData(2,2,4)]
[InlineData(3,3,6)]
public void ShouldAddEquals(int operand1,int operand2,int expected)
{//Arrangevar sut = new Calculator(); //sut-system under test//Actvar result = sut.Add(operand1, operand2);//AssertAssert.Equal(expected, result);
}

2.[MemberData]

可以在多个测试中使用

  1. 创建一个类
    public  class CalculatorTestData{private static readonly List Data = new List{new object[]{ 1,2,3},new object[]{ 1,3,4},new object[]{ 2,4,6},new object[]{ 0,1,1},};public static IEnumerable TestData => Data;}
  1. 使用MemberData
[Theory]
[MemberData(nameof(CalculatorTestData.TestData),MemberType =typeof(CalculatorTestData))]
public void ShouldAddEquals2(int operand1, int operand2, int expected)
{//Arrangevar sut = new Calculator(); //sut-system under test//Actvar result = sut.Add(operand1, operand2);//AssertAssert.Equal(expected, result);
}

3.使用外部数据

  1. 读取外部集合类
//    读取文件并返回数据集合 必须是IEnumerable
public class CalculatorCsvData
{public static IEnumerable TestData{get{//把csv文件中的数据读出来,转换string[] csvLines = File.ReadAllLines("Data\\TestData.csv");var testCases = new List();foreach (var csvLine in csvLines){IEnumerable values = csvLine.Trim().Split(',').Select(int.Parse);object[] testCase = values.Cast().ToArray();testCases.Add(testCase);}return testCases;}}}
 
  1. 使用
[Theory]
[MemberData(nameof(CalculatorCsvData.TestData), MemberType = typeof(CalculatorCsvData))]
public void ShouldAddEquals3(int operand1, int operand2, int expected)
{//Arrangevar sut = new Calculator(); //sut-system under test//Actvar result = sut.Add(operand1, operand2);//AssertAssert.Equal(expected, result);
}

4.DataAttribute

  1. 自定义特性
    public class CalculatorDataAttribute : DataAttribute{public override IEnumerable GetData(MethodInfo testMethod){yield return new object[] { 0, 100, 100 };yield return new object[] { 1, 99, 100 };yield return new object[] { 2, 98, 100 };yield return new object[] { 3, 97, 100 };}}
  1. 使用
[Theory]
[CalculatorDataAttribute]
public void ShouldAddEquals4(int operand1, int operand2, int expected)
{//Arrangevar sut = new Calculator(); //sut-system under test//Actvar result = sut.Add(operand1, operand2);//AssertAssert.Equal(expected, result);
}

相关内容

热门资讯

猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...