Junit 5 - 理解Mockito,提高UT 覆盖率

在这里插入图片描述

前言

当我是1个3年初级程序员时, 我被面试者问到1个问题: 如何保证你的开发任务交付质量
当我是1个7年开发组长时, 我被面试者问到另1个问题:如何保证你的团队的代码质量, 减少rework。

又若干年后, 我才明白当年我的回答是多么的傻嗨, 什么理解业务, 勤沟通, 代码review流程都是废话。
真正的核心是测试! 足够的单元测试!

当你的下属提交pr时,顺便把 一份coverage 达到80% 的Junit testing report 放上jira, 你还不信任他这次任务的代码质量吗?

但是提高Junit 的覆盖率并不简单, 很多当前项目上的依赖, 这时, 我们就是需要Mockito了!



什么是Mockito

Mockito 是一个流行的 Java 单元测试框架,用于模拟和验证对象的行为。它提供了一组简单易用的 API,使得在编写单元测试时,可以轻松地创建和控制模拟对象,以便在测试中隔离依赖项和验证对象的行为。

Mockito 的主要特点包括:

简单易用的 API:Mockito 提供了一组简单易用的 API,使得在编写单元测试时,可以轻松地创建和控制模拟对象。
模拟任意对象的行为:Mockito 可以模拟任意对象的行为,包括静态方法、私有方法和 final 方法等。
验证对象的行为:Mockito 提供了一组 API 来验证对象的行为,例如验证方法是否被调用、验证方法的参数是否正确等。
支持多种测试框架:Mockito 可以与多种测试框架一起使用,如 JUnit、TestNG 等。
强大的功能:Mockito 提供了一些强大的功能,如模拟方法的返回值、模拟方法的异常抛出、模拟方法的调用次数等。
Mockito 广泛应用于 Java 开发中,被广泛认为是 Java 单元测试中最流行和最强大的框架之一。它提供了一种简单而强大的方式来编写单元测试,并且在测试中隔离依赖项和验证对象的行为。



为什么需要Mockito

简单来讲 Mockito 可以模拟1个类的对象, 并强制指定or 改变这个对象的行为

例如, 类A的方法a() 返回 当前日期的String “20241001”
但是 我们可以mock 1个对象 aMock , 并指定aMock.a() 返回 “20000101”
这样, 当我们的测试调用了aMock.a() , 它真的会返回"20000101"

这不是脱裤子放屁吗?

实际上, 既然我们mock了A类, 我们想要测试的并不是A类的代码, 而是另个调用了A类的类的代码 (B 具有1个A对象的成员)

例如 类B 的b() 方法 调用了 aObj.a() (aObj 是 类A的对象)

当我们测试类B() 时, 可以mock掉A, 并随时改变a() 方法的行为, 并测试不同a()返回值下 , B的行为是否正确



一个具体场景

上面还是说的太绕了

举个例子
例如我有1个Service 类 OrderService:
OrderService.java

@Service
@Slf4j
public class OrderService {

    @Autowired
    private OrderDao orderDao;

    // this method is used to query order by id
    public Order queryById(Long id) {
        // if optional.isPresent() is false, it will throw NoSuchElementException
        return orderDao.findById(id).orElseThrow(() -> new NoSuchElementException("No such order by id: " + id));
    }

    public Order updateOrder(Long orderId, Order orderDetails) {

        Order order = null;
        try{
            order = this.queryById(orderId);
        } catch (NoSuchElementException e) {
            log.error("Error in getting order by id...", e);
            throw e;
        } catch (QueryTimeoutException e) {
            log.error("timeout..", e);
            return null;
        }

        order.setCommodityName(orderDetails.getCommodityName());
        order.setPrice(orderDetails.getPrice());

        return orderDao.save(order);
    }

    public Order createOrder(Order order) {
        return orderDao.save(order);
    }

    public void deleteOrder(Long orderId) {
        orderDao.deleteById(orderId);
    }
}

其中该类调用了Dao类 OrderDao, 而OrderDao是直接与数据交互的

如果我们正常编写UT, 与数据真是交互, 编写1个for updateOrder()的测试方法

