名称解释
单元测试(unit testing)
是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,Java里单元指一个类。单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
模拟测试(mock testing)
就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。
相关技术介绍
JUnit
是一个Java语言的单元测试框架。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。
JMockit
JMockit 是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于 Java 5 SE 的 java.lang.instrument 包开发,内部使用 ASM 库来修改Java的Bytecode。所以他能解决当测试的代码包含了一些静态方法,未实现方法,未实现接口的问题。
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。
Jmock,Mockit,EasyMock,Unitils Mock
介绍略,有兴趣可自行搜索
Junit使用
在idea中,需要安装junit插件,具体安装及运行junit的方法参考:
https://jingyan.baidu.com/article/f7ff0bfccd661d2e26bb131a.html
然后在项目中引入jar包。
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
demo类:
public class Calculate {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
}
其测试类:
public class CalculateTest {
private static Calculate calculate = null;
@BeforeClass
public static void beforeClass() {
System.out.println("------------------------BeforeClass------------------------");
calculate = new Calculate();
}
@AfterClass
public static void afterClass() {
System.out.println("------------------------AfterClass------------------------");
calculate = null;
}
@Before
public void setUp() throws Exception {
System.out.println("-------Before Method-------");
}
@After
public void tearDown() throws Exception {
System.out.println("-------After Method-------");
}
@Test
public void add() throws Exception {
assertEquals(10, calculate.add(7, 3));
}
@Test
public void subtract() throws Exception {
assertEquals(4, calculate.subtract(7, 3));
}
}
在测试类中右击鼠标选择Run即可执行Junit单元测试了。结果:
如果要运行多个测试类,那么在测试包上右键,点击右键菜单中的”run Tests in <包名>”即可。
注意事项:
1、测试方法上面必须使用@Test注解进行修饰。
2、测试方法必须使用public void 进行修饰,不能带有任何参数。
3、新建一个源代码目录用来存放测试代码。
4、测试类的包应该与被测试类的包保持一致。
5、测试单元中的每一个方法必须独立测试,每个测试方法之间不能有依赖。
6、测试类使用Test做为类名的后缀(非必要)。
7、测试用例是不是用来证明你是对的,而是用来证明你没有错。
常用注解
1、@BeforeClass所修饰的方法在所有方法加载前执行,而且他是静态的在类加载后就会执行该方法,在内存中只有一份实例,适合用来加载配置文件。
2、@AfterClass所修饰的方法在所有方法执行完毕之后执行,通常用来进行资源清理,例如关闭数据库连接。
3、@Before和@After在每个测试方法执行前都会执行一次。
4、@Test(excepted=XX.class) 在运行时忽略某个异常。
5、@Test(timeout=毫秒) 允许程序运行的时间。
6、@Ignore 所修饰的方法被测试器忽略。
Jmockit使用
pom.xml配置
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.38</version>
<scope>test</scope>
</dependency>
Demo类
public class HelloJMockit {
public String sayHello() {
Locale locale = Locale.getDefault();
if (locale.equals(Locale.CHINA)) {
// 在中国,就说中文
return "你好世界";
} else {
// 在其它国家,就说英文
return "Hello World";
}
}
}
JMockit测试类:
public class HelloJMockitTest {
@Test
public void sayHelloCH() {
new Expectations(Locale.class) {
{
Locale.getDefault();
result = Locale.CHINA;
}
};
// 断言说中文
Assert.assertTrue("你好世界".equals((new HelloJMockit()).sayHello()));
}
@Test
public void sayHelloUS() {
new Expectations(Locale.class) {
{
Locale.getDefault();
result = Locale.US;
}
};
// 断言说英文
Assert.assertTrue("Hello World".equals((new HelloJMockit()).sayHello()));
}
}
在上面的例子中,对当前的位置Mock。即把测试代码的依赖抽象成期待(Expectations),在进行断言。
Jmockit如何模拟非静态对象:
@Mocked
HelloJMockit helloJMockit;
@Test
public void sayHello1() {
// 录制(Record)
new Expectations() {
{
helloJMockit.sayHello();
// 期待上述调用的返回是"hello,david",而不是返回实际返回值
result = "hello david";
}
};
// 重放(Replay)
String msg = helloJMockit.sayHello();
Assert.assertTrue(msg.equals("hello david"));
// 验证(Verification)
new Verifications() {
{
helloJMockit.sayHello();
// 验证helloJMockit.sayHello()这个方法调用了1次
times = 1;
}
};
}
上面的方法也可以按需写成如下格式:
public void sayHello1(@Mocked HelloJMockit helloJMockit) {…}
结构分析
通过上述例子可以看出,JMockit的程序结构包含了测试属性,测试方法。
测试方法体中又包含录制代码块,重放测试逻辑,验证代码块(Record-Replay-Verification): Record: 即先录制某类/对象的某个方法调用,在当输入什么时,返回什么。 Replay: 即重放测试逻辑。 Verification: 重放后的验证。比如验证某个方法有没有被调用,调用多少次。
常用注解及常用类
1. @Mocked
在上述例子中,我们用@Mocked修饰了测试属性HelloJMockit helloJMockit,表示helloJMockit这个测试属性,它的实例化,属性赋值,方法调用的返回值全部由JMockit来接管,接管后,helloJMockit的行为与HelloJMockit类定义的不一样了,而是由录制脚本来定义了。
@Mocked不仅能修饰一个类,也能修饰接口。@Mocked修饰的类/接口,是告诉JMockit,帮我生成一个Mocked对象,这个对象方法(包含静态方法)返回默认值。
2. @Tested & @Injectable
@Injectable 也是告诉 JMockit生成一个Mocked对象,但@Injectable只是针对其修饰的实例,而@Mocked是针对其修饰类的所有实例。此外,@Injectable对类的静态方法,构造函数没有影响。因为它只影响某一个实例。
@Tested修饰的类,表示是我们要测试对象,如果该对象没有赋值,JMockit会去实例化它。
@Tested & @Injectable通常搭配使用。若@Tested的构造函数有参数,则JMockit通过在测试属性、测试参数中查找@Injectable修饰的Mocked对象注入@Tested对象的构造函数来实例化,不然,则用无参构造函数来实例化。
除了构造函数的注入,JMockit还会通过属性查找的方式,把@Injectable对象注入到@Tested对象中。注入的匹配规则:先类型,再名称(构造函数参数名,类的属性名)。若找到多个可以注入的@Injectable,则选择最优先定义的@Injectable对象。当然,我们的测试程序要尽量避免这种情况出现。因为给哪个测试属性/测试参数加@Injectable,是人为控制的。
我们以电商网站下订单的场景为例,在买家下订单时,电商网站后台程序需要校验买家的身份,若下订单没有问题还要发短信给买家。
订单类:
public class OrderService {
// 短信服务类,用于向某用户发短信。
@Autowired MessageService messageService;
// 用户服务类,用于校验某个用户是不是合法用户
@Autowired UserService userService;
// 下单
public boolean submitOrder(long userId) {
// 先校验用户身份
if (!userService.check(userId)) {
// 用户身份不合法
return false;
}
// 下单
this.saveOrder(order);// TODO 逻辑略…
// 下单完成,给买家发短信
if (this.messageService.sendMessage(userId, "下单成功")) {
// 短信发送成功
return true;
}
return false;
}
}
测试类:
public class TestedAndInjectable {
//@Tested修饰的类,表示是我们要测试对象。JMockit会帮我们实例化这个测试对象
@Tested
OrderService orderService;
// 测试注入方式
@Test
public void testSubmitOrder(@Injectable MessageService messageService,
@Injectable UserCheckService userCheckService,
@Injectable Order testOrder) {
long testUserId = 123l;
//实例化MessageService,userCheckService,通过OrderService属性,注入对象中;
new Expectations() {
{
// 当向testUserId发短信时,假设都发成功了
messageService.sendMessage(testUserId, anyString);
result = true;
// 当检验testUserId的身份时,假设该用户都是合法的
userCheckService.check(testUserId);
result = true;
}
};
Order testOrder = new Order("嘟嘟机器人", 996)
Assert.assertTrue(orderService.submitOrder(testUserId, testOrder));
}
}
3. @Capturing
@Capturing主要用于子类/实现类的Mock, 我们只知道父类或接口时,但我们需要控制它所有子类的行为时,子类可能有多个实现(可能有人工写的,也可能是AOP代理自动生成时)。就用@Capturing。
4.MockUp & @Mock
这种方式非常简单,直接,很多程序员们都喜欢用,掌握了MockUp & @Mock能帮我们解决大部分的Mock场景。
案例如下:
class MockUpTest {
@Test
public void testMockUp() {
// 对Java自带类Calendar的get方法进行定制
// 只需要把Calendar类传入MockUp类的构造函数即可
new MockUp<Calendar>(Calendar.class) {
// 想Mock哪个方法,就给哪个方法加上@Mock, 没有@Mock的方法,不受影响
@Mock
public int get(int unit) {
if (unit == Calendar.YEAR) {
return 2017;
}
if (unit == Calendar.MONDAY) {
return 1;
}
return 0;
}
};
// 从此Calendar的get方法,就沿用你定制过的逻辑,而不是它原先的逻辑。
Calendar cal = Calendar.getInstance(Locale.FRANCE);
Assert.assertTrue(cal.get(Calendar.YEAR) == 2017);
Assert.assertTrue(cal.get(Calendar.MONDAY) == 1);
// Calendar的其它方法,不受影响
Assert.assertTrue((cal.getFirstDayOfWeek() == Calendar.MONDAY));
}
}
MockUp & @Mock比较适合于一个项目中,用于对一些通用类的Mock,以减少大量重复的new Exceptations{{}}代码。
在实际Mock场景中,我们需要灵活运用JMockit其它的Mock API。让我们的Mock程序简单,高效。
一个类有多个实例,但只对其中某1个实例进行mock的场景是MockUp & @Mock做不到的,这种时候就需要上述的@Capturing注解了。
5. Expectations
Expectations的作用主要是用于录制。即录制类/对象的调用,返回值是什么。主要有两种使用方式:
a.通过引用外部类的Mock对象(@Injectabe,@Mocked,@Capturing)来录制;
b.通过构建函数注入类/对象来录制.
6. Verifications
Verifications是用于做验证。验证Mock对象(即@Moked/@Injectable@Capturing修饰的或传入Expectation构造函数的对象)有没有调用过某方法,调用了多少次。
通常在实际测试程序中,我们更倾向于通过JUnit/TestNG/SpringTest的Assert类对测试结果的验证, 对类的某个方法有没调用,调用多少次的测试场景并不是太多。因此在验证阶段,我们完全可以用JUnit/TestNG/SpringTest的Assert类取代new Verifications() {{}}验证代码块。除非,你的测试程序关心类的某个方法有没有调用,调用多少次,你可以使用new Verifications() {{}}验证代码块。
常见用法
案例类(这个类有public,static,final,private方法):
class AnOrdinaryClass {
// 普通方法
public int ordinaryMethod() {
return 1;
}
// 静态方法
public static int staticMethod() {
return 2;
}
// final方法
public final int finalMethod() {
return 3;
}
// private方法
private int privateMethod() {
return 4;
}
// 调用private方法
public int callPrivateMethod() {
return this.privateMethod();
}
}
a. 测试类1(用Expectations来Mock):
class ClassMockingByExpectationsTest {
@Test
public void testClassMockingByExpectation() {
AnOrdinaryClass instanceToRecord = new AnOrdinaryClass();
new Expectations(AnOrdinaryClass.class) {
{
// mock普通方法
instanceToRecord.ordinaryMethod();
result = 11;
// mock静态方法
AnOrdinaryClass.staticMethod();
result = 22;
// mock final方法
instanceToRecord.finalMethod();
result = 33;
// private方法无法用Expectations来Mock
}
};
AnOrdinaryClass instance = new AnOrdinaryClass();
Assert.assertTrue(instance.ordinaryMethod() == 11);
Assert.assertTrue(AnOrdinaryClass.staticMethod() == 22);
Assert.assertTrue(instance.finalMethod() == 22);
// 用Expectations无法mock private方法
Assert.assertTrue(instance.callPrivateMethod() == 4);
}
}
b. 测试类2(用MockUp来Mock):
class ClassMockingByMockUpTest {
// AnOrdinaryClass的MockUp类,继承MockUp即可
public static class AnOrdinaryClassMockUp extends MockUp<AnOrdinaryClass> {
// Mock普通方法
@Mock
public int ordinaryMethod() {
return 11;
}
// Mock静态方法
@Mock
public static int staticMethod() {
return 22;
}
@Mock
// Mock final方法
public final int finalMethod() {
return 33;
}
// Mock private方法
@Mock
private int privateMethod() {
return 44;
}
}
@Test
public void testClassMockingByMockUp() {
new AnOrdinaryClassMockUp();
AnOrdinaryClass instance = new AnOrdinaryClass();
// 普通方法被mock了
Assert.assertTrue(instance.ordinaryMethod() == 11);
// 静态方法被mock了
Assert.assertTrue(AnOrdinaryClass.staticMethod() == 22);
// final方法被mock了
Assert.assertTrue(instance.finalMethod() == 33);
// private方法被mock了
Assert.assertTrue(instance.callPrivateMethod() == 44);
}
}
总结
建议使用MockUp & @Mock方法来写单元测试,JUnit的Assert类对测试结果的验证!并且灵活运用JMockit其它的Mock API。
代码覆盖率
由于JMockit使用JavaSE5中的java.lang.instrument包开发,因此一般的单元测试覆盖率统计插件和工具对其无法工作,必须要借助自带的JMockit coverage才行。需要在项目pom.xml中如下定义
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:"${settings.localRepository}\org\jmockit\jmockit\1.38\jmockit-1.38.jar=coverage"</argLine>
<disableXmlReport>false</disableXmlReport>
<systemPropertyVariables>
<coverage-output>html</coverage-output>
<coverage-outputDir>D:\temp\codecoverage-output</coverage-outputDir>
<coverage-metrics>all</coverage-metrics>
</systemPropertyVariables>
</configuration>
</plugin>
配置完成后在运行测试代码,就会在控制台看到如下提示
JMockit: Coverage report written to D:\temp\codecoverage-output
进入到所示目录下(D:\temp\codecoverage-output),打开index.html,即可看到测试报告,如下图所示:
附录
jmockit中文网网址:http://jmockit.cn/