原文连接: blog.yoryor.top/2017-11-02/…html
- 主要内容: 本文从 What, Why, When, How, Deep 几个方面来介绍单元测试相关的基础知识。
- 适合阅读: 对单元测试不了解或者只知其一;不知其二的程序员
- 须要技能: 了解 Java 编程语言,可以使用 IDEA 等。
单元测试(unit test)并不一样于普通的端到端测试,这是一项须要程序员经过实际编码来完成,而且与程序的设计和调试紧密相关的任务。单元的大小,并无严格规定,可是遵守着软件设计中”SRP”(Single Responsibility Principle)原则,在 Java 中一般以类做为一个基本的测试单元,以此编写单元测试来对功能或者说行为进行监视和检测。
JUnit 是 Java 编程语言中比较流行的一款单元测试框架,可以知足平时开发中大部分的需求。java
编写单元测试的主要目的是为了检视代码的行为是否符合预期,而且这种检查一般是成本很低的,开发人员能够方便的在 IDE 或本地开发环境中及时的发现问题,从而提升开发效率。git
编写合理的单元测试能够轻松应付如下的几个场景程序员
在 IDEA 中使用单元测试是很方便的,能够参考官方的教程建立单元测试
接下来使用的示例代码模拟实现了一个功能,根据一个程序员的技能和等级,为一个程序员打分。详细的代码能够到 github 上查看 code-example, 推荐根据提交历史来进行查看。具体的环境以下:github
Junit4 开始使用的注释提升了单元测试的编写效率,在引入 Junit4依赖后,在须要进行测试的方法上添加一个@Test 注释便可。其余经常使用的注释还有@Before、@After、@Rule 等。在后面遇到的时候在详述编程
良好的测试方法命名能起到见名知义的效果。理想状况下,命名中须要包含测试的方法,条件以及指望的返回结果。能够提炼成如下的格式
·callSomeMethodReturnSomeResultWhenSomeConditions
或者其余的相似的变体。好比given-then-when
等等框架
前面的命名中提到的三个信息也正是组织单元测试的基本依据。即编程语言
检验结果 (assert)
Assert 这个单词的直译是断言,意思是判断某项条件是否为真。在单元测试中咱们经过断言来实现结果检验的语义, 以此来监视代码的行为。
JUnit 中可使用两种不一样的断言风格— classic 和 matcher。经过代码能够直观的看出两者之间的差异。ide
public class SkillGraphTest {
@Test
public void getResultReturnZeroWhenSkillGraphEmpty() throws Exception {
// arrange
SkillGraph skills = new SkillGraph();
// act and assert -- classic style
assertTrue(skills.getResult() == 0);
// act and assert -- matchers style
assertThat(skills.getResult(), equalTo(0d));
}
}复制代码
上面为 SkillGraph 类编写的单元测试示例中,咱们验证的行为是当程序员的技能图中没有添加任何技能时调用 getResult
方法须要返回0(double 类型)
因为代码很简单,因此将执行和验证的两个阶段合并到一块儿了。单元测试
咱们主要看一下不一样风格的断言:
skills.getResult
与咱们指望的结果是否相等。也就是skills.getResult() == 0
skills.getResult() is equal to 0d
assertThat(actualResult, matchers)
方法能够利用 Hamcrest 开发的大量 Matcher 为咱们的单元测试提供更好的语义化支持。在编写单元测试时,若是测试结果不符合预期,JUnit 会报告一次failure
;若是单元测试程序运行过程当中若是出现了异常,则会报告一次error
。因为单元测试的隔离性,咱们一般将关注点集中在须要测试的功能上,而不须要浪费时间在异常处理上,所以一些受检查异常直接在方法签名上抛出便可。
有些时候咱们可能会须要验证异常抛出的正确性,以此来保证客户端使用的正确性和可靠性,有三种形式能够来验证异常行为是否合理。
@Test(expected = IllegalArgumentException.class)
public void getResultThrowIllegalArgumentExceptionWhenAddNullValue() throws Exception {
// arrange
SkillGraph skills = new SkillGraph();
// act
skills.add(null);
// assert
}复制代码
@Test
public void getResultThrowIllegalArgumentExceptionWhenAddNullValue() throws Exception {
// arrange
SkillGraph skills = new SkillGraph();
// act
try {
skills.add(null);
} catch (Exception e) {
// assert
assertThat(e, instanceOf(IllegalArgumentException.class));
assertThat(e.getMessage(), equalTo("Skill can not be null."));
}
}复制代码
使用 @Rule 注解,利用内置的 ExpectedException 来实现。JUnit 中的 rule 规则机制实际上就是相似一种 AOP 的实现,为单元测试提供面向切面编程的能力,详细的内容再也不此展开了。异常的断言要放在前面,不然代码就没法执行到
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void getResultThrowIllegalArgumentExceptionWhenAddNullValue() throws Exception {
expectedException.expectMessage("Skill can not be null.");
expectedException.expect(IllegalArgumentException.class);
// arrange
SkillGraph skills = new SkillGraph();
// act
skills.add(null);
// assert
}复制代码
在编写单元测试时,分支条件和边界条件是须要重点关注的。这也会致使一个类的单元测试每每须要编写不少单元测试。所以有必要经过优化重构来保持代码的整洁和良好的可维护性和扩展性。
在单元测试中,咱们在 arrange 阶段每每会执行一些对象初始化等一些初始化操做,这个过程一般是重复的。能够经过@Before
注解一个初始化方法,这样在每一个单元测试以前都会执行这段代码了。相似的还有@After
注解,只不过不太经常使用。
其实以前提到的@Rule
注解实现的就是相似@Before
和 @After
的功能,在执行一个单元测试方法的先后执行一些方法,只不过这些方法都是有具体的目标,经过规则这个语义实现。
注: JUnit 中每一个单元测试都拥有独立的上下文环境,执行每一个测试方法时都会单独生成一个新的实例,所以没法保证单元测试用例的执行顺序,致使咱们在单元测试中不能依赖其余的测试结果,固然这种需求自己就是“anti pattern”的。
一个典型的单元测试用例的执行以下
@Test
注释的方法@Test
注释的方法编写单元测试最主要、最直接的关注点就是方法执行的正确性。其次须要关注数据的边界条件,即在某些极端的或者不正确的条件下,程序可否正常的运行或者合理的运行,以此来保证系统的健壮性。常见的关注点有如下几个
编写单元测试算是一项内功修炼,也是一项烦琐的工做。曾国藩曾说过,“成大事者,必能耐烦”。若是单纯为了提升代码覆盖率,确实很烦;若是为了改进代码结构防患 Bug 于未然,就会发现单元测试是如此有用。