OrderServiceIntrusiveTest.java

@SpringBootTest
@ActiveProfiles("dev")
class OrderServiceIntrusiveTest {

    @Autowired
    private OrderService orderService;

    @Test
    void updateOrder() {

        Order order = Order.builder().commodityName("ASUS ROG ZEPHYRUS").price(3L).userId(3L).build();
        order = orderService.createOrder(order);

        Order orderUpdated = Order.builder().commodityName("ASUS ROG ZEPHYRUS V3").price(4L).build();
        orderUpdated = orderService.updateOrder(order.getId(), orderUpdated);
        assertEquals("ASUS ROG ZEPHYRUS V3", orderUpdated.getCommodityName());
        assertEquals(4L, orderUpdated.getPrice());
        
        orderService.deleteOrder(order.getId());
    }
}

这个case 能正常执行

但是有下面若干缺点:

  1. 依赖dev env的db 环境, 如果dev env 出问题, 测试就failed了
  2. 必须启动springboot 容器, 执行时间偏长, 特别是多个测试类的情况下
  3. 为了让测试可重复执行, 测试完之后必须清理数据
  4. 覆盖率低, 难以cover exception 处理场景
    在这里插入图片描述
    上红色条标记的代码都是未有cover的

至于怎么测试 Timeout 场景下业务代码? 难道还要测试中拔网线吗?
如果Exception 场景无法测试, 只测试了happyflow, 如何让你的技术经理放心的你的交付呢?

而Mockito 可以解决这些问题.



构建Mock对象

从这里开始我们拆解Mockito的各个方法使用要点

首先引入mockito 依赖

 <!-- mockito -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>5.7.0</version>
            <scope>test</scope>
        </dependency>



使用Mockito.mock()

public class BuildMockObjTest {

    @Test
    void testBuildMockObj() {
        StringUtil1 util = Mockito.mock(StringUtil1.class);
        String str = util.formatString("test");
        assertNull(str); // should be null
    }
}


class StringUtil1 {
    public String formatString(String str) {
        return str + ": " + LocalDate.now().toString();
    }
}

这例子中, 我们先写了1个 StringUtil1类, 它有1个方法可以为1个String 添加日期suffix

我们可以用Mockito.mock方法构建1个mock 的StringUtil 对象, 这个mock对象创建后里面的所有方法都是未知空位, 等待我们的指定
所以当我们直接调用 String str = util.formatString(“test”); 时, 其实并没有调用真正 formatString()里的代码, 返回的是null



使用@Mock 注解

下面写法与上面是等价的
在项目中使用注解更加常见
注意的事测试类本身需要 @ExtendWith(MockitoExtension.class) 注解

@ExtendWith(MockitoExtension.class)
public class BuildMockObjWithAnnotationTest {

    @Mock
    private StringUtil1 util;

    @Test
    void testBuildMockObj() {
        String str = util.formatString("test");
        assertNull(str); // should be null
    }
}



打桩 (Stub)



什么是打桩Stub与断言

先说下什么是打桩

Mockito 的核心 概念就是打桩和断言, 其中打桩是Mockito 特有的行为
简单来讲打桩就是指定 mock 对象的方法行为

举个例子

我令 dao 方法正常返回, 那么service 的update() 方法输出1
我令 dao 方法timeout, 那么service 的update()应该10秒后爆出异常

上面的令 某个mock方法 执行某种行为的本身 就是打桩, 后面的那么 就是断言, 如果update()的行为并不是断言描述的情况, 我们认为测试不通过, 需要开发人员检查业务代码。

断言就是所谓的Assert了, 这个与mockito没关, 任何UT 都应该包含至少一个断言



Stub 修改方法的返回逻辑



when().thenReturn()

thenReturn 可以直接修改1个对象方法的输出逻辑
例如:

@ExtendWith(MockitoExtension.class)
@Slf4j
public class StubTest {

    @Mock
    private StringUtil3 mockUtil;
    
