3分钟入门mockito使用

最近周杰伦发新歌了. 我来蹭下上一首歌《Mojito》的热度.

歌名mojito,是一种透明无色鸡尾酒的名字。有个低侵入性的Java框架,也取自这酒的谐音: mockito.

3分钟入门mockito使用

mockito logo

mockito简介

mockito 是 Java 测试中常用的 mock 工具,用于对对象和对象上的方法进行 mock,指定针对各种不同参数时候的行为.

mockito 框架引入

引入了Spring-boot-starter-test项目呢, 都会包含有mockito, 不用额外引入.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

使用示例

框架 API 简洁,主要入口,都在Mockito.class上的静态方法里.

假设现有基本类两个:

1. KissService


@Service
public class KissService {
    @Autowired
    KissDao kissDao;

    public Integer sum(Integer a, int b) {
        return kissDao.add(a, b);
    }
}

2. KissDao


@Repository
public class KissDao {
    @Autowired
    JdbcTemplate jdbc;

    public Integer add(Integer a, int b) {
        return jdbc.queryForObject("SELECT ? + ?"new SingleColumnRowMapper<>(), a, b);
    }
}

借着这个场景,简单介绍下几种基本使用方式.

Mockito.mock

获取一个假对象

如果要测试service逻辑, 一般是要隔离开数据层的.这时候就不能用真实的 DAO,而是做一个假的 DAO 接入进去.

获取假KissDao的方法是Mockito.mock(Class), 会直接得到一个假对象.

@Test
void mock(){
var service = new KissService();
service.kissDao = Mockito.mock(KissDao.class);

Assertions.assertNotNull(service.kissDao);
}

实现方式,是生成了一个当前类的代理或子类的对象,具体会涉及到MockMaker的不同实现:

  1. ProxyMockMaker
  2. SubclassByteBuddyMockMaker
  3. ….

现在大概知道,这种方式,final 类支持不太好就行.如何处理final类,晚点再说.

设定方法返回值

前面的类,已经 mock 过了, 但是方法都是空的,没有具体实现代码. 如果直接调用, 不会报错,但会返回默认值.

@Test
void mockCall() {
    var kissDao = Mockito.mock(KissDao.class);
    System.out.println(kissDao.add(1, 2));
    //0
}

可以通过when()来设定 mock 对象调用方法时候的返回值.

//入参是固定值1,2时候返回3
when(kissDao.add(1,2)).thenReturn(3);
assertEquals(kissDao.add(1,2),3);

设定某种情况下的返回值

传递给mock方法的参数, 有个术语叫ArgumentMatcher.matcher相当于是一个充当filter的callback, 当参数传入时候,会通过matcher判断条件是否满足.

比如下面这句, 就是判断第一个数大于10,第二个数<3时候, 返回3.

when(kissDao.add(
     ArgumentMatchers.argThat(x->x>10),
     ArgumentMatchers.intThat(x->x<3)))
     .thenReturn(3);
assertEquals(kissDao.add(112), 3);

当然, 不用每次都这么复杂的去写.

ArgumentMatchers上有多个工厂方法, 可以返回各种各样的预设Matcher,不够用的话再用自定义的.

比如下面用的都是默认的.

//入参等于1,2时候返回3
when(kissDao.add(eq(1), eq(2))).thenReturn(3);
assertEquals(kissDao.add(12), 3);
//入参是任意整数时候都返回3
when(kissDao.add(anyInt(),anyInt())).thenReturn(3);
assertEquals(kissDao.add(4,5),3);
//入参是任意对象和整数时候都返回3
when(kissDao.add(any(),anyInt())).thenReturn(3);
assertEquals(kissDao.add(5,4),3);

这里有个细微差别

  • any是针对对象进行过滤,返回对象的默认值null.
  • anyInt是按照int过滤,返回的是int默认值:0.

除此以外, eq,refEq,same也都有支持.

动态响应

前面thenReturn返回的是固定值, 这里也可以放一个回调进去,不过要用thenAnswer接口.

