单元测试
序言
单元测试在质量上可以帮助我们杜绝低级bug的出现,减少逻辑型错误的发生,提高产品质量;在开发过程中还可以帮助大家提高工作效率。也许哪天等你遇到一个应用,打包启动时间花掉1+分钟以后,当你写sql经常免不了一些小问题的时候,当你不得不启动服务才能检查api返回json数据是否正常的时候,当你对某个方法甚至是整个系统进行重构的时候,当有新人或其他组员加入修改了你的代码的时候,你会意识到它真的可以帮助我们很多很多。
单元测试的概念
关于“单元”的概念:
- 指的是具体某个函数,方法 —原子性
- 测试的时候需要将其他的依赖“解耦” —隔离性
单元测试覆盖范围:
- 原则上所有自己写的的java方法/单元都要写
- resource/servlet/filter/controller/action逻辑简单的可以不写
- service/manager/dao必须写
- 覆盖范围应达到70%以上
单元测试工具之Junit
Junit大家应该都很熟悉,在新建完maven项目时很多情况下都会自动给你倒入一个初始的依赖,就是Junit。
单元测试框架(亦可称回归测试框架)。Junit测试属于程序员测试,也就是白盒测试。Junit4充分利用了java5的Annotation特性,以便更加方便的写好单元测试。
- junit主要提供以下几个功能:
- 用于测试期望结果的断言(Assertion)
- 用于共享共同测试数据的测试工具
- 用于方便的组织和运行测试的测试套件
- 图形和文本的测试运行器
junit使用
使用idea IDE 进行单元测试,首先需要安装JUnit 插件。
- 安装JUnit插件步骤
File–>settings–>Plguins–>Browse repositories–>输入JUnit–>选择JUnit Generator V2.0安装。 - 重启Idea
- 在你要创建测试用例的类中按 Command + Shift + T (右键goto–> test 亦可)
@Test
在 JUnit4 中,测试是由 @Test 注释来识别的,测试类使用Test做为类名的后缀(非必要),测试方法使用test作为方法名的前缀(非必要)。测试方法必须使用public void 进行修饰,不能带有任何参数。测试类的包应该与被测试类的包保持一致。
JUnit生命周期
@Before,@BeforeClass,@After,@AfterClass
@BeforeClass:修饰static的方法,在整个类执行之前执行该方法一次。比如你的测试用例执行前需要一些高开销的资源(连接数据库)可以用@BeforeClass搞定。值得注意的是如果测试用例类的父类中也存在@BeforeClass修饰的方法,它将在子类的@BeforeClass之前执行。
@AfterClass:同样修饰static的方法,在整个类执行结束前执行一次。如果你用@BeforeClass创建了一些资源现在是时候释放它们了。
@Before:修饰public void的方法,在每个测试用例(方法)执行时都会执行。
@After:修饰public void的方法,在每个测试用例执行结束后执行。
@Ignore
忽略的测试方法,标注的含义就是“某些方法尚未完成,暂不参与此次测试”;这样的话测试结果就会提示你有几个测试被忽略,而不是失败。一旦你完成了相应函数,只需要把@Ignore标注删去,就可以进行正常的测试。
@RunWith
Runner:Runner是一个抽象类,是JUnit的核心组成部分。用于运行测试和通知Notifier运行的结果。JUnit使用@RunWith注解标注选用的Runner,由此实现不同测试行为。
测试在Spring容器环境下执行, 则对测试类使用: @RunWith(SpringJUnit4ClassRunner.class)
启动方式
如果是以.xml文件启动的方式,需要使用@ContextConfiguration注解,用于指定配置文件所在的位置 。
@ContextConfiguration(locations = “classpath:applicationContext.xml”)
如果是spring boot框架,通过Application入口类启动,需要使用@SpringApplicationConfiguration注解
@SpringApplicationConfiguration(classes = Application.class)
Assert
对于验证结果,则一般是通过一些assert方法来完成的。JUnit为我们提供的assert方法,多数都在 Assert 这个类里面。
- 最常用的那些如下:
- assertTrue/False([String message,]boolean condition);
用来查看变量是是否为false或true,如果assertFalse()查看的变量的值是false则测试成功,如果是true则失败,assertTrue()与之相反;
fail([String message,]);
直接用来抛出错误。 - assertEquals([String message,]Object expected,Object actual);
判断是否相等,可以指定输出错误信息。
第一个参数是期望值,第二个参数是实际的值。
这个方法对各个变量有多种实现 - assertEquals(expected, actual, tolerance)
这里传入的expected和actual是float或double类型的,大家知道计算机表示浮点型数据都有一定的偏差,所以哪怕理论上他们是相等的,但是用计算机表示出来则可能不是,所以这里运行传入一个偏差值。如果两个数的差异在这个偏差值之内,则测试通过,否者测试失败。 - assertNotNull/Null([String message,]Object obj);
判读一个对象是否非空(非空)。 - assertSame/NotSame([String message,]Object expected,Object actual);
判断两个对象是否指向同一个对象。看内存地址。 - failNotSame/failNotEquals(String message, Object expected, Object actual)
当不指向同一个内存地址或者不相等的时候,输出错误信息。
注意信息是必须的,而且这个输出是格式化过的。
- assertTrue/False([String message,]boolean condition);
单元测试工具之仿冒对象(Mock)
构造伪对象
Q:我们现在要写的是单元测试,既然是单元测试,那么就要做到只是测试一个类或者一个方法,但是实际情况是,web层会调用service层,service层继续调用直到数据库,这样如果不做隔离处理的话,似乎单元测试就不再“单元”化了。
S:用伪对象来进行单元测试:它的主要作用是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开。什么叫测试边界以外的对象?一个方法中,你使用到的接口就是测试边界外的对象。
Q:隔离能带给我们什么样的好处呢?
S:单元测试变得不再难写了:我们在写单元测试的时候,可能会有很多问题困扰着我们,比如协作开发的时候,其他团队的模块接口没有开发完成,比如要和一些非常复杂不容易构造的对象进行交互(like HttpSession),隔离就是最好的方法。这时候我们谈及的mock技术就派上用场了。听着很玄乎,其实很简单。熟悉spring的人都应该知道spring里有个单元测试类MockHttpServletRequest,它就是一个伪对象,当我们的测试代码中需要用及HttpServletRequest的时候,MockHttpServletRequest就可以代替HttpServletRequest的所有操作。
快速精确定位问题所在:当我们把其他接口隔离开的时候,如果发现了bug,我们能够及时定位到是哪里出错了。因为测试失败的点,就是你bug所在。
一些仿冒对象的框架
- EasyMock:早期比较流行的MocK测试框架。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常
- PowerMock:这个工具是在EasyMock和Mockito上扩展出来的,目的是为了解决EasyMock和Mockito不能解决的问题,比如对static, final, private方法均不能mock。其实测试架构设计良好的代码,一般并不需要这些功能,但如果是在已有项目上增加单元测试,老代码有问题且不能改时,就不得不使用这些功能了
- JMockit:JMockit 是一个轻量级的mock框架是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于 Java 5 SE 的 java.lang.instrument 包开发,内部使用 ASM 库来修改Java的Bytecode
- mockito:本次使用的框架,已经被广泛的使用,其他mock框架很多都是基于mockito开发而来。参考Mockito使用指南
单元测试工具之DbUnit
DbUnit是一个基于junit扩展的数据库测试框架。它提供了大量的类对与数据库相关的操作进行了抽象和封装,主要提供以下功能:
1.可以控制测试数据库的状态。进行一个DAO单元测试之前,DbUnit为数据库准备好初始化数据;而在测试结束时,DbUnit会把数据库状态恢复到测试前的状态。
2.隔离:将测试数据隔离到新的db库中
- 根据业务,做好测试用的准备数据和预想结果数据,通常准备成xml格式文件。
- 在setUp()方法里边读入准备数据。
- 对测试类的对应测试方法进行实装:执行对象方法,把数据库的实际执行结果和预想结果进行比较。
- 在tearDown()方法里边,把数据库还原到测试前状态。
DAO层单元测试
一般来说,dao层测试由于需要导入springcontent容器来进行测试,因为许多orm框架都已经和spring整合。
在测试类前加上注解 导入spring配置文件,若不知道导入哪些spring配置文件则去web.xml中contextConfigLocation下拷贝一份出来
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:applicationContext.xml",
"classpath*:config/spring/common/appcontext-*.xml"})
@TransactionConfiguration(defaultRollback = true)
public class DAOTest {
@Autowired
CityDAO cityDAO;
@Test
public void testInsert(){
int result=cityDAO.insert(new CityEntity(1,"北京"));
assertEquals(1,result);
}
}
问题:没有实际检测数据库状态
可以看到我们只是想当然的认为返回1就是insert成功了,但是这条数据有没有真正的被插进去,我们尚不可知。
解决方案:
对于初始数据的加载,手动输入很麻烦,一个解决方案就是使用Dbunit,从Xml文件甚至Excel中加载初始数据到数据库,是数据库的值达到一个已知状态。同时还可以使用Dbunit,对数据库的结果状态进行判断,保证和期望的一致。数据修改的还原,可以依赖Spring TransactionalTests,在测试完成后回滚数据库。
//加载spring的配置(提取出dao和datasource的)设定自动回滚
@ContextConfiguration(locations = { "classpath:testApplicationContext.xml" })
@TransactionConfiguration(defaultRollback = true)
public class UserDaoTest extends AbstractTransactionalJUnit4SpringContextTests {
@Autowired
private UserDao userDao;
@Autowired
private DataSource dataSource;
private IDatabaseConnection conn;
@Before
public void initDbunit() throws Exception {
conn = new DatabaseConnection(DataSourceUtils.getConnection(dataSource));
}
@Test
public void saveUser() throws Exception {
User user = new User();
user.setNick("user001");
user.setPassword("password001");
userDao.save(user);
QueryDataSet actual = new QueryDataSet(conn);
actual.addTable("user",
"select * from user where user.nick = 'user001'");
IDataSet expected = new FlatXmlDataSet(new ClassPathResource(
"user001.xml").getFile());
//对比期望结果与实际结果
Assertion.assertEquals(expected, actual);
}
}
user001.xml如下
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<user nick="user001" password="password001" />
</dataset>
使用以上方法做测试有以下几点便利:
- 可以做断言判断实际上数据库中的内容有没有变更
- 断言后自动回滚数据 不影响下一次测试
注意在spring4.2后TransactionConfiguration过时 请使用@Transactional与@Rollback
Manager&&Service其他中间层测试
这一类测试归于一类,是因为他们都需要使用的技术是仿冒对象(mock),在这些层主要是为了实现各种逻辑单元的验证,但是逻辑单元很大程度上会依赖dao层或者是远程调用其他服务,为了使单元测试不依赖于其他服务这里使用mockito作为工具来进行对象仿冒,详细的使用请参照上面的mockito官网及使用帮助博客链接,这里只举例说明。
//假设现在需求时测试CityService下的一个saveCity(CityEntity)方法是否正常,而这个CityService依赖于CityDAO
@Mock
CityDAO cityDAO;
@InjectMocks
CityService cityService;//使用InjectMocks标签后init时会自动注入对应的mock对象(cityDAO)
@Before
public void setUp() throws Exception {
//初始化注入(当使用mock注解时)
MockitoAnnotations.initMocks(this);
}
@Test
public void testSave(){
CityEntity cityEntity=new CityEntity(1,"北京");
//设定一些仿冒的行为
when(cityDAO.get(1)).thenReturn(null);
when(cityDAO.save(cityEntity)).thenReturn(true);
//执行方法
boolean result=cityService.saveCity(cityEntity);
//断言结果
assertTrue(result);
//断言判断dao是否执行过一次save一个CityEntity对象的操作
verify(cityDAO,times(1)).save(any(CityEntity.class));
}
总结
单元测试是开发人员的基本功,它可以检查自己逻辑代码的漏洞,确保自己逻辑单元在测试的情景下的正确性,如果不能很好的编写单元测试代码,那么写出来的程序很可能千疮百孔,对测试人员的的压力陡增。而且项目不仅是一个人在开发,在之后或者其他的开发人员进行开发时如果不小心影响到了你的逻辑单元,在进行测试时就会将对应的错误直接抛出警告,也就不会出现到上线之后再后知后觉,给公司带来无谓的损失。