    @Test
    void testStubReturn() {
        Mockito.when(mockUtil.formatString(Mockito.any(String.class))).thenReturn("test: " + LocalDateTime.now().toString());
        String str = mockUtil.formatString("test");
        log.info("str: {}", str);
        assertNotNull(str);
    }

}

class StringUtil3 {
    
    public String formatString(String str) {
        return str + ": " + LocalDate.now().toString();
    }
}

原本方法是 输出 参数 + 日期, 但是thenReturn 直接修改为 “test" + 日期时间, 注意这里的test 是hardcode的
输出:

10:13:31.827 [main] INFO com.home.javacommon.mockito.StubTest -- str: test: 2024-09-28T10:13:31.816733893



DoReturn().when()

这是另1风格写法

    @Test
    void testStubReturn2() {
        Mockito.doReturn("test: " + LocalDateTime.now().toString()).when(mockUtil).formatString(Mockito.any(String.class));
        String str = mockUtil.formatString("test");
        log.info("str: {}", str);
        assertNotNull(str);
    }

注意这里when() 后值, 而when()参数是1个mock对象, 而不是mork对象带方法



given().willReturn()

这里是BDD 写法, 我更prefer 这种

    void testStubReturn3() {
        given(mockUtil.formatString(Mockito.any(String.class))).willReturn("test: " + LocalDateTime.now().toString());
        String str = mockUtil.formatString("test");
        log.info("str: {}", str);
        assertNotNull(str);
    }

效果也是一样的



Stub 令对象方法抛出异常



when().thenThrow()

例子:

    @Test
    void testStubThrow() {
        Mockito.when(mockUtil.formatString(Mockito.any(String.class))).thenThrow(new UnsupportedOperationException());
        String str = null;
        assertThrows(UnsupportedOperationException.class, () ->  mockUtil.formatString("test"));
    }

值得注意是, Mock对象的方法定义如果没有throws 1个 CheckedException的话, 不能打桩让Mock对象抛出这个CheckException的, 这里的例子是让其抛出1个RuntimeException



doThrow().when()
    @Test
    void testStubThrow2() {
        Mockito.doThrow(new UnsupportedOperationException()).when(mockUtil).formatString(Mockito.any(String.class));
        String str = null;
        assertThrows(UnsupportedOperationException.class, () ->  mockUtil.formatString("test"));
    }



given().willThrow()
    @Test
    void testStubThrow3() {
        given(mockUtil.formatString(Mockito.any(String.class))).willThrow(new UnsupportedOperationException());
        String str = null;
        assertThrows(UnsupportedOperationException.class, () ->  mockUtil.formatString("test"));
    }



Stub 灵活地令对象方法执行指定行为

例如, 如果我想让方法返回其第1和第2个参数的合并字符串

而上面的Return 方式是无法获取参数的值的



when().thenAnswer()

例子:

@ExtendWith(MockitoExtension.class)
@Slf4j
public class StubAnswerTest {

    @Mock
    private StringUtil4 mockUtil;

    /*
     *  invocation is a concept of the mockito invocation, it includes the context information of the mock
     *  e.g. the argument of the method,  return values
     *
     *  usually, you can use the argument(0) to get the first argument
     *
     *  some methods
     *  getArgument(int index): get the argument at index
     *  getArguments(): get all arguments
     *  getMethod(): get the method
     *  getMock(): get the mock object
     */
    @Test
    void testStubAnswer() {
        Mockito.when(mockUtil.formatString(Mockito.any(String.class), Mockito.any(String.class)))
                .thenAnswer((invocation) -> invocation.getArgument(0) + ":" + invocation.getArgument(1));
        String str = mockUtil.formatString("test", "test2");
        assertEquals("test:test2", str);
    }
}

class StringUtil4 {

    public String formatString(String str1, String str2) {
        return "it's hard coded";
    }
}

值得注意的是 invocation的使用, 参考上面的注解



doAnswer().when()

另1个写法

   @Test
    void testStubAnswer2() {
        Mockito.doAnswer((invocation) -> invocation.getArgument(0) + ":" + invocation.getArgument(1))
                .when(mockUtil).formatString(Mockito.any(String.class), Mockito.any(String.class));
        String str = mockUtil.formatString("test", "test2");
        assertEquals("test:test2", str);
    }



