xUnit单元测试
测试的分类
单元测试:对某个类或者某个方法进行测试
集成测试:可使用Web资源、数据库数据进行测试
皮下测试:在Web中对controller下的节点测试
UI测试:对界面的功能进行测试
程序员主要关注单元测试和集成测试
xUnit
xUnit是一个可对.net进行测试的框架,支持.Net Framework、.Net Core、.Net Standard、UWP、Xamarin。
支持多平台
并行测试
数据驱动测试
可扩展
测试一般针对Public方法进行测试,如果是私有方法需要改变修饰符才能进行测试。同时测试项目需添加对被测项目的引用,同时测试项目需要引入xUnit框架库。
最简单的测试
创建一个.net core类库:Demo,添加一个Calculator类
namespace Demo { public class Calculator { public int Add(int x,int y) { return x + y; } } }
在同一解决方案,创建一个xUnit测试项目:DemoTest
命名规则:一般是项目名+Test命名测试项目。创建一个类:CalculatorTests:
public class CalculatorTests { [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); } }
运行测试,s自带的测试资源管理器,找到测试项目,选择运行
测试的三个阶段
Arrange: 在这里做一些准备。例如创建对象实例,数据,输入等。
Act: 在这里执行生产代码并返回结果。例如调用方法或者设置属性。
Assert:在这里检查结果,会产生测试通过或者失败两种结果。
Assert详解
xUnit提供了以下类型的Assert
类型 | 行为 |
bool | True/False |
string | 是否相等、空、以什么开始/结束、是否包含、是否匹配正则 |
数值 | 是否相等、是否在范围内、浮点的精度 |
集合 | 内容是否相等、是否包含、是否包含某种条件的元素、每个元素是否满足条件 |
事件 | 自定义事件、.net事件 |
Object | 是否为某种类型、是否继承某类型 |
实例
创建一个类库
public class Patient : Person, INotifyPropertyChanged { public Patient() { IsNew = true; BloodSugar = 4.900003f; History = new List<string>(); //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<string> History { get; set; } /// 事件 public event EventHandler<EventArgs> 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() { // Arrange var patient = new Patient(); // Act var result = patient.IsNew; // Assert Assert.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() { //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); }
object测试
[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); }
异常测试
[Fact] public void ThrowException() { var p = new Patient(); //判断是否返回指定类型的异常 var ex = Assert.Throws<InvalidOperationException>(()=> { p.NotAllowed(); }); //判断异常信息是否相等 Assert.Equal("not able to create", ex.Message); }
判断是否触发事件
[Fact] public void RaizeSleepEvent() { var p = new Patient(); Assert.Raises<EventArgs>( 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<LongTimeFixture>,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); } }
相同测试类
创建一个类:
public class LongTimeFixture { public LongTimeTask Task { get; } public LongTimeFixture() { Task = new LongTimeTask(); } } }
测试类实现
IClassFixture<LongTimeFixture>
接口,并在构造函数中使用依赖注入的方式获取方法
public class PatientShould:IClassFixture<LongTimeFixture>,IDisposable { private readonly Patient _patient; private readonly LongTimeTask _task; public PatientShould(ITestOutputHelper output,LongTimeFixture fixture) { this._output = output; _task = fixture.Task;//获取方法 } } //这样的话其实只有一个LongTimeTask实例,所以要保证该实例不能有副作用
不同测试类
如果多个测试类都要用到相同的耗时任务,则可以这样用
添加一个类
[CollectionDefinition("Lone Time Task Collection")] public class TaskCollection:ICollectionFixture<LongTimeFixture> { }
在使用的类上加上
[CollectionDefinition("Lone Time Task Collection")]
注意里面的字符串要相同
[Collection("Lone Time Task Collection")] public class AAAShould:IClassFixture<LongTimeFixture>,IDisposable { private readonly LongTimeTask _task; public PatientShould(LongTimeFixture fixture) { _task = fixture.Task;//获取方法 } } [Collection("Lone Time Task Collection")] public class BBBShould:IClassFixture<LongTimeFixture>,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) { //Arrange var sut = new Calculator(); //sut-system under test //Act var result = sut.Add(operand1, operand2); //Assert Assert.Equal(expected, result); }
2.[MemberData]
可以在多个测试中使用
创建一个类
public class CalculatorTestData { private static readonly List<object[]> Data = new List<object[]> { new object[]{ 1,2,3}, new object[]{ 1,3,4}, new object[]{ 2,4,6}, new object[]{ 0,1,1}, }; public static IEnumerable<object[]> TestData => Data; }
使用MemberData
[Theory] [MemberData(nameof(CalculatorTestData.TestData),MemberType =typeof(CalculatorTestData))] public void ShouldAddEquals2(int operand1, int operand2, int expected) { //Arrange var sut = new Calculator(); //sut-system under test //Act var result = sut.Add(operand1, operand2); //Assert Assert.Equal(expected, result); }
3.使用外部数据
读取外部集合类
// 读取文件并返回数据集合 必须是IEnumerable public class CalculatorCsvData { public static IEnumerable<object[]> TestData { get { //把csv文件中的数据读出来,转换 string[] csvLines = File.ReadAllLines("Data\\TestData.csv"); var testCases = new List<object[]>(); foreach (var csvLine in csvLines) { IEnumerable<int> values = csvLine.Trim().Split(',').Select(int.Parse); object[] testCase = values.Cast<object>().ToArray(); testCases.Add(testCase); } return testCases; } } }
使用
[Theory] [MemberData(nameof(CalculatorCsvData.TestData), MemberType = typeof(CalculatorCsvData))] public void ShouldAddEquals3(int operand1, int operand2, int expected) { //Arrange var sut = new Calculator(); //sut-system under test //Act var result = sut.Add(operand1, operand2); //Assert Assert.Equal(expected, result); }
4.DataAttribute
自定义特性
public class CalculatorDataAttribute : DataAttribute { public override IEnumerable<object[]> 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 }; } }
使用
[Theory] [CalculatorDataAttribute] public void ShouldAddEquals4(int operand1, int operand2, int expected) { //Arrange var sut = new Calculator(); //sut-system under test //Act var result = sut.Add(operand1, operand2); //Assert Assert.Equal(expected, result); }