下面写法是调用时候,返回随机数.

when(kissDao.add(any(),anyInt()))
   .thenAnswer(c->new Random().nextInt());
assertNotEquals(kissDao.add(5,4),kissDao.add(5,4));

根据参数动态响应

响应也可以针对参数做变化, 这时候可以在回调内部去获取参数.

when(kissDao.add(any(),anyInt()))
.thenAnswer(
    c-> c.getArgument(0, Integer.class
    + c.getArgument(1, Integer.class));
assertEquals(kissDao.add(1,5),6);

主要是使用getArgument方法, 剩下的就是回调的编写问题.

返回异常

前面都是返回内容,但有时需要抛出异常,要用thenThrows.

when(kissDao.add(any(),anyInt()))
  .thenThrow(NullPointerException.class);
assertThrows(NullPointerException.class,
    ()->kissDao.add(1,5));

几种特殊情况

学一个mock..when..return基本能解决 1 半以上测试场景.

不过除了基本的 mock,还有些特殊情况需要处理,下面说一下.

重置函数 reset

写了太多when以后,可能自己也搞不清楚当前的对象状态. 这时候可以用Mockito.reset恢复到出厂设置.

半真半假的Spy

mock 行为, 是生成假的子类.  when设置过的方法调用走自定义,其他默认所有方法都是调用假的.

这里的默认行为是可以改的,还有一个常用方法,叫Mockito.spy,用来生成半真半假的对象.

spy 对象,可以当成对已有对象的装饰, 正好是跟 mock 反过来. 所有方法都调用真实的, 除非设置了假的.

@Test
void spyWhen() {
    KissService spy = Mockito.spy(new KissService());
    Mockito.doReturn(4)
    .when(spy).sum(any(), any());
    assertDoesNotThrow(() -> spy.sum(12));
    assertEquals(4, spy.sum(12));
}

这就是给spy 做一个假方法的方式:Mockito.doReturn.

无返回值的方法怎么处理

有时候,我们会遇到很多没有返回值,全靠副作用的方法需要处理.

比如给前面的 DAO 加上下面这个方法:

public void findAllInto(String sql, List<Object> result){
    List<Map<String, Object>> maps = jdbc.queryForList(sql);
    result.addAll(maps);
}

3分钟入门mockito使用单纯的mock.. when.. return流派是不行的,处理不了,when 是需要参数的,这里会报错.

针对这种场景,最好像前面在spy部分用的Mockito.doReturn一样, 这里用Mockito.doAnswer来处理.

@Test
void voidReturn() {
    doAnswer(x -> {
        x.getArgument(1, List.class).add("k");
        return null;
    })
            .when(kissDao).findAllInto(any(), any());
    ArrayList<Objectresult = new ArrayList<>();
    kissDao.findAllInto("", result);
    System.out.println(result);
}

当然,do 系列还有很多别的,都是类似的.需要提到前面写的一般都用这个doXXX.

静态方法怎么 mock?

前面的方法,都是在对象上的, mock 或者 spy 以后用 when 就可以了. 那么静态方法怎么处理?

比如,还是给KissDao加方法:

public static int staticMethod(){
    return 1;
}

这问题确实很棘手, 老版本的 mockito 对静态方法支持确实不太行,一般会采用powermock的方案来解决. 不过 3.4 以后, mockito 支持了. 这里就不提powermock了,直接上mockito-inline.

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>3.8.0</version>
    <scope>test</scope>
</dependency>

有这个包在,inline maker就算是启用了, 测试可以借助try-with-resources语法写成这样.

@Test
void mockStaticMethod(){
    try(MockedStatic<KissDao> mock = mockStatic(KissDao.class)){
        mock.when(KissDao::staticMethod) .thenReturn(75);
        int n = KissDao.staticMethod();
        assertEquals(n,75);
    }
}

注意中间的过滤器写法,是个Method Reference.如果静态方法有参数话,这里可能要换成 lambda.

mock.when(()->KissDao.staticMethod()) .thenReturn(75);

有参数的怎么写,我就不展开了.

final怎么处理

最早部分说过, 用子类这种方式来做mock,解决不了final的问题.有了inline-marker以后, 可以了.  这里不再给例子.

自动注入怎么 mock

前面直接把KissDao设置到KissService里去,这是我个人习惯,为了测试,不太爱用private field来做注入.

如果做了完全私有的属性控制, mockito 提供了一套完善解决办法,自动注入.

class KissServiceTest {
    @Mock
    KissDao kissDao;
    @InjectMocks
    KissService kissService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }
}

