什么是单元测试
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
为什么要写单元测试
以下列举了一些我为什么使用单元测试的好处:
- 允许代码做出改变,因为你了解单元测试会在你的预期之中。
- 单元测试可以有效地降低程序出现BUG的机率;
- 帮助你更深入地理解代码–因为在写单元测试的时候,你需要明确程序所有的执行流程及对应的执行结果等;
- 允许在任何时候代码重构,而不必担心破坏现有的代码。这使得我们编写程序更灵活;确保你的代码的健壮性,因为所有的测试都是通过了的。
- 文档记录。单元测试就是一种无价的文档,它是展示函数或类如何使用的最佳文档,这份文档是可编译、可运行的、并且它保持最新,永远与代码同步。
- 具有回归性。自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地地快速运行测试,而不是将代码部署到设备之后,然后再手动地覆盖各种执行路径,这样的行为效率低下,浪费时间。
什么时候写单元测试
编写单元测试的时机通常有三种情况。首先,在具体实现代码之前进行,这是测试驱动开发(TDD)所倡导的方法。其次,与具体实现代码同步进行,即先编写少量功能代码,然后紧接着编写单元测试(重复这两个过程,直到完成功能代码开发)。实际上,这种方案与第一种非常接近,基本上功能代码开发完成后,单元测试也差不多完成了。最后,第三种情况是在编写完功能代码后再编写单元测试。根据我的经验,事后编写的单元测试的粒度通常较粗。对于同样的功能代码,采用前两种方案的结果可能是用10个较小的单元测试来覆盖,每个单元测试相对简单易懂,可读性和可维护性都较好(重构时单测的改动较小);而采用第三种方案编写的单元测试,往往是用1个较大的单元测试来覆盖,这个单元测试的逻辑就比较复杂,因为它需要测试的东西很多,可读性和可维护性相对较差。
建议:我个人比较推荐将单元测试与具体实现代码同步进行的方案。只有对需求有一定的理解后,才能知道什么是代码的正确性,才能编写出有效的单元测试来验证正确性。能够编写一些功能代码说明对需求已经有一定理解了。
单元测试要写多细
单元测试不是越多越好,而是越有效越好!进一步解读就是哪些代码需要有单元测试覆盖:
- 逻辑复杂的
- 容易出错的
- 不易理解的,即使是自己过段时间也会遗忘的,看不懂自己的代码,单元测试代码有助于理解代码的功能和需求
- 公共代码。比如自定义的所有http请求都会经过的拦截器;工具类等。
- 核心业务代码。一个产品里最核心最有业务价值的代码应该要有较高的单元测试覆盖率。
JUnit4
HelloWorld 例子
我们已一个简单的例子来快速展示 JUnit4 的基本用法.
首先新建一个名为 JUniTest 的 Maven 工程, 然后添加依赖:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
编写测试代码:
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class TestJunit {
@Test
public void testingCrunchifyAddition() {
assertEquals("Here is test for Addition Result: ", 30, addition(27, 3));
}
@Test
public void testingHelloWorld() {
assertEquals("Here is test for Hello World String: ", "Hello + World", helloWorld());
}
public int addition(int x, int y) {
return x + y;
}
public String helloWorld() {
String helloWorld = "Hello +" + " World";
return helloWorld;
}
}
使用测试用例:
public class App {
public static void main(String[] args) {
Result result = JUnitCore.runClasses(TestJunit.class);
for (Failure failure : result.getFailures()) {
System.out.println(failure.toString());
}
if (result.wasSuccessful()) {
System.out.println("Both Tests finished successfully...");
}
}
}
public class TestJunit {
@Test
public void testingCrunchifyAddition() {
assertEquals("Here is test for Addition Result: ", 30, addition(27, 3));
}
...
}
JUnit4 注解
-
@Test (expected = Exception.class) 表示预期会抛出Exception.class 的异常
-
@Ignore 含义是“某些方法尚未完成,暂不参与此次测试”。这样的话测试结果就会提示你有几个测试被忽略,而不是失败。一旦你完成了相应函数,只需要把@Ignore注解删去,就可以进行正常的测试。
-
@Test(timeout=100) 表示预期方法执行不会超过 100 毫秒,控制死循环
-
@Before 表示该方法在每一个测试方法之前运行,可以使用该方法进行初始化之类的操作
-
@After 表示该方法在每一个测试方法之后运行,可以使用该方法进行释放资源,回收内存之类的操
-
@BeforeClass 表示该方法只执行一次,并且在所有方法之前执行。一般可以使用该方法进行数据库连接操作,注意该注解运用在静态方法。
-
@AfterClass 表示该方法只执行一次,并且在所有方法之后执行。一般可以使用该方法进行数据库连接关闭操作,注意该注解运用在静态方法。
JUnit4测试用例的完整的生命周期
要经历如下几个阶段:
- 类级初始化资源处理
- 方法级初始化资源处理
- 执行测试用例中的方法
- 方法级销毁资源处理
- 类级销毁资源处理
其中, 类级初始化和销毁资源处理在每一个测试用例类这仅仅执行一次, 方法级初始化, 销毁资源处理方法在执行测试用例这的每个测试方法中都会被执行一次.