given().willAnswer()

BDD 风格

    @Test
    void testStubAnswer3() {
        given(mockUtil.formatString(Mockito.any(String.class), Mockito.any(String.class)))
                .willAnswer((invocation) -> invocation.getArgument(0) + ":" + invocation.getArgument(1));
        String str = mockUtil.formatString("test", "test2");
        assertEquals("test:test2", str);
    }



Stub 调用真实的方法

有一种场景

1个Mock 对象里有若干个方法, 但是Mock出来后, 所有的方法的行为都是空的 , 但是Mockito 提供了CallRealMethod 功能, 让我们可以让其中某个(or 若干个) 方法使用真实定义的逻辑



when().thenCallRealMethod()

下面的例子, Mock对象里有两个方法
而我们只打桩了 第2个让其执行真实定义的代码

所以第一个方法输出是null
第2个方法正常输出

@ExtendWith(MockitoExtension.class)
public class StubCallRealMethodTest {

    @Mock
    private StringUtil5 mockUtil;


    @Test
    void testStubCallRealMethod() {
        Mockito.when(mockUtil.formatString(Mockito.any(String.class), Mockito.any(String.class))).thenCallRealMethod();
        String str = mockUtil.formatString("test");
        assertNull(str);

        str = mockUtil.formatString("test", "test2");
        assertEquals("test:test2", str);
    }
}


class StringUtil5 {

    public String formatString(String str) {
        return str + ": " + LocalDate.now().toString();
    }

    public String formatString(String str1, String str2) {
        return str1 + ":" + str2;
    }
}



DoCallRealMethod().when()

另一种写法

    @Test
    void testStubCallRealMethod2() {
        Mockito.doCallRealMethod().when(mockUtil).formatString(Mockito.any(String.class), Mockito.any(String.class));
        String str = mockUtil.formatString("test");
        assertNull(str);
    }



given().willCallRealMethod()

BDD 风格

    @Test
    void testStubCallRealMethod3() {
        given(mockUtil.formatString(Mockito.any(String.class), Mockito.any(String.class))).willCallRealMethod();
        String str = mockUtil.formatString("test");
        assertNull(str);

        str = mockUtil.formatString("test", "test2");
        assertEquals("test:test2", str);
    }



Stub 让无返回值 void 对象方法不抛出任何异常, DoNothing

如果只是让 方法不抛出异常 , 例如Mock 1个 Dao对象的 void sqlExec() 方法。
实际上就是让它 不做任何事情

而 when().then() 和 given().will() 都不适用于 mock1个无返回值方法的

这时我们只能用DoNothing



doNothing().when()

例子:

@ExtendWith(MockitoExtension.class)
@Slf4j
public class StubTest {

    @Mock
    private StringUtil3 mockUtil;

    @Test
    void testStubNotThrow() {
        Mockito.doNothing().when(mockUtil).formatString2(Mockito.any(String.class));
        assertDoesNotThrow(() -> mockUtil.formatString2("test"));
    }

}

class StringUtil3 {

    public void formatString2(String str) {
        throw new UnsupportedOperationException();
    }
}





Spy - Mock 1个"真实" 对象

有1个场景

1个类里定义了n个方法
我只想让定义某个方法的行为, 但是要让其他方法正常执行

当然我们可以Mock 这个对象后, 把其他的方法都打桩为CallRealMethod, 但是这个方法很蠢

更好的方法是Spy 1个对象



Mockito Spy 对象定义

什么是Spy?
在Mockito中,spy是一种特殊类型的mock对象,它可以部分模拟一个真实对象。
与普通的mock对象不同,spy对象保留了被模拟对象的真实行为,除非显式进行了模拟。
Spy对象的特点:
Spy对象会保留被模拟对象的原始行为,除非显式指定了模拟行为。
通过spy创建的对象是真实对象的一个代理,可以使用Mockito的方法来验证其行为。
可以通过spy对象来监视真实对象的方法调用,并可以选择性地进行模拟。
Spy的使用场景:
当您想要部分模拟一个真实对象,同时保留其原始行为时,可以使用spy对象。
适用于需要对对象的部分方法进行跟踪或验证的情况,而不需要完全模拟整个对象。



