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:
  1. 测试Inner, mock掉DAO
  2. 测试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();
        }
    }
}

 

posted @ 2018-12-28 17:26  比利乘二  阅读(318)  评论(0)    收藏  举报