|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- # .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<OverflowException>(actual);
- }
- ```
-
- 好的
-
- ```
- [Fact]
- void Add_MaximumSumResult_ThrowsOverflowException()
- {
- var stringCalculator = new StringCalculator();
- const string MAXIMUM_RESULT = "1001";//赋值给常量
-
- Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);
-
- Assert.Throws<OverflowException>(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<ArgumentException>(() => stringCalculator.Add(null));
- Assert.Throws<ArgumentException>(() => 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<ArgumentException>(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<IDateTimeProvider>();//模拟接口
- 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<IDateTimeProvider>();
- dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);
-
- var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);
-
- Assert.Equals(1, actual);
- }
- ```
-
-
-
|