构建1个Spy 对象



方法一, 我们可以用Mockito.spy() 来创建

例如:

	SpyUtil = Mockito.spy(StringUtil.class)
方法二, 我们可以用@Spy 来定义, 同样需要@ExtendWith(MockitoExtension.class)

例如

@ExtendWith(MockitoExtension.class)
class xxx

@Spy
private StringUtil spyUtil;



spy的打桩

其实spy的打桩与mock对象的打桩无任何区别, 只不过mock对象的方法默认不打桩的话就是不执行任何代码
而spy 则相反, 不打桩的话会执行真实代码

例子:
下面的Spy对象有两个方法, 其中1个方法循环调用另1个方法

而这里只Stub了第2个方法, 让外部方法正常执行循环

@ExtendWith(MockitoExtension.class)
public class SpyTest {

    @Spy
    private StringUtil8 spyUtil;


    @Test
    void testRealMethod() {

        StringUtil8 util = new StringUtil8();
        List<String> list = Arrays.asList("test", "test2");
        util.formatString(list);
        //assert the list
        List<String> expected = Arrays.asList("test: 20240101", "test2: 20240101");
        assertEquals(expected,list);
    }

    @Test
    void testSpyMethod() {

        List<String> list = Arrays.asList("test", "test2");
        given(spyUtil.formatString(Mockito.any(String.class)))
                .willAnswer((invocation) -> invocation.getArgument(0) + ": " + "20241111");
        spyUtil.formatString(list);
        //assert the list
        List<String> expected = Arrays.asList("test: 20241111", "test2: 20241111");
        assertEquals(expected,list);
    }


}

@Slf4j
class StringUtil8 {
    
    public void formatString(List<String> list) {
        list.replaceAll(this::formatString);

        log.info("list: {}", list);
    }

    public String formatString(String str) {
        return str + ": " + "20240101";
    }
}



Mock 1个静态方法 – MockStatic

上面的例子都是Mock 1个对象内的方法。 而且Mock内的对象对真实其他对象的相同方法是无影响的, 它们分布在不同的heap 内存

问题来了,

如果1个静态方法可以被mock吗? 因为1个类的静态方法是共享的, 即使这个类创建了多个对象。

Mocktio.core 早期并不支持Mock 静态方法, 但是现在可以了

例子:

@ExtendWith(MockitoExtension.class)
public class MockitoStaticTest {

    @Test
    void testMockitoStatic() {

        try (MockedStatic<StringUtil9> mockStaticObj = Mockito.mockStatic(StringUtil9.class)) {
            given(StringUtil9.formatString(Mockito.any(String.class))).willReturn("test: " + "20241111");
            assertEquals("test: " + "20241111", StringUtil9.formatString("test"));
        } // try will auto execute the mockStaticObj.close()
        

        assertEquals("test: " + "20000101", StringUtil9.formatString("test"));

    }
}


class StringUtil9 {

    public static String formatString(String str) {
        return str + ": " + "20000101";
    }
}

注意, 强烈建议使用try with resource block 来使用 mockstatic , 否则你需要手动close mockobj, otherwise 程序不知道mock 什么时候结束



Inject 1个Mock 对象到另1个真实对象

大部分我们要面对的场景是:

我们要测试A类, A有1个B类成员 b, B类的方法对环境有依赖
所以我们要mock 1个B对象, 然后让B对象成为A测试对象的1个成员

方法有多种

方法一 通过构造函数inject

@Mock
private B b;

@Test
testA(){
	A a = new A(b)
	//Stub
	given(b.....)
	assert(A....)
}

但是很多时候B 是通过反射注入的(Spring)
A并没有1个有参的构造函数

方法二 同过反射注入Mock - ReflectionTestUtils

 @Mock
private B b;

@Test
testA(){
	A a = new A()
    ReflectionTestUtils.setField(a, "b", b); // “b" is the properties name in class A
	//Stub
	given(b.....)
	assert(A....)
}

