Jmockit 使用方法小结
maven 包依赖, jmockit 必须在 junit 之前
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>${jmockit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
准备
先创建一个场景, 方便后面的讲解; 假设 现在有一个User 服务 UserService, 其功能是对 User表的增删改查; 则依赖 UserDAO; 代码如下:
@Getter
@Setter
public class UserDO {
private Long id;
private String name;
}
public interface UserDAO {
Long insert(UserDO userDO);
UserDO findById(Long id);
}
public interface UserService {
/**
* 创建 user , 演示用, 特意使用多参数 而不是 UserDO
* @param name
* @return
*/
Long createUser(String name);
/**
*
* @param id
* @return
*/
UserDO findById(Long id);
}
public class UserServiceImpl implements UserService {
@Autowired
UserDAO userDAO;
@Override
public Long createUser(String name) {
UserDO newUser = new UserDO();
newUser.setName(name);
Long count = userDAO.insert(newUser);
if(count != 1){
throw new RuntimeException("some error");
}
return count;
}
@Override
public UserDO findById(Long id) {
UserDO userDO = userDAO.findById(id);
//演示用特殊逻辑
if(null != userDO && userDO.getName().startsWith("BLACK")){
return null;
}
return userDO;
}
}
测试类的结构
@RunWith(JMockit.class)
public class UserServiceImplTest {
@Tested //要测试的目标, 使用此注解
UserServiceImpl userService;//我们测试的是 "接口的实现", 只有实现才有"逻辑", 所以是 UserServiceImpl
@Injectable //测试目标依赖的类, 使用此注解 注入 "假的实例"
UserDAO userDAO; //依赖的"逻辑实现" 是靠mock, 所以这里用 "接口" 即可
}
"返回" 类mock 测试
这种测试是比较常规的, 就是调用一个依赖的接口, 根据接口返回的东西进行逻辑处理, 那么此时只需要mock掉依赖的接口返回即可, 这种比较好理解, 示例如下:
@Test
public void findById(){
// userService.findById() 调用了 userDAO.findById()
// 所以进行一个 mock "录制"
// 根据不同参数, 指定 userDAO.findById() 有不同返回, 以覆盖 userService.findById() 里的 一个分支逻辑
new Expectations(){{ //StrictExpectations 会对顺序 和 录制 进行严格检查
userDAO.findById(1L);
result = new UserDO(){{
setName("n1");
}};
userDAO.findById(anyLong); // 任何未被指定的参数, 都走 "any" 录制
result = new UserDO(){{
setName("BLACK_MAGIC");
}};
}};
UserDO userDO = userService.findById(1L);
assertThat(userDO).isNotNull();
assertThat(userDO.getName()).isEqualTo("n1");
userDO = userService.findById(2L);
assertThat(userDO).isNull(); //覆盖逻辑分支, name 为 BLACK 开头的 不返回
userDO = userService.findById(200L); // 任何未被指定的参数, 都走 "any" 录制
assertThat(userDO).isNull();
}
"更新类" 测试
按照非mock的测试思路, 这种测试场景, 一般我们会insert 或者 update 一个假数据, 然后再查出来看看结果是否是跟新之后的值; 使用mock的话, 依赖的 UserDAO 并没有"执行" 任何代码; "数据更新"并没有真正执行, 查询的结果也是我们指定的, "更新"逻辑错误, 只要我们mock的返回结果正确, 这个case 也会通过, 那这样的测试是不是没有意义?
首先要明确一个原则: "信任依赖目标, 依赖目标的逻辑由目标的单测类去保证正确"; 那么, 调用一个依赖接口 "只要参数正确, 结果我就认为正确"
所以, "更新类" 的逻辑, 我只需要验证传入的参数是否正确即可
回看UserService 的 createUser(), 属于"UserService" 的逻辑只有2个: 封装参数, 并调用 UserDAO 接口; 操作数据库, 并不是 "UserService" 负责的, 所以, "UserServiceImplTest" 这个单测并不需要测 操作数据库的逻辑
@Test
public void createUser() {
new Expectations(){{
userDAO.insert((UserDO)any);
result = 1;
}};
userService.createUser("pangliang");
//校验 传入的参数是否 正确
new VerificationsInOrder() {{
userDAO.insert(withArgThat(
new ArgumentMatcher<UserDO>() {
/**
* 参数校验器, jmockit 会将 userService.createUser() 中
* 调用 userDAO.insert 的该位置的参数传给校验器
* @param o
* @return 返回true 则表示 参数 满足期望
*/
@Override
public boolean matches(Object o) {
UserDO userDO = (UserDO)o;
assertThat(userDO).isNotNull();
assertThat(userDO.getName()).isEqualTo("pangliang");
return true;
}
}));
}};
}
@Test
public void createUserException() {
new Expectations(){{
userDAO.insert((UserDO)any);
result = -1;
}};
assertThatThrownBy(() -> {
userService.createUser(“pangliang");
}).extracting("detailMessage").contains("some error");
}
假如我们把 UserService 的 逻辑改一下, 故意 改错
@Override
public Long createUser(String name) {
UserDO newUser = new UserDO();
newUser.setName(null); // <---------------
Long count = userDAO.insert(newUser);
if(count != 1){
throw new RuntimeException("some error");
}
return count;
}
org.junit.ComparisonFailure:
Expected :"pangliang"
Actual :null
<Click to see difference>
at com.zhaoxiaodan.bootstrap.hsf.UserServiceImplTest$3$1.matches(UserServiceImplTest.java:85)
at com.zhaoxiaodan.bootstrap.hsf.UserServiceImplTest$3.<init>(UserServiceImplTest.java:73)
at com.zhaoxiaodan.bootstrap.hsf.UserServiceImplTest.createUser(UserServiceImplTest.java:72)
另外, 实际上在 Expectations 录制块 中也可以做参数校验, 所以代码可以省略为:
@Test
public void createUser() {
new Expectations(){{
userDAO.insert(withArgThat(
new ArgumentMatcher<UserDO>() {
/**
* 参数校验器, jmockit 会将 userService.createUser() 中
* 调用 userDAO.insert 的该位置的参数传给校验器
* @param o
* @return 返回true 则表示 参数 满足期望
*/
@Override
public boolean matches(Object o) {
UserDO userDO = (UserDO)o;
assertThat(userDO).isNotNull();
assertThat(userDO.getName()).isEqualTo("pangliang1");
return true;
}
}));
result = 1;
}};
userService.createUser("pangliang");
}
一个Test 测 两个实现类
比如现在把 Service 和 DAO 中间加一个 Inner 层, 调用路径: Service -> Inner -> DAO, 正常思路, 创建如下两个 Test:
-
测试Inner, mock掉DAO
-
测试Service , mock掉Inner
@RunWith(JMockit.class)
public class InnerUserImplTest {
@Tested
InnerUserImpl innerUser;
@Injectable
UserDAO userDAO;
}
@RunWith(JMockit.class)
public class UserServiceImplTest {
@Tested
UserServiceImpl userService;
@Injectable
InnerUser innerUser;
}
但是我们可能会发现, 两个Test 需要 录制 "同样的各种case", 因为 中间层和 上层 可能都有同样的比如"参数检查"逻辑, 那么其实我们可以在一个Test 里去 测两个 实现类
public class UserServiceImplTest {
@Tested
InnerUserImpl innerUser; //需要先声明, 会自动注入到 userService
@Tested
UserServiceImpl userService;
@Injectable
UserDAO userDAO;
//因为 Service -> Inner -> DAO, 所以只需要 mock DAO
//创造不同的返回 来覆盖 Service 和 Inner 层 的分支即可
new Expectations() {{
userDAO.findById(1L);
result = new UserDO() {{
setName("n1");
}};
userDAO.findById(anyLong);
result = new UserDO() {{
setName("BLACK_MAGIC");
}};
userDAO.findById(anyLong);
result = new UserDO() {{
setName("RED_MAGIC"); //inner 把RED开头的也屏蔽了
}};
}};
}
使用spring启动, 并只载入指定的部分class 来测试
有些时候确实是必须使用spring boot 来起应用, 比如有一些 aop切面的代码需要测试; 原来我们都是
extends BaseTest 去把整个应用都跑起来, 这样比较慢;那么可能会想要只载入一些特定的bean, 就比如
@ContextConfiguration(locations = "classpath:spring/特定的xml.xml"), 但是这样每个test都搞一个xml也是不太好的实际上
ContextConfiguration 是可以指定去load 哪些 class的, 这样spring起起来, 手动的去 load 这个测试需要用到的 class , spring会自动去 初始化这些bean就比如下面这个测试,
RegionMethodAspect.class 是测试目标 PopProxyClient 是 它的依赖(mock掉)@RunWith(SpringJUnit4ClassRunner.class)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ContextConfiguration(
classes = {TestService.class, TestRouter.class, RegionMethodAspect.class}
)
public class RegionMethodAspectTest {
@Autowired
public TestService service;
@Autowired
public TestRouter testRouter;
private String userId = "userId";
@MockBean
PopProxyClient popProxyClient;
@Test
public void test1() throws Exception {
List<Param> args = Arrays.asList(new Param(String.class, userId));
PopProxyServiceResponse response = new PopProxyServiceResponse();
response.setSuccess(true);
response.setData("haha");
new Expectations(popProxyClient){
{
popProxyClient.getAcsResponse((PopProxyServiceRequest)any, anyString);
result = response;
}
};
String rs = service.forward(userId);
assertThat(rs).isEqualTo("haha");
}
@Component
public static class TestService{
@RegionMethod(type = Type.FORWARD, popMethod = "popMethod", paramName = "userId", router = TestRouter.class)
public String forward(String userId){
return "hello";
}
}
@Component
public static class TestRouter extends BaseRouter<String>{
@Override
public String router(String paramValue) {
return Region.CHINA.getRegionName();
}
}
}

浙公网安备 33010602011771号