美文网首页
【测试相关】Java Mock框架之Mockito

【测试相关】Java Mock框架之Mockito

作者: 伊丽莎白2015 | 来源:发表于2022-10-04 20:51 被阅读0次

【资源】
官网:https://site.mockito.org/

【本文内容】
介绍了Mockito的重要注解@Mock,并介绍了Mockito的一些重要方法,如:

并且介绍了与JUnit5的集成:@ExtendWith(MockitoExtension.class)
以及与Spring Boot的集成,使用注解@MockBean
写了一个用Mockito mock的用来测试自定义Filter的单元测试。

最后,则介绍了BDD(行为驱动测试)风格的类BDDMockito


1. 为什么是Mockito

关于Java的Mock框架,市面上也有很多流行的框架,最流行的当属Mockito,而它还天然的支持Spring Boot,在Spring Boot的官方文档中有提到:

image.png

列举了Mockito与其它框架在Hithub上的star数:

EasyMock与Mockito的对比文章:https://code.google.com/archive/p/mockito/wikis/MockitoVSEasyMock.wiki
Mockito官网的与EasyMock对比文章:https://github.com/mockito/mockito/wiki/Mockito-vs-EasyMock

而Powermock的侧重点并不是独立的mock框架,而是作为EasyMock、Mockito或是其它Mock框架的扩展,比如支持static, final, private的测试。

以下是EasyMock的Contributors:

image.png

以下是Mockito的Contributors:

image.png

总结就是Mockito的社区相对于其它Mock框架还是比较活跃的。

2. Mockito与JUnit5的集成

参考:https://www.baeldung.com/mockito-junit-5-extension

除了Mockito本身的jar以及JUnit5本身的jar外,还需要依赖:mockito-junit-jupiter,简单起见,可直接引入Spring Boot的test,它已经帮我们解决了依赖问题,并且天然的支持Mockito和JUnit5(需要Spring Boot 2.4.0+,否则支持的是JUnit4):

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

我们先创建一个UserService接口,以及它的实现UserServiceImpl,并且有一个方法,version(),返回1.0:

@AllArgsConstructor
public class UserServiceImpl implements UserService {
    UserRepository userRepository;

    public int count() {
        return userRepository.count();
    }
}

使用Mockito mock UserService中的userRepository:

  • 首先要声明扩展器类:MockitoExtension.class,正是位于上述的mockito-junit-jupiter包中。
  • 其次,@Mock注解位于mockito-core包中,是Mockito重要的注解之一,被它声明的类,则会自动创建Mock实例。可以通过when()/given()来模拟它的行为。
  • 下述的测试方法中使用了when(userRepository的version方法被调用),则thenReturn(返回预期值)来模拟userRepository的行为。
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    @Mock
    private UserRepository userRepository;

    private UserService userService;

    @BeforeEach
    public void before() {
        userService = new UserServiceImpl(userRepository);
    }

    @Test
    public void countTest() {
        Mockito.when(userRepository.count()).thenReturn(5);
        Assertions.assertEquals(5, userService.count());
    }
}
使用@Mock注解生成的userRepository: image.png

3. Mockito的其它功能

3.1 使用Mockito.lenient()来避免因为Mock方法没有被调用而抛出的UnnecessaryStubbingException

参考:https://www.baeldung.com/mockito-unnecessary-stubbing-exception

比如userRepository有两个方法:version()和count(),我们在测试userService.count()的时候,顺道也Mock了userRepository的version()方法:

    @Test
    public void countTest() {
        Mockito.when(userRepository.count()).thenReturn(5);
        Mockito.when(userRepository.version()).thenReturn(5);
        Assertions.assertEquals(5, userService.count());
    }
运行后发现报错了: image.png

即如果Mock的方法没有被调用,那么Mockito会主动抛出错误:UnnecessaryStubbingException,这时候可以用Mock.lenient(),来表明可以忽略Mock方法没有被调用:

Mockito.lenient().when(userRepository.version()).thenReturn(5);
3.2 使用Mockito.doNothing()来Mock void 方法

参考:https://www.baeldung.com/mockito-void-methods

    @Test
    public void updateTest() {
        Mockito.doNothing().when(userRepository).update(Mockito.any());
        Assertions.assertTrue(userService.update(new UserEntity(1, "test")));
    }
3.3 Mock抛错

UserRepository两个方法,一个是有返回值的,一个没有:

    void validate(Integer userId);
    boolean check(Integer userId);

UserService两个方法:

    public boolean validate(Integer userId) {
        userRepository.validate(userId);
        return true;
    }

    public boolean check(Integer userId) {
        return userRepository.check(userId);
    }

写测试方法:

  • 使用Mockito.doThrow来Mock void方法的抛错。
  • 使用thenThrow来Mock有返回值方法的抛错。
    @Test
    public void validateTest() {
        Mockito.doThrow(new IllegalArgumentException()).doNothing().when(userRepository).validate(Mockito.any());
        Assertions.assertThrows(IllegalArgumentException.class, () -> userService.validate(1));
    }

    @Test
    public void checkTest() {
        Mockito.when(userRepository.check(Mockito.any())).thenThrow(IllegalArgumentException.class);
        Assertions.assertThrows(IllegalArgumentException.class, () -> userService.check(1));
    }
3.4 使用Mockito.doAnswer() / thenAnswer(Answer)来Mock更为复杂的result