方法三 同过@InjectMocks 注解

@Mock
private B b;

@InjectMocks
private A a

@Test
testA(){
	//Stub
	given(b.....)
	assert(A....)
}

项目更多地用这种写法

注意:

  1. InjectMocks 还是基于反射注入Mock 对象
  2. 会把所有用@Mock or@Spy 定义的mock 对象注入到@InjectMocks的对象中
  3. InjectMock 默认会使用类A 的无参函数来构建A对象a, 如果A没有 无参函数,有可能会有异常
  4. 可以使用 @InjectMocks(constructorArgs = {“dependency1”, “dependency2”}) 来指定使用某个有参函数

例子:

@RunWith(MockitoJUnitRunner.class)
public class MockitoInjectMocksTest {

    @Mock
    private Dependency1 dependency1;

    @Mock
    private Dependency2 dependency2;

    @InjectMocks(constructorArgs = {"dependency1", "dependency2"})
    private MyClass myClass;

    @Test
    public void testMyClass() {
        // ...
    }

    public static class MyClass {
        private final Dependency1 dependency1;
        private final Dependency2 dependency2;

        public MyClass(Dependency1 dependency1, Dependency2 dependency2) {
            this.dependency1 = dependency1;
            this.dependency2 = dependency2;
        }

        // ...
    }

    public interface Dependency1 {
        // ...
    }

    public interface Dependency2 {
        // ...
    }
}



用Mockito去解决本文开始提供的场景问题

很明显
上面的A类就是 场景下的Service 类, B类就是Dao 类, Dao类具有DB 的依赖!

重写后的test 方法:

@Slf4j
@ExtendWith(MockitoExtension.class)
class OrderServiceBddTest {

    @Mock
    private OrderDao orderDao;

    @InjectMocks
    private OrderService orderService;

    private Order orderToTest;

    private Order orderDetails;

    @BeforeEach
    void beforeEach() {
        orderToTest = Order.builder().commodityName("ASUS ROG ZEPHYRUS").price(3L).userId(3L).build();
        orderDetails= Order.builder().commodityName("ASUS ROG ZEPHYRUS V3").price(4L).build();
    }

    @Test
    void updateOrder() {
        log.info("updateOrder test start");


        // ==== happy flow ==========================================================================
        // force the orderDao to return the orderToTest
        given(orderDao.findById(Mockito.anyLong())).willReturn(Optional.of(orderToTest));

        // let orderDao.save() successfully return the orderToTest
        given(orderDao.save(any(Order.class))).willAnswer(invocation -> invocation.getArgument(0));


        Order orderUpdated = orderService.updateOrder(101L, orderDetails);
        Mockito.verify(orderDao).findById(101L);
        //Mockito.verify(orderService).queryById(101L); // not work
        assertEquals(orderDetails.getCommodityName(), orderUpdated.getCommodityName());
        assertEquals(orderDetails.getPrice(), orderUpdated.getPrice());


        // ==== exception case 1 , order not found ============================================================================

        // clean all stubs for an object
        Mockito.reset(orderDao);

        given(orderDao.findById(Mockito.anyLong())).willThrow(NoSuchElementException.class);

        assertThrows(NoSuchElementException.class, () -> orderService.updateOrder(101L, orderDetails));

        // ==== exception case 1 , order not found ============================================================================
        Mockito.reset(orderDao);
        given(orderDao.findById(Mockito.anyLong())).willThrow(QueryTimeoutException.class);

        assertDoesNotThrow(() -> orderService.updateOrder(101L, orderDetails));
        Mockito.verify(orderDao).findById(101L);
        assertNull(orderService.updateOrder(101L, orderDetails));
    }
}

可以见到, 使用mockito 我们很容易模拟db timeout 和 没有数据的问题, 真正地令覆盖率达到100%

Mockito 的verify

我认为verify 也是断言(Assert)的一种, 一种特殊的断言, 只能用于mock 对象上
verify 可以用于检查mock 对象的方法执行次数, 和执行时的参数等

个人并不常用

例子:
参考上面的例子

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nvd11

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值