在 .NET Core 项目中使用 xUnit.NET进行单元测试
一、为什么要进行自动化测试
自动化测试的分类
单元测试:可以测试某个类或一个类的某个方法或功能,它具有非常好的深度。(大多是对行为进行测试public)
集成测试:没有单元测试那么细致,可以测试功能系统的组合,以及数据库及文件系统的测试。
皮下测试:具有很好的覆盖率,对于一个MVC结构的应用来说,它就是针对刚好在Controller下面一层的测试;对于Web Services来说,就是对于节点下面的测试。
UI测试:深度欠佳。
测试阶段
xUnit.NET
xUnit是一个测试框架,可以针对.net/core进行测试。
测试项目需引用被项目从而对其进行测试,测试项目同时需要引用xUnit库。测试编写好后,用Test Runner来运行测试。Test Runner可以读取测试代码,并且会知道我们所使用的测试框架,然后执行,并显示结果。目前可用的Test Runner包括vs自带的Test Explorer,或者dotnet core命令行,以及第三方工具,例如resharper等等。
特点:支持多平台/运行时,并行测试,数据驱动测试,可扩展。
官网地址:https://xunit.net/
简单的测试项目
新建一个计算器的Demo之后,再添加xUnit测试项目DemoTest,结构如下:
首先在Demo中写一个计算器,如下:
public class Calculator { public int Add(int x,int y) { return x + y; } }
然后去写测试代码:
public class CalculatorTests { [Fact] public void ShouldAdd() { //Arrange var sut = new Calculator();//sut-System Under Test //Act var result = sut.Add(x:1,y:2); //Assert Assert.Equal(expected: 3, actual: result); } }
我们用VS自带的测试器进行测试,如下:
二、Assert
Assert基于代码的返回值、对象的最终状态、事件是否发生等情况来评估测试的结果。
Assert的jieg结果可能是Pass或者Fail,如果所有的asserts都pass了,那么整个测试就pass了;如果有任何assert fail了,那么测试就fail了。
Assert类型:
现在新建一个Patient类:
public class Patient { public Patient() { IsNew = true; } public string FirstName { get; set; } public string LastName { get; set; } public string FullName => $"{FirstName} {LastName}"; public int HeartBeatRate { get; set; } public bool IsNew { get; set; } public void IncreaseHeartBeatRate() { HeartBeatRate = CalculateHeartBeatRate() + 2; } private int CalculateHeartBeatRate() { var random = new Random(); return random.Next(1, 100); } }
测试类PatientShould,首先写一个bool测试:
public class PatientShould { [Fact] public void HaveHeartBeatWhenNew() { //Arrange var patient = new Patient(); //Act var result = patient.IsNew; //Assert Assert.True(result); } }
string测试:
[Fact] public void CalculateFullName() { var p = new Patient { FirstName = "Nick", LastName = "Carter" }; var fullName = p.FullName; Assert.Equal(expected:"Nick Carter",actual: fullName); Assert.StartsWith(expectedStartString: "Nick", actualString: fullName); Assert.EndsWith(expectedEndString: "Carter", actualString: fullName); Assert.Contains(expectedSubstring: "Nick Carter", actualString: fullName); }
数值型:
[Fact] public void HaveDefaultBloodSugarWhenCreated() { var p = new Patient(); var bloodSugar = p.BloodSuger; Assert.Equal(expected: 4.9f, actual: bloodSugar, precision: 5); Assert.InRange(actual:bloodSugar,low:3.9f,high:6.1f); }
NULL:
[Fact] public void HaveNoNameWhenCreate() { var p = new Patient(); Assert.Null(p.FirstName); Assert.NotNull(p); }
集合:
[Fact] public void HaveHadAcoldBefore() { var p = new Patient(); p.History.Add("感冒"); p.History.Add("发烧"); p.History.Add("肺炎"); Assert.Contains(expected:"感冒",p.History); Assert.DoesNotContain(expected: "高血压", p.History); Assert.Contains(p.History,filter:x=>x.StartsWith(value:"肺")); }
Object:
新建一个Person类,Patient继承Person:
[Fact] public void BeAPerson() { var p = new Patient(); var p1 = new Patient(); Assert.IsType<Patient>(p); Assert.IsNotType<Person>(p); Assert.IsAssignableFrom<Person>(p); //Assert.Same(p, p1); }
测试:
异常:
在Patient类里初始化 throw new InvalidOperationException(message:"Not able Create");
[Fact] public void ThrowExceptionWhenErrowOccurred() { Assert.Throws<InvalidOperationException>(testCode: () => new Patient()); }
测试:
三、分组,忽略,Log,上下文
1、测试分组
[Trait("Name","Vaue")] 方法级,Class级
[Fact] [Trait("Category","New")] public void HaveHeartBeatWhenNew() { //Arrange var patient = new Patient(); //Act var result = patient.IsNew; //Assert Assert.True(result); }
[Trait("Category", "Calc")] public class CalculatorTests { [Fact] public void ShouldAdd() { //Arrange var sut = new Calculator();//sut-System Under Test //Act var result = sut.Add(x:1,y:2); //Assert Assert.Equal(expected: 3, actual: result); } }
2、忽略测试
[Fact(Skip = "不跑这个测试")]
3、自定义测试输出信息
ITestOutputHelper
private readonly ITestOutputHelper _outputHelper; public PatientShould(ITestOutputHelper testOutput) { _outputHelper = testOutput; } [Fact] [Trait("Category","New")] public void HaveHeartBeatWhenNew() { _outputHelper.WriteLine("第一个测试"); //Arrange var patient = new Patient(); //Act var result = patient.IsNew; //Assert Assert.True(result); }
4、共享上下文
public class LongTimeTask { public LongTimeTask() { Thread.Sleep(millisecondsTimeout:2000); } }
private readonly ITestOutputHelper _outputHelper; private readonly LongTimeTask longTimeTask; public PatientShould(ITestOutputHelper testOutput) { _outputHelper = testOutput; longTimeTask = new LongTimeTask(); }
任意跑两个测试方法:
每个测试方法时间都是2秒了,为了解决这个问题
public class LongTimeTaskFixture:IDisposable { public LongTimeTask Task { get; set; } public LongTimeTaskFixture() { Task = new LongTimeTask(); } public void Dispose() { } }
private readonly ITestOutputHelper _outputHelper; private readonly LongTimeTask longTimeTask; public PatientShould(ITestOutputHelper testOutput,LongTimeTaskFixture timeTaskFixture) { _outputHelper = testOutput; longTimeTask = timeTaskFixture.Task; }
多个类共用一个
[CollectionDefinition("Long Time Task Collection")] public class TaskCollection:ICollectionFixture<LongTimeTaskFixture> { }
[Collection("Long Time Task Collection")]
四、数据驱动测试
[Theory]
[Fact] public void ShouldAdd() { //Arrange var sut = new Calculator();//sut-System Under Test //Act var result = sut.Add(x:1,y:2); //Assert Assert.Equal(expected: 3, actual: result); } [Fact] public void ShouldAdd2() { //Arrange var sut = new Calculator();//sut-System Under Test //Act var result = sut.Add(x: 1, y: 3); //Assert Assert.Equal(expected: 4, actual: result); }
这种写法比较傻,重复代码多,下面是新写法:
1、 [InlineData(x,y,.....)]
[Theory] [InlineData(1, 2, 3)] [InlineData(2, 2, 4)] [InlineData(3, 2, 5)] public void ShouldAdd(int x,int y,int expected) { //Arrange var sut = new Calculator();//sut-System Under Test //Act var result = sut.Add(x,y); //Assert Assert.Equal(expected, actual: result); }
2、[MemberData(nameof(xxx),MemberType=typeof(xxx))]
public class CalculatorTestsData { private static readonly List<object[]> Data = new List<object[]> { new object[]{1,2,3}, new object[]{2,3,5}, new object[]{2,4,6} }; public static IEnumerable<object[]> TestData => Data; }
[Theory] [MemberData(nameof(CalculatorTestsData.TestData),MemberType =typeof(CalculatorTestsData))] public void ShouldAdd(int x,int y,int expected) { //Arrange var sut = new Calculator();//sut-System Under Test //Act var result = sut.Add(x,y); //Assert Assert.Equal(expected, actual: result); }
执行结果:
3、外部数据源
新建一个csv文件,如下:
public class CalculatorCvsData { public static IEnumerable<object[]> TestData { get { string[] csvlines = File.ReadAllLines(path:"Data\\TestData.csv"); var testCases = new List<object[]>(); foreach(var csvline in csvlines) { IEnumerable<int> values = csvline.Split(separator: ',').Select(int.Parse); object[] testCase = values.Cast<object>().ToArray(); testCases.Add(testCase); } return testCases; } } }
[Theory] [MemberData(nameof(CalculatorCvsData.TestData),MemberType =typeof(CalculatorCvsData))] public void ShouldAdd(int x,int y,int expected) { //Arrange var sut = new Calculator();//sut-System Under Test //Act var result = sut.Add(x,y); //Assert Assert.Equal(expected, actual: result); }
4、自定义Data Attribute
public class CalculatorDataAttribute: DataAttribute { public override IEnumerable<object[]> GetData(MethodInfo methodInfo) { yield return new object[] { 0, 100, 100 }; yield return new object[] { 1, 100, 101 }; yield return new object[] { 200, 100, 300 }; yield return new object[] { 15, 100, 115}; } }
[CalculatorData] public void ShouldAdd(int x,int y,int expected) { //Arrange var sut = new Calculator();//sut-System Under Test //Act var result = sut.Add(x,y); //Assert Assert.Equal(expected, actual: result); }
运行测试:
至此,xunit.Net学习到此结束了。