欢迎大家来到IT世界,在知识的湖畔探索吧!
主要内容: 从 What, Why, When, How, Deep 几个方面来介绍单元测试相关的基础知识。
需要技能: 了解 Java 编程语言,能够使用 IDEA 等。
What
单元测试(unit test)并不同于普通的端到端测试,这是一项需要程序员通过实际编码来完成,并且与程序的设计和调试紧密相关的任务。单元的大小,并没有严格规定,但是遵照着软件设计中”SRP”(Single Responsibility Principle)原则,在 Java 中通常以类作为一个基本的测试单元,以此编写单元测试来对功能或者说行为进行监视和检测。
JUnit 是 Java 编程语言中比较流行的一款单元测试框架,能够满足平时开发中大部分的需求。
Why
编写单元测试的主要目的是为了检视代码的行为是否符合预期,并且这种检查通常是成本很低的,开发人员可以方便的在 IDE 或本地开发环境中及时的发现问题,从而提高开发效率。
When
编写合理的单元测试可以轻松应付以下的几个场景
●检查新添加的代码功能是否符合预期
●检查新添加的代码是否兼容旧版本
●第三方库代码的运行是否符合预期
●了解当前代码的行为
How
在 IDEA 中使用单元测试是很方便的,可以参考官方的教程创建单元测试(https://www.jetbrains.com/help/idea/creating-tests.html)
接下来使用的示例代码模拟实现了一个功能,根据一个程序员的技能和等级,为一个程序员打分。详细的代码可以到 github 上查看 code-example(https://github.com/michalyao/junit4-code-example.git), 推荐根据提交历史来进行查看。具体的环境如下:
●JDK:1.8+
●IDEA:2017.2
●JUnit: 4.12
注解
Junit4 开始使用的注释提高了单元测试的编写效率,在引入 Junit4依赖后,在需要进行测试的方法上添加一个@Test 注释即可。其他常用的注释还有@Before、@After、@Rule 等。在后面遇到的时候在详述
命名
良好的测试方法命名能起到见名知义的效果。理想情况下,命名中需要包含测试的方法,条件以及期望的返回结果。可以提炼成以下的格式
·callSomeMethodReturnSomeResultWhenSomeConditions
或者其他的类似的变体。比如given-then-when等等
方法体
前面的命名中提到的三个信息也正是组织单元测试的基本依据。即
1.准备条件 (arrange)
2.执行目标方法 (act)
3.检验结果 (assert)
Assert 这个单词的直译是断言,意思是判断某项条件是否为真。在单元测试中我们通过断言来实现结果检验的语义, 以此来监视代码的行为。
JUnit 中可以使用两种不同的断言风格— classic 和 matcher。通过代码可以直观的看出二者之间的差别。
上面为SkillGraph类编写的单元测试示例中,我们验证的行为是当程序员的技能图中没有添加任何技能时调用getResult方法需要返回0(double类型)
由于代码很简单,所以将执行和验证的两个阶段合并到一起了。
我们主要看一下不同风格的断言:
1.classic风格,即验证某项条件为真。在这个方法中即需要验证执行方法的结果skills.getResult与我们期望的结果是否相等。也就是skills.getResult()==0
2.matchers风格,通过语义化的代码来验证结果是否匹配。整个代码不需要太多的逻辑思考,更贴近我们的阅读习惯—skills.getResult()isequalto0d
在Junit中,Assert类中提供了基础的断言API。通过assertThat(actualResult,matchers)方法可以利用Hamcrest开发的大量Matcher为我们的单元测试提供更好的语义化支持。
在此,我不强烈推荐其中任何一种,但是需要注意的是一个项目中的断言风格应该尽量保持一直。
测试异常
在编写单元测试时,如果测试结果不符合预期,JUnit会报告一次failure;如果单元测试程序运行过程中如果出现了异常,则会报告一次error。由于单元测试的隔离性,我们通常将关注点集中在需要测试的功能上,而不需要浪费时间在异常处理上,因此一些受检查异常直接在方法签名上抛出即可。
有些时候我们可能会需要验证异常抛出的正确性,以此来保证客户端使用的正确性和可靠性,有三种形式可以来验证异常行为是否合理。
1.在@Test注解中增加expected属性。如下代码如果去掉expected属性,执行单元测试则会由于运行时异常而报告一次error
2.使用try-catch方法来验证。如下,虽然可以实现,但是不够优雅。
3.使用@Rule注解,利用内置的ExpectedException来实现。JUnit中的rule规则机制实际上就是类似一种AOP的实现,为单元测试提供面向切面编程的能力,详细的内容不再此展开了。异常的断言要放在前面,否则代码就无法执行到
Deep
优化重构测试代码
在编写单元测试时,分支条件和边界条件是需要重点关注的。这也会导致一个类的单元测试往往需要编写很多单元测试。因此有必要通过优化重构来保持代码的整洁和良好的可维护性和扩展性。
在单元测试中,我们在arrange阶段往往会执行一些对象初始化等一些初始化操作,这个过程通常是重复的。可以通过@Before注解一个初始化方法,这样在每个单元测试之前都会执行这段代码了。类似的还有@After注解,只不过不太常用。
其实之前提到的@Rule注解实现的就是类似@Before和@After的功能,在执行一个单元测试方法的前后执行一些方法,只不过这些方法都是有具体的目标,通过规则这个语义实现。
注:JUnit中每个单元测试都拥有独立的上下文环境,执行每个测试方法时都会单独生成一个新的实例,因此无法保证单元测试用例的执行顺序,导致我们在单元测试中不能依赖其他的测试结果,当然这种需求本身就是“antipattern”的。
JUnit运行
一个典型的单元测试用例的执行如下
创建单元测试类实例
调用@Before方法
执行某一个被@Test注释的方法
调用@After方法
创建新的单元测试类实例
调用@Before方法
执行另外某一个被@Test注释的方法
调用@After方法
……
边界条件
编写单元测试最主要、最直接的关注点就是方法执行的正确性。其次需要关注数据的边界条件,即在某些极端的或者不正确的条件下,程序能否正常的运行或者合理的运行,以此来保证系统的健壮性。常见的关注点有以下几个
1.不合实际的值(例:表示人类年龄的字段输入180或者负数)
2.不符合格式的值(例:邮件,手机等)
3.算数溢出
4.空值,Null,空集合等
5.不合理的重复值
6.没有合理排序的值
7.事件发生的顺序异常(例:在创建用户之前就进行信息编辑等)
小结
编写单元测试算是一项内功修炼,也是一项烦琐的工作。曾国藩曾说过,“成大事者,必能耐烦”。如果单纯为了提高代码覆盖率,确实很烦;如果为了改进代码结构防患Bug于未然,就会发现单元测试是如此有用。
更多关于单元测试文章,请前往51Testing软件测试网。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/17930.html