Java测试框架之JMockit
JMockIt
maven依赖:
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
这里的依赖是成对的并且有先后顺序耦合
@Mocked
当@Mocked修饰一个具体类时,会mock该具体类所有成员属性,若是基本类型,返回原始0,若是String则返回null,若是其他依赖的引用类型,则继续mock它使其不为空引用,但递归地,其内部的对象引用任然像上面那样继续递归mock。
//@Mocked注解用途
public class MockedClassTest {
// 加上了JMockit的API @Mocked, JMockit会帮我们实例化这个对象,不用担心它为null
@Mocked
Locale locale;
// 当@Mocked作用于class
@Test
public void testMockedClass() {
// 静态方法不起作用了,返回了null
Assert.assertTrue(Locale.getDefault() == null);
// 非静态方法(返回类型为String)也不起作用了,返回了null
Assert.assertTrue(locale.getCountry() == null);
// 自已new一个,也同样如此,方法都被mock了
Locale chinaLocale = new Locale("zh", "CN");
Assert.assertTrue(chinaLocale.getCountry() == null);
}
}
//@Mocked注解用途
public class MockedInterfaceTest {
// 加上了JMockit的API @Mocked, JMockit会帮我们实例化这个对象,尽管这个对象的类型是一个接口,不用担心它为null
@Mocked
HttpSession session;
// 当@Mocked作用于interface
@Test
public void testMockedInterface() {
// (返回类型为String)也不起作用了,返回了null
Assert.assertTrue(session.getId() == null);
// (返回类型为原始类型)也不起作用了,返回了0
Assert.assertTrue(session.getCreationTime() == 0L);
// (返回类型为原非始类型,非String,返回的对象不为空,这个对象也是JMockit帮你实例化的,同样这个实例化的对象也是一个Mocked对象)
Assert.assertTrue(session.getServletContext() != null);
// Mocked对象返回的Mocked对象,(返回类型为String)的方法也不起作用了,返回了null
Assert.assertTrue(session.getServletContext().getContextPath() == null);
}
}
@Injected @Tested
@Mocked对mock的类所有实例进行mock。在特定场景下,只需要对依赖的实例进行mock,搭配使用@Injected @Tested来实现这种功能:
简单demo演示@Injected @Mocked区别
public class TestJMockitTest {
@Injectable
private TestJMockit testJMockit;
@Test
public void printStringInConsole() {
System.out.println("start ========");
new TestJMockit().printStringInConsole(); // 会打印
System.out.println("end ========");
}
}
public class TestJMockitTest {
@Mocked
private TestJMockit testJMockit;
@Test
public void printStringInConsole() {
System.out.println("start ========");
new TestJMockit().printStringInConsole(); // 不会打印
System.out.println("end ========");
}
}
public class TestJMockit {
public void printStringInConsole() {
System.out.println("Im Stephen Curry 3 Points!!!");
}
}
可见@Mocked针对类型mock,@Injected针对类实例mock
@Capturing
@Capturing 意为捕捉,JMockit中,当知道基类或者接口时,想要控制其所有子类的实现,则使用@Capturing,就像 “捕捉” 本身的意义一样。
public interface BasketballPlayer {
/**
* 篮球运动员得分技能
*/
int getScore(String name);
}
@SpringBootTest
@RunWith(SpringRunner.class)
public class BasketballPlayerTest {
private BasketballPlayer basketballPlayer1 = name -> {
if (name.equals("Kobe")) {
return 81;
}
return 0;
};
private BasketballPlayer wonderfulGiftPlayer =
(BasketballPlayer) Proxy.newProxyInstance(BasketballPlayer.class.getClassLoader(),
new Class[]{BasketballPlayer.class},
(proxy, method, args) -> {
if (args[0].equals("Kobe")) {
return 81;
}
return 0;
});
// 上面是JDK动态代理生成的BasketballPlayer实例
// 如果是科比则得分81
@Test
public void getScore(@Capturing BasketballPlayer basketballPlayer) {
// @Capturing 会捕捉BasketballPlayer所有实例,即使是运行时生成的动态实例。
new Expectations() {
{
basketballPlayer.getScore(anyString);
result = 81;
}
};
Assert.assertEquals(81, basketballPlayer1.getScore("couldBeAnyString"));
// Assert.assertEquals(81, wonderfulGiftPlayer.getScore("couldBeAnyString")); // 不能测试成功,JDK动态代理
}
}
上面的JDK动态代理的mock失败了,但是文档上貌似显示是支持的,暂未找到原因...
可见,@Mocked 和 @Capturing 的区别,前者mock不能影响子类和实现类。因此,在一些第三方API需要mock时,就使用@Capturing去捕捉这种关系,但是其实若是单一的第三方接口,直接@Mocked出一个匿名内部类也可以实现,@Capturing可以运用于一些比较特定的场合,到时候找不到解决方式时就会想到还有一个@Capturing,一般情况下,是不怎么使用到@Capturing的。
mockup 和 @mock的搭配使用
不建议使用此方式,因为这种方式是new一个匿名内部类,在其中对想要mock的方法一个一个添加@Mock去mock,看似增加了定制化,但是实际上每个需要被mock的方法都要手动实现一遍:一是不够优雅,二是比如mock一个HTTPSession这种,需要大量的@Mock手动实现,而此时@Mocked一行就可以解决。功能只需要在expection中去record即可。
Expectations
所谓的 record-replay (录制-回放)功能,在此种方式下,可录制所有想要被mock的方法和它的返回值,代码比较优雅。
需要注意的是在new expectations内部中录制过程中,要再手动添加一对大括号{}。
new Expectations() {
// 需要用一对大括包住录制过程
{
basketballPlayer.getScore(anyString);
result = 81;
}
};
@Verification
new Verifications() {
// 这是一个Verifications匿名内部类
{
// 这个是内部类的初始化代码块,我们在这里写验证脚本,脚本的格式要遵循下面的约定
//方法调用(可是类的静态方法调用,也可以是对象的非静态方法调用)
//times/minTimes/maxTimes 表示调用次数的限定要求。赋值要紧跟在方法调用后面,也可以不写(表示只要调用过就行,不限次数)
//...其它准备验证脚本的代码
//方法调用
//times/minTimes/maxTimes 赋值
}
};
还可以再写new一个Verifications,只要出现在重放阶段之后均有效。
new Verifications() {
{
//...验证脚本
}
};
整个验证过程大致分为
{录制}
回放
验证
这里的@Verification就是用来验证,比如一个方法调用几次,或者是调用次数的上下限,都可以检验。不满足即抛出错误。使用较少。
以上,大概就是JMockit使用基础。
零配置启动mock
package cn.emitor.spring4d.utils;
import mockit.Expectations;
import mockit.Injectable;
import mockit.Tested;
import org.junit.Assert;
import org.junit.Test;
/**
* @author Emitor
* on 2018/12/26.
*/
public class MyJMockitTestWorkTest {
@Injectable
MyMockItDependencyObject dependencyObject;
@Tested
MyJMockitTestWork work;
@Test
public void sdOutPrint() {
new Expectations(){
{
dependencyObject.getMySayHelloANumber();
result = 1;
}
};
Assert.assertEquals("oh it's not my expectation~", work.sdOutPrint(1), dependencyObject.getMySayHelloANumber());
new Expectations() {
{
dependencyObject.getMyA();
result = 1;
}
};
Assert.assertEquals("oh, it's not my expectation!", work.sdOutPrint(1), dependencyObject.getMyA());
}
}
依赖的两个类:
public class MyJMockitTestWork {
public Integer sdOutPrint(Integer a) {
return a;
}
}
@Component
public class MyMockItDependencyObject {
public final void a() {
}
public static void b() {
}
public static final String e = "1";
private static void c() {
}
private Integer a = 1;
public Integer getMySayHelloANumber() {
return new Random().nextInt(10);
}
public Integer getMyA() {
return a;
}
}
就可以实现 record-replay 功能的测试,解决功能依赖性问题。
简单注解配置实现拥有Spring上下文的测试环境
@RunWith(SpringRunner.class)
@SpringBootTest
public class CanIBeAutowiredssTest {
@Autowired
private CanIBeAutowired canIBeAutowired;
@Autowired
DoShitController doShitController;
@Test
public void getAString() {
String xMas = canIBeAutowired.getAString();
Assert.assertNotNull("oh, no xMas", xMas);
Assert.assertEquals(xMas, "merry Xmas");
Assert.assertEquals(doShitController.doShitLikeAlways(""), "doShitLikeAlwaysBe: ");
}
}
上面即实现了自动注入,即这里已经出现Spring上下文。
其中, @RunWith(SpringRunner.class) 和 @SpringBootTest 缺一不可。前者缺失导致无法注入值,即@AutoWired下面的为null。而缺失后者导致bean无法创建错误。
简单配置实现controller测试
package cn.emitor.spring4d.controllers;
import mockit.Injectable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Emitor
* on 2018/12/26.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class DoControllerTest {
@Autowired
protected WebApplicationContext wac;
@Injectable
MockMvc mockMvc;
@Before() //这个方法在每个方法执行之前都会执行一遍
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); //初始化MockMvc对象
}
@Test
public void doLikeAlways() throws Exception {
ResultActions resultActions = mockMvc.perform(get("/api").param("ApiName", "this is a api name"));
String returnString = resultActions.andExpect(status().isOk())
.andDo(print())
.andReturn().getResponse().getContentAsString();
System.out.println(returnString);
}
}
这里产生spring运行content以便mock出想要的网络请求和自动注入效果。
JMockit大概是现在Java测试框架中,功能最强大的之一了。它具有上手容易,代码优雅等有点,使用得当,在开发中写测试用例时比较得力,很好的解决了非传统意义上单元测试中的依赖问题。