建议155:随生产代码一起提交单元测试代码
首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的版本稳定,性能也更差了,Bug似乎也变多了。也就是说,重构的代码看上去质量更高了,可实际测试结果却不如人意。
几乎每个程序员都因为此类问题纠结过。我们要修改的代码也许来自某些不负责任或经验欠佳的程序员,也许这些代码是自己一年前写的,但是看上去已经惨不忍睹。我们想要修改这些代码,却担心重构出别的问题。即便是一个开发周期中的产品,也会有这样的选择出现。某个模块可能已经提交测试并确认通过,不过现在发现有更优的算法和逻辑,改还是不改,成了一个问题。
“单元测试”减轻甚至消除了开发者这种恐惧。如果项目没有测试代码,说明我们只是生产“定时炸弹”。很多人将生产代码和测试代码分别对待,这是一种过时的做法。程序员在提交自己的生产代码时,必须同时提交自己的单元测试代码。很多现代化的版本管理工具可以在后台订制项目构建计划,自动运行测试项目,统计代码覆盖率,并生成相应报告。我们应该在早上一边喝咖啡,一边读取这样的报告。
有了测试代码做保证,在很大程度上我们可以放心去重构了。如果某个功能偏离了既有成果,就会有醒目的提醒。
将单元测试放在首要地位的一种开发模式是TDD模式。TDD(Test Driven Development测试驱动开发)有三条严格的定律:
- 在编写不能通过测试的单元测试前,不要编写任何生产代码。
- 只编写恰好无法通过的单元测试,不能编译也算不通过。
- 只编写刚好足以通过当前失败测试的生产代码。
即使我们的团队没有完全采用TDD的开发模式,也可以借鉴这些定律来编写我们自己的测试代码。我们无需一次性编写完全部的测试代码,那没有必要,这跟过度设计一样,也不可能实现。事实上,我们应该逐步地编写测试代码,而且按如下步骤来编写:
测试代码->生产代码->测试代码
下面编写一个单元测试的例子:
先编写一个Add方法:
public class SampleClass { public int Add(int a, int b) { return a + b; } }
右键创建单元测试:
VS会自动为我们生成一个测试方法:
/// <summary> ///Add 的测试 ///</summary> [TestMethod()] public void AddTest() { SampleClass target = new SampleClass(); // TODO: 初始化为适当的值 int a = 0; // TODO: 初始化为适当的值 int b = 0; // TODO: 初始化为适当的值 int expected = 0; // TODO: 初始化为适当的值 int actual; actual = target.Add(a, b); Assert.AreEqual(expected, actual); Assert.Inconclusive("验证此测试方法的正确性。"); }
将方法修改成我们需要的方法就可以了:
/// <summary>
///Add 的测试
///</summary> [TestMethod()] public void AddTest() { SampleClass target = new SampleClass(); // TODO: 初始化为适当的值 int a = 1; // TODO: 初始化为适当的值 int b = 2; // TODO: 初始化为适当的值 int expected = 3; // TODO: 初始化为适当的值 int actual; actual = target.Add(a, b); Assert.AreEqual(expected, actual); Assert.Inconclusive("验证此测试方法的正确性。"); }
VS这个可视化测试工具太重量级了,导致开发的过程中运行测试代码太繁琐也太耗时。可以考虑用测试工具TestDriven.NET,这里不再介绍。
单元测试要注意一下几点:
首先,单元测试不应引入任何人机交互的内容。如,测试过程中不应该弹出对话框,等待用户输入或确认。单元测试不应该是被阻滞的。
其次,多线程也不属于单元测试范畴,单元测试应该是快速被执行的,而不是需要等待的。
最后,单元测试不应该跨应用程序域,例如,数据访问或者远程通信属于集成测试范畴,而不是单元测试。
转自:《编写高质量代码改善C#程序的157个建议》陆敏技