倚楼听风雨
淡看江湖路

阿里巴巴Java开发手册第三章-单元测试篇

关于单元测试,参考许晓斌大大的《Maven实战》简单整理一下关于单元测试与Maven的结合使用以及基本的单元测试框架的介绍。另外再次安利一遍,如果你是Java从业者,或者说你正在使用Maven来管理自己的项目,请购买许晓斌大大的这本《Maven实战》,看书是小成本,大回报,另外别再四处搜寻那些盗版的pdf电子书了。。。

在Java社区中基本流行着两个单元测试框架,一个是Junit,另外一个是TestNG,因为后者老四基本没用过(压根),所以可能几句话带过。

随着敏捷开发模式的日益流行,软件开发人员也越来越认识到日常编程工作中单元测试的重要性。Maven的重要职责之一就是自动运行单元测试,它通过maven-surefire-plugin与主流的单元测试框架JUnit 3,JUnit 4以及TestNG集成,并且能够自动生成丰富的结果报告。

1.测试方法

JUnit 4中使用注释类识别: @Test,也不必约束测试方法的名字。

2.固件测试

所谓固件测试(Fixture),就是测试运行运行程序(test runner)会在测试方法之前自动初始化、和回收资源的工作。JUnit 4中,通过@Before注解替代之前的setUp方法;@After注解替代之前tearDown方法。在一个测试类中,甚至可以使用多个@Before 来注释多个方法。

注意: @Before是在每个测试方法运行前均初始化一次,同理@After 在每个测试方法运行完毕后也运行一次,虽然保证了各个测试之间的独立性,互不干扰,但是却消耗了系统资源,效率变低。为了不运行多次,你可以使用@BeforeClass和@AfterClass注解,这两个注解在类的初始化和类结束之后调用,并且就运行一次。

3.异常测试

通过对@Test注解传入expected参数,即可测试异常。通过传入异常类后,测试类如果没有抛出异常或者抛出一个不同的异常,本测试方法就将失败。

4.超时测试

通过在@Test 注释中,为timeout参数指定时间值,即可进行超时测试。如果测试运行时间超过指定的毫秒数,则测试失败。超时测试对网络连接类非常重要,通过timeout进行超时测试,简单异常。

5.测试运行器

JUnit中所有的测试方法都是由它负责执行。JUnit为单元测试提供了默认的测试运行器,但是没有限制必须使用默认的运行器。自己定制的测试运行器必须继承自org.junit.runner.Runner。而且还可以通过@Runwith(CustomTestRunner.class)注解为每一个测试类指定某个运行器。

6.测试套件

JUnit4中最显著的特性是没有套件(套件机制用于将测试从逻辑上的分组并将这些测试作为一个单元测试来运行)。为了替代老版本的套件测试,套件被@RunWith、@SuteClasses注解替代。通过@RunWith指定一个特殊的运行器: Suite.class套件运行器,并通过@SuiteClasses注解,将需要进行测试的类列表作为参数传入。

7.参数化测试

为测试程序健壮性,可能需要模拟不同的参数对方法进行测试,为每一个类型的参数都创建一个测试方法是不理智的。参数化测试能够创建由参数值供给的通用测试,从而使每个参数都运行一次,而不必要创建多个测试方法。编写流程如下:

  • @RunWith注解指定特殊的运行器: Parameterized.class;
  • 测试类中声明几个变量,分别用于存储期望值和测试用的数据,并创建一个使用者几个参数的构造函数;
  • 创建一个静态测试数据供给(feed)方法,其返回类型为集合(Collection),并用@Parameter注解加以修饰;
package com.glorze.junit;

import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.Before;
import org.junit.Test;
import org.junit.runners.Parameterized.Parameters;

/**
 * 参数化单元测试类
 * @ClassName ParameterTest 
 * @author: glorze.com  
 * @since: 2018年6月24日 下午7:24:39
 */
public class ParameterTest {
	private String dateReg;
	private Pattern pattern;
	/**
	 * 数据成员变量
	 */
	private String phrase;
	private boolean match;

	/**
	 * 使用数据的构造函数
	 * ParameterTest.  
	 * @param phrase
	 * @param match
	 */
	public ParameterTest(String phrase, boolean match) {
		super();
		this.phrase = phrase;
		this.match = match;
	}

	@Before
	public void init() {
		dateReg = "^\\d{4}(\\-\\d{1,2}){2}";
		pattern = Pattern.compile(dateReg);
	}

