六、测试驱动开发 TDD
在编写业务代码前,先考虑如何编写测试,再编写业务代码,这种开发方式称作:TDD test-driven development。
使用TDD的主要优点
就通常的单元测试而言,最为明显优点就增强了我们对代码按照设计运行的信心。
而TDD,由于是在编写业务代码提前设计,可以说,这些单元测试就反映了业务需求(当然依赖单元测试的质量),为重构提供了保障。
简单的开始
下面的例子是一个完整的TDD的流程,目的是实现Profile类。一遍遵循如下的流程:
- 编写一个会失败的测试
- 通过编写新的代码使得测试通过
- 清理掉无效代码并重复上面的步骤
首先编写最简单测试用例与业务代码
1 2 3 4 5 6 7 8 9 10 11 | package iloveyouboss; import static org.junit.Assert.assertFalse; import org.junit.Test; public class ProfileTest { @Test public void matchesNothingWhenProfileEmpty() { Profile profile = new Profile(); } } |
这是肯定会出现编译错误,原因是没有Profile类,此时我们建立一个最简单的Profile,仅满足测试用例不报错。Profile类如下:
1 2 3 4 5 | package iloveyouboss; public class Profile { } |
现在改进一下测试用例
补充测试用例的内容,让用例先失败,再成功。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | package iloveyouboss; import static org.junit.Assert.assertFalse; import org.junit.Test; public class ProfileTest { @Test public void matchesNothingWhenProfileEmpty() { Profile profile = new Profile(); Question question = new BooleanQuestion( 1 , "Relocation package?" ); Criterion criterion = new Criterion( new Answer(question, Bool.TRUE), Weight.DontCare); boolean result = profile.matches(criterion); assertFalse(result); } } |
假定其他依赖的类均存在,此时profile.matches()方法会报错,原因是没有matches方法。这时修改Profile类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package iloveyouboss; public class Profile { private Answer answer; public boolean matches(Criterion criterion) { return answer != null ; } public void add(Answer answer) { this .answer = answer; } } |
清理测试
在经过上面的循环之后,我们发下这两个测试用例均初始化了Profile,因此可以将其提权到@Before函数中。
1 2 3 4 5 6 7 8 9 10 11 | public class ProfileTest { private Profile profile; @Before public void createProfile() { profile = new Profile(); } .... } |
TDD美好的地方在于,在特性之前编写测试,也就意味着可以始终对重构和清理代码充满信心。
我们还可以不断的重构测试,如提取公共的变量到@Before中,对变量进行重命名以提高可读性,使得单元测试能够明确的表述功能,并自然的成为文档的一部分。下面是经过重构后的测试代码,可以看到profile, questionIsThrereRelocation, answerThereIsRelocation被提取和改名了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | package iloveyouboss; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.junit.Before; import org.junit.Test; public class ProfileTest { private Profile profile; private BooleanQuestion questionIsThereRelocation; private Answer answerThereIsRelocation; @Before public void createProfile() { profile = new Profile(); } @Before public void createQuestion() { questionIsThereRelocation = new BooleanQuestion( 1 , "Relocation package?" ); answerThereIsRelocation = new Answer(questionIsThereRelocation, Bool.TRUE); } @Test public void matchesNothingWhenProfileEmpty() { Criterion criterion = new Criterion(answerThereIsRelocation, Weight.DontCare); boolean result = profile.matches(criterion); assertFalse(result); } @Test public void matchesWhenProfileContainsMatchingAnswer() { profile.add(answerThereIsRelocation); Criterion criterion = new Criterion(answerThereIsRelocation, Weight.Important); boolean result = profile.matches(criterion); assertTrue(result); } } |
再一次改进
这次增加一个测试用例,用来覆盖答案不能覆盖问题的场景,首先增加一个用例:
1 2 3 4 5 6 7 8 9 | @Test public void doesNotMatchWhenNoMatchingAnswer() { profile.add(answerThereIsNotRelocation); Criterion criterion = new Criterion(answerThereIsRelocation, Weight.Important); boolean result = profile.matches(criterion); assertFalse(result); } |
这里用例是执行不通过的,然后通过修改profile类使得该测试通过:
1 2 3 4 5 6 7 8 9 10 11 12 13 | package iloveyouboss; public class Profile { private Answer answer; public boolean matches(Criterion criterion) { return answer != null && answer.match(criterion.getAnswer()); } public void add(Answer answer) { this .answer = answer; } } |
作为开发者,工作就是考虑代码中所有的可能性和场景。如果要取得TDD的成功,需要将这些场景拆分为测试,并将他们按照一定的顺序排列,以减少使得每个测试通过的代码增量。
将测试作为文档一部分
对于TDD,测试用例应该作为文档的一部分,其他开发者可以通过阅读测试用例来了解内部逻辑,这要求测试用例的名称足够清晰。如:
1 2 3 4 5 6 7 8 9 | matchesWhenProfileContainsMatchingAnswer doesNotMatchWhenNoMatchingAnswer matchesWhenContainsMultipleAnswers doesNotMatchWhenNoneOfMultipleCriteriaMatch matchesWhenAnyOfMultipleCriteriaMatch doesNotMatchWhenAnyMustMeetCriteriaNotMet matchesWhenCriterionIsDontCare scoreIsZeroWhenThereAreNoMatches |
另外,为了清晰,也可以将不同关注点的测试用例放到不同的测试类中。
TDD的节奏
TDD遵循一个普遍的循环:
- 编写测试
- 编写逻辑使得测试通过
- 重构代码保持清晰
- 重复上述过程
最好将每次循环控制得足够小,例如10分钟以内,如果10分钟不能完成则说明测试的粒度有些大,需要重新写测试,这样不断循环,逐渐逼近和完善功能。
最终交付的,将不只是代码,同时提供了作为文档使用的测试用例以及重构的保证。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探