# .NET中的测试 测试类型 - 单元测试(`Unit tests`):测试单个组件或方法,与数据库、文件、外界交互无关 - 集成测试(Integration tests):测试多个组件或方法,与数据库、文件、外界交互有关 - 压力测试(Load tests):测试系统对压力的承受程度,比如并发用户、并发请求等 测试框架 - `xUnit`:来自`NUnit v2`的作者,包括了最新的测试技术和思想。属于`.NET基金会`下的项目。 - `NUnit`:从`JUnit`演变而来,最新的版本增加了很多新功能。属于`.NET基金会`下的项目。 - `MSTest`:微软的测试框架,支持在`.NET CLI`和`Visual Studio`中的扩展 `.NET CLI` `.NET CLI`集成了`IDE`下的大部分功能,通过使用`dotnet test`命令运行。可以被用到持续集成交付管道中,也可以被用在测试自动化任务中。 `IDE` 相比`.NET CLI`有更多的功能,比如`Live Unit Testing`是`IDE`独有的测试功能。 # 单元测试的最佳实践 为什么需要单元测试 - 相比功能测试更少的时间:功能测试需要一个懂领域知识的人,测试时间较长。单元测试不需要对被测试系统有太多了解,在极短的时间内完成测试。 - 能更好地应对变化:只要有代码的更改,无论是新功能,还是老功能,跑一遍单元测试就可以做到 - 测试的文档化:单元测试本身说明了给定输入下的输出 - 反向促使代码解耦:耦合性很强的代码很难测试 好单元测试的特点 - 快:毫秒级 - 隔离的:不依赖于任何文件或数据库,可以单独跑通 - 可重复的:如果在测试测时候没有改变条件,测试结果是一样的 - 自检的:在没有人员介入的情况下判定测试成功或失败 - 用时较少的:用时比写代码更少,如果发现写测试的时间较多,说明哪个地方有问题 测试覆盖率`Code coverage` 测试覆盖率不见得越大越好,只是表明有多少代码被测试了,需要在实际项目中把握测试覆盖率的度。 ## 理解`Fake`, `Mock`, `Stub` - `Fake`:不确定是`Mock`还是`Stub`的时候用`Fake` - `Mock`:直接影响到测试结果就用`Mock` - `Stub`: 只是某个依赖的替代,不直接影响到测试结果 不好的 ``` var mockOrder = new MockOrder(); var purchase = new Purchase(mockOrder); purchase.ValidateOrders(); Assert.True(purchase.CanBeShipped);//测试结果与MockOrder没有直接关系 ``` 好的 ``` var stubOrder = new FakeOrder();//使用Fake可以转换成Mock或Stub var purchase = new Purchase(stubOrder); purchase.ValdiateOrders(); Assert.True(purchase.CanBeShipped);//测试结果与stubOrder没有关系 ``` 好的 ``` var mockOrder = new FakeOrder();//使用Fake可以转换成Mock或Stub var purchase = new Purchase(mockOrder); purchase.ValidateOrders(); Assert.True(mockOrder.Valdiated);//测试结果与mockOrder有直接关系 ``` > 命名。好的命名说明了测试意图。通常是:`被测方法名称_场景或条件_期待结果` 不好的 ``` [Fact] public void Test_Single(){} ``` 好的 ``` [Fact] public void Add_SingleNumber_ReturnsSameNumber(){} ``` ## 遵循`AAA`原则 不好的 ``` [Fact] public void Add_EmptyString_ReturnsZero() { //Arrange var stringCalculator = new StringCalculator(); //Assert Assert.Equal(0, stringCalculator.Add("")); } ``` 好的 ``` [Fact] public void Add_EmptyString_ReturnsZero() { //Arrange var stringCalculator = new StringCalculator(); //Act var actual = stringCalculator.Add(""); //Assert Assert.Equal(0, actual); } ``` ## 保持输入简单原则 不好的 ``` [Fact] public void Add_SingleNumber_ReturnsSameNumber() { var stringCalculator = new StringCalculator(); var actual = stringCalculator.Add("42"); Assert.Equal(42, actual); } ``` 好的 ``` [Fact] public void Add_SingleNumber_ReturnsSameNumber() { var stringCalculator = new StringCalculator(); var actual = stringCalculator.Add("0"); Assert.Equal(0, actual); } ``` ## 避免使用系统保留字 不好的 ``` [Fact] public void Add_BigNumber_ThrowsException() { var stringCalculator = new StringCalculator(); Action actual = () => stringCalculator.Add("1001"); Assert.Throws(actual); } ``` 好的 ``` [Fact] void Add_MaximumSumResult_ThrowsOverflowException() { var stringCalculator = new StringCalculator(); const string MAXIMUM_RESULT = "1001";//赋值给常量 Action actual = () => stringCalculator.Add(MAXIMUM_RESULT); Assert.Throws(actual); } ``` ## 避免逻辑 不好的 ``` [Fact] public void Add_MultipleNumbers_ReturnsCorrectResults() { var stringCalculator = new StringCalculator(); var expected = 0; var testCases = new[] { "0,0,0", "0,1,2", "1,2,3" }; foreach (var test in testCases) { Assert.Equal(expected, stringCalculator.Add(test)); expected += 3; } } ``` 好的 ``` [Theory] [InlineData("0,0,0", 0)] [InlineData("0,1,2", 3)] [InlineData("1,2,3", 6)] public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected) { var stringCalculator = new StringCalculator(); var actual = stringCalculator.Add(input); Assert.Equal(expected, actual); } ``` ## 帮助方法替代`setup`和`teadown` 不好的 ``` private readonly StringCalculator stringCalculator; public StringCalculatorTests() { stringCalculator = new StringCalculator(); } ``` ``` [Fact] public void Add_TwoNumbers_ReturnsSumOfNumbers() { var result = stringCalculator.Add("0,1"); Assert.Equal(1, result); } ``` 好的 ``` [Fact] public void Add_TwoNumbers_ReturnsSumOfNumbers() { var stringCalculator = CreateDefaultStringCalculator(); var actual = stringCalculator.Add("0,1"); Assert.Equal(1, actual); } private StringCalculator CreateDefaultStringCalculator() { return new StringCalculator(); } ``` ## 避免一个测试方法中出现多个推断 不好的 ``` [Fact] public void Add_EdgeCases_ThrowsArgumentExceptions() { Assert.Throws(() => stringCalculator.Add(null)); Assert.Throws(() => stringCalculator.Add("a")); } ``` 好的 ``` [Theory] [InlineData(null)] [InlineData("a")] public void Add_InputNullOrAlphabetic_ThrowsArgumentException(string input) { var stringCalculator = new StringCalculator(); Action actual = () => stringCalculator.Add(input); Assert.Throws(actual); } ``` ## 测试公共方法而不是私有方法 ``` public string ParseLogLine(string input) { var sanitizedInput = TrimInput(input); return sanitizedInput; } private string TrimInput(string input) { return input.Trim(); } [Fact] public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult() { var parser = new Parser(); var result = parser.ParseLogLine(" a "); Assert.Equals("a", result); } ``` ## 使用接口获取静态值 有一个和日期相关的方法 ``` public int GetDiscountedPrice(int price) { if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday) { return price / 2; } else { return price; } } ``` 不好的 ``` public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice() { var priceCalculator = new PriceCalculator(); var actual = priceCalculator.GetDiscountedPrice(2); Assert.Equals(2, actual) } public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice() { var priceCalculator = new PriceCalculator(); var actual = priceCalculator.GetDiscountedPrice(2); Assert.Equals(1, actual); } ``` 不管哪一天,总一个通不过。 好的。提炼出一个获取日期的接口。 ``` public interface IDateTimeProvider { DayOfWeek DayOfWeek(); } ``` ``` public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider) { if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday) { return price / 2; } else { return price; } } ``` ``` public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice() { var priceCalculator = new PriceCalculator(); var dateTimeProviderStub = new Mock();//模拟接口 dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);//模拟接口实现 var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub); Assert.Equals(2, actual); } public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice() { var priceCalculator = new PriceCalculator(); var dateTimeProviderStub = new Mock(); dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday); var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub); Assert.Equals(1, actual); } ```