	@Test
	public void verifyDate() {
		Matcher matcher = pattern.matcher(phrase);
		boolean isValid = matcher.matches();
		assertEquals("Pattern don't validate the data format", isValid, match);
	}

	/**
	 * 数据供给方法(静态,用 @Parameter注释,返回类型为 Collection)
	 * @Title: dateFeed
	 * @return Collection
	 * @author: 高老四博客 
	 * @since: 2018年6月24日 下午7:24:00
	 */
	@Parameters
	public static Collection<Object[]> dateFeed() {
		return Arrays.asList(new Object[][] { { "2010-1-2", true }, { "2010-10-2", true }, { "2010-123-1", false },
				{ "2010-12-45", false } });
	}
}

8.数组断言

JUnit4中添加了一个用于比较数组的新断言(Assert),这样不必使用迭代比较数组中的条目。

public static void assertEquals(Object[] expected, Object[] actual) {
		
}
public static void assertEquals(String message, Object[] expected,Object[] actual) {
		
}

JUnit的基本知识已经大致说了一些了,接下来我继续浅析孤尽的单元测试规约。

1.[强制] 好的单元测试必须遵守AIR原则。

说明: 单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。

老四附言:
如何在Maven中跳过测试?日常工作中,我们总有很多理由来跳过单元测试,就拿老四来说,总觉得测试是测试人员专职做的事情,总是不想在这上面浪费时间,然而事实证明我是极其错误的。因为无论是逻辑清晰的改动还是小小的修改都曾让我付出巨大的上线崩溃的代价。所以其实任何改动的都要去遵守测试流程,最起码自己是要验证一下的。然而很多时候在Maven中我们是跳过测试的,比如开源项目的运行,抑或是孤尽所言的上线测试要像空气一样并不存在,这个时候都需要配置的。Maven中的跳过测试的命令如下:

mvn package -DskipTests
# 连测试代码的编译也跳过,不推荐
mvn package -Dmaven.test.skip=true

2.[强制] 单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.out来进行人肉验证,必须使用assert来验证。

老四附言:
一个相对标准的单元测试类供你参考:

package com.glorze.junit;

/**
 * 标准单元测试类
 * @ClassName Claculate 
 * @author: glorze.com  
 * @since: 2018年6月24日 下午7:46:27
 */
public class Claculate {
	
	public int add(int a, int b) {
		return a + b;
	}

	public int subtract(int a, int b) {
		return a - b;
	}

	public int multiply(int a, int b) {
		return a * b;
	}

	public int divide(int a, int b) {
		return a / b;
	}

}
package com.glorze.junit;

import static org.junit.Assert.*;
import org.junit.Test;

/**
 * 使用assert做单元测试
 * @ClassName ClaculateTest 
 * @author: glorze.com  
 * @since: 2018年6月24日 下午7:48:09
 */
public class ClaculateTest {

	@Test
	public void testAdd() {
		assertEquals(4, new Claculate().add(1, 3));
	}

	@Test
	public void testSubtract() {
		assertEquals(4, new Claculate().subtract(9, 5));
	}

	@Test
	public void testMultiply() {
		assertEquals(6, new Claculate().multiply(2, 3));
	}

	@Test(expected = ArithmeticException.class)
	public void testDivide() {
		assertEquals(3, new Claculate().divide(9, 0));
	}
	
}
3.[强制] 保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。

反例: method2需要依赖method1的执行,将执行结果作为method2的输入。

老四附言:

个人写个人的,别去别人那里瞎掺和。

4.[强制] 单元测试是可以重复执行的,不能受到外界环境的影响。

说明: 单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。

正例: 为了不受外界环境影响,要求设计代码时就把SUT(System under test,被测系统)的依赖改成注入,在测试时用spring这样的DI框架注入一个本地(内存)实现或者Mock实现。

老四附言:

5.[强制] 对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。

说明: 只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域。

老四附言:

单元测试:

测试最小的功能单元,通常是一个方法/函数(例如,给定一个具有特定状态的类,在类上调用x方法应该导致y发生)。单元测试应该集中在一个特定的功能上(例如,当栈空时调用pop方法应该抛出一个InvalidOperationException)。它所触及的一切应该在记忆中完成; 这意味着测试代码和测试代码不应该:

  • 访问网络
  • 打一个数据库
  • 使用文件系统
  • 开启一个线程

简而言之,单元测试尽可能简单,易于调试,可靠(由于外部因素减少),执行速度快,并且有助于证明程序中最小的构建块在组合之前按照预期运行。需要注意的是,尽管你可以证明它们完全孤立地工作,但是代码的单元在组合时可能会炸毁。