参考:

thenAnswer通过接收参数,实现一个通用的接口,而这个通用接口基本上可以返回任何想要的东西。相比于常用的thenReturn、thenThrow方法(只返回固定的值),Answer可以计算返回值。

我们在UserRepository中定义一个方法,这个方法的意思是接收两个user,然后将他们的id相加返回:

int addId(UserEntity user1, UserEntity user2);

测试,可以看到通过thenAnswer可以拦截入参,然后进行具体的计算,从来达到更为复杂的Mock:

@Test
    public void addIdTest() {
        Mockito.when(userRepository.addId(Mockito.any(), Mockito.any())).thenAnswer(invocation -> {
            UserEntity user1 = invocation.getArgument(0);
            UserEntity user2 = invocation.getArgument(1);
            return user1.getId() + user2.getId();
        });

        Assertions.assertEquals(5, userService.addId(new UserEntity(2), new UserEntity(3)));
    }

对于void的方法,不能用thenAnswer来Mock,但可以使用Mockito.doAnswer(),如我们可以Mock Filter中的filterChain.doFilter方法:

Mockito.doAnswer((Answer) invocation -> {
                Object arg0 = invocation.getArgument(0);
                Object arg1 = invocation.getArgument(1);

                System.out.println(MDC.get("test"));
                // to more logic

                return null;
            }).when(filterChain).doFilter(Mockito.any(), Mockito.any());

4. Mockito与Spring Boot的集成

Spring Boot官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing.spring-boot-applications.mocking-beans

因为要和Spring Boot集成,那么首先项目是Spring Boot项目:

  • 创建Spring Boot启动类,有@SpringBootApplication注解。
  • 创建BookServiceImpl,有@Service注解。
  • BookServiceImpl中有bookRepository,有@Autowired注解。

我们对BookServiceImpl进行测试,创建BookServiceTest
这里使用的@MockBean创建的bookRepository可以将这个成员变量以mock的方式成为Spring ApplicationContext中的bean。

@SpringBootTest
public class BookServiceTest {
    @Autowired
    private BookService bookService;

    @MockBean
    BookRepository bookRepository;

    @Test
    public void countTest() {
        Mockito.when(bookRepository.count()).thenReturn(5);
        Assertions.assertEquals(5, bookService.count());
    }
}
可以看到通过@MockBean,可以直接将Mock的bookRepository注入到BookService中: image.png

5. 实例,通过Mockito.doAnswer()测试自定义的Filter

定义一个UserFilter,在这个filter中,如果request header中有userId,那么就用它,没有就自己定义一个叫testUser。最终放入request attribute中:

public class UserFilter implements Filter {
    public static final String USER_ID = "userId";

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String userId = request.getHeader(USER_ID);
        if (!StringUtils.hasText(userId)) {
            userId = "testUser";
        }
        request.setAttribute(USER_ID, userId);

        filterChain.doFilter(servletRequest, servletResponse);
    }
}

以下是单元测试,除了要用到spring-test中的MockHttpServletRequest以及MockHttpServletResponse,那么要怎么验证呢?我们可以用Mockito.doAnswer来验证filterChaindoFilter方法:

@ExtendWith(MockitoExtension.class)
public class UserFilterTest {
    @Mock
    FilterChain filterChain;

    @Test
    public void userId_should_get_from_parameter() throws Exception {
        UserFilter filter = new UserFilter();

        MockHttpServletRequest req = new MockHttpServletRequest();
        req.addHeader(UserFilter.USER_ID, "user1");
        MockHttpServletResponse res = new MockHttpServletResponse();

        Mockito.doAnswer(invocation -> {
            HttpServletRequest request = invocation.getArgument(0);
            Assertions.assertEquals("user1", request.getAttribute(UserFilter.USER_ID));
            return null;
        }).when(filterChain).doFilter(Mockito.any(), Mockito.any());

        filter.init(new MockFilterConfig());
        filter.doFilter(req, res, filterChain);
    }
}

6. BDDMockito

参考:https://www.baeldung.com/bdd-mockito
BDDMockito官网文档:https://javadoc.io/static/org.mockito/mockito-core/4.8.0/org/mockito/BDDMockito.html

BDD的意思是Behavior-driven development,即行为驱动开发(BDD中比较有名的自动化测试工具,Cucumber,具体可以查看文章: 【测试相关】Cucumber与Junit4集成示例,以及与Spring Boot+Junit5集成示例

对于Mockito来说,它提供的BDDMockito类则是实现的BDD风格。
传统的Mockito是when(...)/thenReturn(...),而BDDMockito则是使用given(...)/willReturn(...)这样的方法来Mock对象。

具体来说,Mockito的Mock如:

Mockito.when(userRepository.count()).thenReturn(5);

BDDMockito来Mock的话,则会是:

BDDMockito.given(userRepository.count()).willReturn(5);

又比如对于void的方法进行throw exception的Mock,用Mockito来写的话会是:

Mockito.doThrow(new IllegalArgumentException()).doNothing().when(userRepository).validate(Mockito.any());

换成BDDMockito则是:

BDDMockito.willThrow(new IllegalArgumentException()).willDoNothing().given(userRepository).validate(BDDMockito.any());


参考:

相关文章

网友评论

      本文标题:【测试相关】Java Mock框架之Mockito

      本文链接:https://www.haomeiwen.com/subject/lmysartx.html