这里是注解语法,不用自己手动调用mock,spy之类了.

  • @Mock 当前属性是 mock 的
  • @Spy 当前需要 spy
  • @InjectMocks 当前对象里需要把 mock 出来的内容给自动注入

这一套标记下来, 省了不少事.再调用openMocks执行注入就好.

@BeforeEach
void setUp() {
    MockitoAnnotations.openMocks(this);
}

如果不愿意使用手动打开, 也可以直接使用runner,会自动进行这部分操作.需要写成这样.

@ExtendWith(MockitoExtension.class)
class KissServiceTest {

    //...
}

有了这个注解以后,就不用自己手动打开了.  当然, 要是连这都懒得写, @MockitoSettings注解加上也可以.

除了这些注解, 还有一个叫做@MockBean的.这个主要用在Spring环境下,会把mock对象加入Context.怎么用就不提了.

verify & ArgumentCapture

还有一套验证用的verfify,和验证时候用来获取参数的ArgumentCapture. 我不太用他们,有需求的记得又这么个东西就行, 功能还是挺丰富的.

BDDMockito

前面提到的都是Mockito.class上面的接口. 下面提一下另一个入口类BDDMockito.

TDDTTest, BDDBBehavior. 这是一种测试风格, 更偏重业务角度.更倾向于用一种模式化的自然语言来描述规范. 讲究given.. when.. then.

mockito项目自身的单测, 就是用这种方式写的.喜欢这种API风格的, 可以用.

Junit 小技巧

前面的都说完了,回头来说点儿junit小技巧. 目前示例中出现的测试都是junit5的测试.

参数化测试

如果熟悉testng, 这个应该很熟悉了.junit4时代的支持有点烂, 5代看起来好了点.

大概如下图可以提供两个方法, 一个方法做测试,同名静态方法提供参数.

static List<Arguments> add() {
    return Arrays.asList(
            Arguments.of(1,2,3)
            ,Arguments.of(1,2,3)
            ,Arguments.of(1,2,3)
            ,Arguments.of(1,2,3)
    );
}

@ParameterizedTest
@MethodSource //不同名话可以手动指定
void add(int a, int b, int expect) {
    assertEquals(a + b,expect );
}

这里的参数源,除了MethodSource,也可以使用CSV等作为输入源.这里自定义能力很强,需要的话可以自己研究.

assert 快速补完技巧

测试中有大量 assert 要写, 是个体力活.

为了简化操作, 可以编写一些测试相关的IDEA自定义postfix completion模板,来加速这个流程.

3分钟入门mockito使用点击 ➕ 填写要自定义的命令,和展开后的内容.3分钟入门mockito使用

当然,敲完回车,光标位置也可以自定义.比如assertEquals,需要另一个参数.3分钟入门mockito使用

具体定义成什么样, 还是看个人习惯, 我目前定制了下面几个,凑合够用.

Assertions.assertTrue($EXPR$); //at
Assertions.assertEquals($EXPR$,$END$); //ae
Assertions.assertNotNull($EXPR$); //ann
3分钟入门mockito使用

后话

以后有时间或许可以解读下 mockito 源码和 API 设计. 今天是纯实用性的技巧, 就到这里吧.



3分钟入门mockito使用




原文始发于微信公众号(K字的研究):3分钟入门mockito使用

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/24746.html

(1)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!