集成测试:

集成测试通过将代码单元和测试结果组合起来进行单元测试,从而生成的组合功能正确。这可以是一个系统的内部结构,也可以将多个系统组合在一起,做一些有用的事情。另外,区分集成测试和单元测试的另一件事是环境。集成测试可以并将使用线程,访问数据库或执行所需的任何操作,以确保所有代码和不同的环境更改都能正常工作。

如果你已经构建了一些序列化代码,并且单元测试了它的内部而不接触磁盘,那么当你加载并保存到磁盘时,你怎么知道它会起作用?也许你忘了刷新和处理文件流。也许你的文件权限是不正确的,你已经测试了在内存流中使用的内部。唯一可以确定的方法是使用最接近生产的环境对其进行"真实"测试。

主要优点是他们会发现单元测试无法接收错误的错误(例如A类的实例意外收到B的空实例)和环境错误(它在我的单CPU机器上运行良好,但我的同事的4核心机器不能通过测试)。主要缺点是集成测试涉及更多的代码,不太可靠,故障难以诊断并且测试难以维护。

此外,集成测试并不一定能证明一个完整的功能。

功能测试:

就是测试系功能是否正常。。。。

6.[强制] 核心业务、核心应用、核心模块的增量代码确保单元测试通过。

说明: 新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。

老四附言:
业务代码要与测试代码同时进行,即使不同时进行也要缺一不可。

7.[强制] 单元测试代码必须写在如下工程目录: src/test/java,不允许写在业务代码目录下。

说明: 源码构建时会跳过此目录,而单元测试框架默认是扫描此目录。

老四附言:
无论什么业务代码都不要在你的类里面写main方法,导致每个人的版本管理系统都不尽相同,最后提交代码的时候经常导致与本地冲突。测试代码写在src/test/java中是Maven中的规定,如果你不使用Maven,请遵守当前框架自己约定的测试体系来进行测试代码的编写。

关于Maven中的这种规定,实际上是体现了Maven的最核心设计理念之一----"约定优于配置(Convention Over Configuration)"。参考许晓斌大大的《Maven实战》,老四简单的说一说这个"约定优于配置":  我们都知道web应用都需要基于HTTP协议,Java的特性是一次编译,处处运行,那么这些技术应用的兼容性往往都是因为他们遵循某种特定的规则来运行,再比如,电脑上的接口无论是USB还是VGA他都是那个样子的,不可能一台的电脑USB接口与其他电脑的USB接口不一样,只有这样的标准才能使外接设备完美的适配扩展起来。同理,Maven的设计者也是采取了这样的设计思想,我们都知道Maven的源码目录src/main/java,编译输出目录target/classes,配置文件目录src/main/resources,这都是我们必须严格遵守的,不能随意安排目录结构,这样的设计虽然让用户付出一定的不灵活代价,但是却为Maven节省了大量的繁杂配置,使得Maven为你对项目进行编译、清理、打包、部署等带来了极其简便的配置。关于Maven的更多内容如果您想了解,强烈建议您买一本《Maven实战》看一看。

8.[推荐] 单元测试的基本目标:语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%。

说明: 在工程规约的应用分层中提到的DAO层,Manager层,可重用度高的Service,都应该进行单元测试。

老四附言:
工程规约的浅析老四之前写过了,关于应用分层您可以参考这篇文章《阿里巴巴Java开发手册第六章-应用分层篇》。

9.[推荐] 编写单元测试代码遵守BCDE原则,以保证被测试模块的交付质量。
  • B: Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
  • C: Correct,正确的输入,并得到预期的结果。
  • D: Design,与设计文档相结合,来编写单元测试。
  • E: Error,强制错误信息输入(如: 非法数据、异常流程、非业务允许输入等),并得到预期的结果。

老四附言:

已经很清晰明了了,BCDE原则是你项目稳定的基石。

10.[推荐] 对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。

反例: 删除某一行数据的单元测试,在数据库中,先直接手动增加一行作为删除目标,但是这一行新增数据并不符合业务插入规则,导致测试结果异常。

老四附言:

其实这样要求就是为了避免手动给测试带来的结果偏差,我们手动操作的数据往往会因为心神不宁、失误而将数据错误化,所以在对数据库的操作当中,我们尽力要用程序来生成测试数据,不懂万不得已不要直接去改动某一数据行。

11.[推荐] 和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者对单元测试产生的数据有明确的前后缀标识。

