作者
Richard Paul
Kiwiplan NZ Ltd
27 Feb 2009
单元测试是什么?
在计算机编程里,单元测试是一种用来验证单独的代码是否正确的方法
http://en.wikipedia.org/wiki/Unit_testing
单元测试带来的好处
- 自动化的回归测试,保证代码持续地正常
- 作为代码的一种可运行的,一直保持更新的文档
- 让代码更加稳定,使人能放心重构
- 能与系统的其它部分分开进行测试,并且其它系统部分可能还不存在。
- 更少的BUG!
单元测试与功能测试
单元测试是从程序员的观点来写的,他们保证特定的类或者方法正常地执行一系列指定的任务。
功能测试是由用户的观点而来的, 这些测试确保系统是按用户的需求来运行的。
http://www.ibm.com/developerworks/library/j-test.html
单元测试的结构
- 一个用例一个测试方法
- 测试方法互相独立地运行
public class ItemControllerTest {
private ItemController itemController;
@Mock
private ItemService itemService;
private Map<String, Object> modelMap;
@Before
public void setup(){
itemController = new ItemController(itemService);
modelMap = new HashMap<String, Object> ();
}
@Test
public void testViewItem() throws Exception {
Item item = new Item(1, "Item 1");
when(ItemService.getItem(item.getId())).thenReturn(item);
String view = itemController.viewItem(item.getId(), modelMap);
assertEquals(item, modelMap.get("item"));
assertEquals("viewItem", view);
}
单元测试最佳实践
单元测试应该运行得快
- 允许程序员快速地编程
- 除非真正需要,不要碰到数据库
测试应该只断言其需要的场景
- 包含太多不必要的测试会带来过高的维护
单元测试应与代码同时编写
单元测试应该在代码提交时自动运行
- 使用持续集成的服务器,比如Hudson, Jenkins.
- 快速反馈集成时出现的问题
测试驱动开发
- 添加一个基础测试
- 编写刚好足够通过测试的代码
- 添加另一个测试,重复第二步
测试驱动的优势
- 鼓励你考虑如何使用你的对象
- 开发更快,不需要编译和部署项目来确定一段代码可以运行
- 调试一段单元测试要比调试一个部署好的应用程序要容易得多
- 一系列的回归测试让你重构代码的时候不用担心破坏已有的功能
- 单元测试可以覆盖更多的边缘测试情景,因为作为程度员你更熟悉对象的模型
显示接缝(Seam)
- 接缝是指被测试的代码与其它组件隔离开的分割线
- 与接缝另一面组件的合作应该是可替换的,以确保我们可以隔离想要测试的代码。
- 能表现出接缝的代码可以认为是低耦合的
应用接缝
- 在实际运行中,使用的都是实际的对象。
- 代码有接缝:可以使用模拟的对象(Mock)来替换实际的其它组件进行测试。
- 反模式-静态代码:无接缝,无法替换代码中与其它组件的合作部分,导致复杂冗长的测试代码。
- 本地创建合作对象:无接缝,被测试的类自己创建合作的其它类的对象,我们无法替换这些对象。
反模式(Anti Panttern)更多资料
Static Methods are Death to Testability
http://misko.hevery.com/2008/12/15/static-methods-are-death-to-testability/
To "new" or not to "new"…
http://misko.hevery.com/2008/09/30/to-new-or-not-to-new/
Mocking框架
Mocking框架提供了便利的方法来来”仿造”其它组件的对象,使得我们能专心测试想要的代码。
这些框架包括
Java
- Mockito
- EasyMock
- JMock
- .Net
.Net
- Moq (需要.Net 3.5)
- Rhino
- TypeMock
实例 - 访问一个合作对象
为了给使用其它服务类的代码提供一个接缝,需要把这些服务类替换出去。
依赖注入
Java
- Spring框架
- Guice
.Net
- SpringFramework.Net
- Castle MicroKernel/Windsor
服务访问
Poor mans dependency injection
实例 - 依赖注入
资源被注入进需要他们的类中
构造器注入
public class ItemController {
private ItemService itemService;
public ItemController(ItemService itemService) {
this.itemService = itemservice;
}
...
Setter注入
public class ItemController {
private ItemService itemService;
public ItemController () {}
public void setItemService(ItemService itemService) {
this.itemService = itemService;
}
...
实例 - 服务访问
如果你没有依赖注入容器,使用单例模式也对测试有好处
public class ServiceAccess {
private ItemService itemService;
public static ItemService getItemService() {
return itemService;
}
public void setItemService(ItemService itemService) {
ServiceAccess.itemService = itemService;
}
}
在你的测试里,可以把itemService变成Mock版本,这样就能有接缝了
实例 - 环境
Java : Eclipse, JUnit 4.5, Mockito 2.7
VB.net: Visual Studio 2005, NUnit 2.4, Rhino Mocks 3.5
更多Java相关的例子(Mockito)请参考
http://www.rapaul.com/2008/11/19/mocking-in-java-withmockito/
测试覆盖率
让测试覆盖率亮绿灯
Java - EMMA, 有Eclipse 插件EclEmma
.Net - NCover, 有Visual Studio支持
总结
- 每个测试用例应该明确地定义在单独的测试方法里
- 单元测试提供阐述代码如何工作的能运行的文档
- 测试的可维护很重要,因为测试代码占代码的比重很大
- 记录下单元测试覆盖的用例
- 过度地定义单元测试会导致更多的维护量
- 制造接缝使得代码之前耦合低,并且通过Mock对象能让单元测试变得简单
测试驱动开发注重代码是如何使用的,而且降低了编译和部署的周期 = 更快