编写基于Property-based的单元测试
目录
单元测试的局限性
Property-based测试
如何实践Property-based测试
编写基于Property-based的单元测试
作为一个开发者,你可能认为你的职责就是编写代码从而完成需求。我不敢苟同,开发者的工作是通过软件来解决现实需求,编写代码只是软件开发的其中一个方面,编写可靠的软件和产出有价值的代码更加重要。而TDD则是前辈通过经验总结出的一套切实可行的软件开发实践,TDD旨在帮助开发者编写高质量的代码。
TDD的过程可以总结为以下几个步骤:
- 先添加一个测试用例
- 执行测试,查看这个测试的失败结果
- 对代码做少量修改
- 再次执行测试,查看测试结果
- 对代码进行重构,执行测试
单元测试的局限性
设想你要编写一个加法
功能,接受两个数字,返回这两个数字的和。让我们来按照TDD的流程走一遍:
1.添加一个测试用例
1 2 3 4 5 6 7 | [Fact] public void Given3And1ShouldReturn4() { var result = Add(3, 1); result.Should().Be(4); } |
2.执行代码,发现测试并不能通过,因为我们还没有实现add方法
3.对代码做少量修改,让测试通过
1 2 3 4 5 6 7 8 9 | public int Add( int a, int b) { if (a==3 && b ==1) { return 4; } return 0; } |
4.继续编写测试
1 2 3 4 5 6 7 | [Fact] public void Given1And2ShouldReturn3() { var result = Add(1, 2); result.Should().Be(3); } |
5.修改代码让测试通过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public int Add( int a, int b) { if (a==3 && b ==1) { return 4; } if (a == 1 && b == 2) { return 3; } return 0; } |
至此为止,你一直在遵守TDD的步骤,测试全部变成了绿色,但是你始终没有得到正确的Add实现。
哪里出了问题?你也许会觉得,咱们实现的Add方法有问题,我们故意犯了一些显而易见的错误从而给TDD挑毛病。但是我任然可以反驳,他之所以看起来是显而易见的错误是因为对两个数字求和这样的需求是每个人都明白的道理,所以你才觉得显而易见,试想这是一个正式的场景,你也许真的就编写了这样的代码从而让两个测试用例都能恰好通过。
如果说我们并不是故意编写了这样的代码,那么单元测试和TDD这种实践本身可能就有一些瑕疵。
换个角度来说,我们之所以没有编写出完整的业务逻辑,是因为单元测试是用例驱动的,而有限的测试用例漏掉了很多可能性。
如果我们对a和b分别取100个随机值,Add方法都能够通过,那么我们几乎很难编写出上面的Add实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 | [Fact] public void WhenAddTwoNumberShouldGetSum() { for ( int i = 0; i < 100; i++) { var a = GetRandomNumber(); var b = GetRandomNumber(); var result = Add(a, b); result.Should().Be(a + b); } } |
要想保证这样的测试通过,你只能编写出正确的Add实现:
1 2 3 4 | public int Add( int a, int b) { return a + b; } |
这个测试看起来不错,通过产生大量随机的输入来驱动代码实现,但是这个代码存在一个致命的问题,测试代码和被测试代码使用了相同的业务逻辑。
1 2 3 4 5 6 7 8 | //我们期望的数字是a + b result.Should().Be(a + b); //而被测对象也是a + b public int Add( int a, int b) { return a + b; } |
如果a + b这个逻辑本身就有问题,但是因为你在测试代码里重复了这一有问题的逻辑,实际上你的测试并没有发现任何问题。
Property-based测试
如果你不在测试代码里重复a + b这个逻辑,你如何通过这100个随机输入来断言测试的准确性?什么样的断言能被用在这100个随机输入的测试用例中?
答案是断言Add这一能力的属性,某种能够适用于所有测试用例的属性。
举个例子:a + b = b + a
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | [Fact] public void A_Add_B_Should_EqualTo_B_Add_A() { for ( int i = 0; i < 100; i++) { var a = GetRandomNumber(); var b = GetRandomNumber(); var result1 = Add(a, b); var result2 = Add(b, a); result1.Should().Be(result2); } } |
这一特性正好是加法交换律,如果只是测试交换律还是不能够保证Add方法的准确性,因为你可以把Add方法实现为a * b。
我们还可以断言起结合律,即a + b + c = a + (b + c)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | [Fact] public void A_Add_B_Add_C_Should_EqualTo_B_Add_C_Add_A() { for ( int i = 0; i < 100; i++) { var a = GetRandomNumber(); var b = GetRandomNumber(); var c = GetRandomNumber(); var result1 = Add(Add(a, b), c); var result2 = Add(a, Add(b, c)); result1.Should().Be(result2); } } |
如何实践Property-based测试
所以什么是Property-based测试?从上面的分析能够看出Property-based测试实际上提出了两个策略来保证测试的有效性:
- 随机产生输入值,保证足够多的测试用例
- 找出并断言功能具有的普遍适应性的属性
在.NET领域,FsCheck用来进行Property-based测试,Property-based是从Haskell移植过来的,几乎所有的主流语言都有其移植版本。
下篇我们将介绍如何通过FsCheck来做Property-based测试。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述