正例: 在RDC(Research and Development Collaboration,研发合作)内部单元测试中,使用RDC_UNIT_TEST_的前缀标识数据。

老四附言:
我们在做单元测试中其实一般都是要有测试数据库的,在测试数据库中其实没有必要做测试数据的标识,完全可以模拟正式数据进行测试工作。然而真正的业务场景当中,我们常常需要对正式的数据库进行业务测试,所以有时候测试数据难免入库,这个时候我们需要通过前缀或者临时表等方案来表示测试数据以免影响正式数据的分析等等。

12.[推荐] 对于不可测的代码建议做必要的重构,使代码变得可测,避免为了达到测试要求而书写不规范测试代码。

老四附言:
就目前的老四所处、所经历的项目来讲(多半是Java Web项目或者前后端分离的项目),现在测试技术覆盖面还是相当全面的,所以测试范围一定要尽全力覆盖,不要因为测试流程复杂或者难以开头而放弃测试,那样的话早晚会付出代价。

13.[推荐] 在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例(UC,Use Case)。

老四附言:
测试到位,上线无忧啊!此时一名名叫小帅的网友路过,据说他经常因为项目没有任何测试而经常部署项目。

14.[推荐] 单元测试作为一种质量保障手段,不建议项目发布后补充单元测试用例,建议在项目提测前完成单元测试。

老四附言:

这是体系成型的项目必须具备的,不应该建议,而应该称为准则。然而老四所在企业一切都正在完善当中,这也正好锻炼我,且行且珍惜,且努力吧。

15.[参考] 为了更方便地进行单元测试,业务代码应避免以下情况:
  • 构造方法中做的事情过多。
  • 存在过多的全局变量和静态方法。
  • 存在过多的外部依赖。
  • 存在过多的条件语句。

说明: 多层条件语句建议使用卫语句、策略模式、状态模式等方式重构。

老四附言:

看到卫语句是不是有点懵逼?是不是现在立马就想找李彦宏问问?还是找谷大哥吧~~其实呢卫语句这个词太学术性了。但是它具体是什么其实我们应该都体会过并且自己亲自写过的。举个栗子:

package com.glorze.interview.one;

/**
 * 卫语句示例
 * @ClassName GuardClauses
 * @author: glorze.com
 * @since: 2018年6月4日 下午11:17:47
 */
public class GuardClauses {

	private static Integer type = 8888;

	public static void setType() {
		type = 666;
	}

	public static void main(String[] args) {
		
		int num1 = 0;
		int num2 = 1;
		int num3 = 2;
		
		if (type == num1) {
			return;
		} else {
			if (type == num2) {
				return;
			} else {
				if (type == num3) {
					return;
				} else {
					setType();
				}
			}
		}
		// 使用卫语句改写上面繁杂的条件判断
		if (type == num1) {
			return;
		}
		if (type == num2) {
			return;
		}
		if (type == num3) {
			return;
		}
		setType();
	}
}

恩恩,这就是卫语句,说白了就是尽量避免if-else if-else这样繁杂的写法,而是根据具体逻辑及时return或者拆解条件语句。

至于策略模式以及状态模式是两大设计模式,具体可以参考浅析的设计模式模式系列之《浅析设计模式第二章之策略模式》和《浅析设计模式第十六章之状态模式》这两篇文章。

16.[参考] 不要对单元测试存在如下误解:
  • 那是测试同学干的事情。本文是开发手册,凡是本文内容都是与开发同学强相关的。
  • 单元测试代码是多余的。汽车的整体功能与各单元部件的测试正常与否是强相关的。
  • 单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态。
  • 单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障。

老四附言:
说得真好,其实一直到现在老四都有这样的毛病,对单元测试一直都比较排斥,但是由于公司没有专职的测试人员,现在我也正在一步一步走向正轨的道路上,希望看到这篇文章的你之后跟老四一样有这个毛病的能够改邪归正,没有这个毛病的就好好深化的自己的用例测试,规范自己,养成良好的编码习惯。
最后把JUnit和TestNG官网链接奉上,JUnit 5已经发布了。

更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请赞助盒烟钱,点我去赞助。抱拳。

赞(85) 给你买杜蕾斯
本站原创文章受自媒体平台原创保护,未经允许不得转载高老四博客 » 阿里巴巴Java开发手册第三章-单元测试篇

开始你的表演 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下老四,鼓励我更好的创作

支付宝扫一扫打赏

微信扫一扫打赏