Unit Test高阶:Mock的使用、测试Collection结果、参数化设置

A. 概念

Mock: 在进行单元测试时,往往测试的方法在被执行时会调用其它的方法,而为了保证测试的单元性和独立性,我们通常创建一个模拟方法来模拟这个在被测试方法中被调用的其它方法。而实现这个功能我们通常要用到@Mock及其相关的关键字。

参数化设置:我们可以使用参数化设置来用一个单元测试测试多个不同的参数。

B. Mock的使用

通常测试类的样式是这样的:

import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;

@ExtendWith(MockitoExtension.class)    // 开启Mockito功能
class classTest {

    @Mock // 需要被模拟的对象
    private MockObject mockObject1;

    @Mock // 需要被模拟的对象
    private MockObject mockObject2;

    @InjectMocks // 被测试的类和对象
    private testClass testObject;
}

I. @Mock

使用@Mock可以模拟一个对象,当被测试方法中此对象被使用时,直接进行模拟而不是走正常的方法。

II. @InjectMocks

使用@InjectMocks可以标注需要做单元测试的类和对象。

III. 方法模拟

import static org.mockito.Mockito.when;

when(mockObject.mockMethod()).thenReturn(someValue)

当我们需要模拟有返回值的方法时可以使用when().thenReturn(),这样被Mock的方法会直接放回设置好的值。

import static org.mockito.Mockito.doNothing;

doNothing().when(mockObject).someMethod();

当需要模拟一个void方法时且不需要返回值时,我们可以使用doNoting().when()。

import static org.mockito.Mockito.when;

when(mockObject.mockMethod())
.then(importParameter -> {
    ...
    // return someValue;
});

当我们需要模拟一个有复杂逻辑的方法时可以在then里使用Lambda函数来实现。左边是传入值,右边是方法体。

模拟静态方法

public class StaticUtils {

    private StaticUtils() {}

    public static List<Integer> range(int start, int end) {
        return IntStream.range(start, end)
          .boxed()
          .collect(Collectors.toList());
    }

    public static String name() {
        return "Baeldung";
    }
}
@Test
void givenStaticMethodWithNoArgs_whenMocked_thenReturnsMockSuccessfully() {
    assertThat(StaticUtils.name()).isEqualTo("Baeldung");

    try (MockedStatic<StaticUtils> utilities = Mockito.mockStatic(StaticUtils.class)) {
        utilities.when(StaticUtils::name).thenReturn("Eugen");
        assertThat(StaticUtils.name()).isEqualTo("Eugen");
    }

    assertThat(StaticUtils.name()).isEqualTo("Baeldung");
}

如果我们想模拟静态方法可以使用try函数,并在代码块中按照如上例子对静态方法进行模拟。如果需要模拟多个静态方法,可以用try函数嵌套来进行模拟。

IV. 验证

在模拟完整个方法后,我们需要对结果进行验证。通常我们使用``org.junit.jupiter.api.Assertions.*;中的方法就足够了,但是有时候我们也需要用到verify函数。

import static org.mockito.Mockito.verify;

verify(mockObject, times(num)).someMethod(); 
verify(mockObject, never(num)).someMethod();
verify(mockObject).someMethod(); // 等价于verify(mockObject, times(1)).someMethod(); 
verify(mock, atLeast(2)).someMethod("was called at least two times");

verify() ****函数可以计算指定对象的指定方法在整个模拟过程中被执行的次数。

V. 参数模拟

import static org.mockito.ArgumentMatchers.any;

any()
isA(Class)
notNull()
isNull()
anyInt()
anyList()
...

有时候我们Mock方法具体传入的值并不关心,这个时候我们可以使用any函数来模拟传入的值。

Note: When()里面的方法要么参数全是any(), 要么全不是,不然就会报错。

VI. 常用的模拟场景Template

1. 模拟Controller Template

import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
class SomeControllerTest {

    @Mock
    private SomeService someService;

    @InjectMocks
    private SomeController someController;

    @Test
    void testSomeMethod() {

        MockHttpServletRequest request = new MockHttpServletRequest();
        RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));

        ResponseEntity<?> responseEntity = someController.someMethod(someValue);

        assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
        verify(someService, times(1)).someMethod(someValue);

    }

2. ExceptionHandlerTest Template

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import static org.junit.jupiter.api.Assertions.assertEquals;

class APIExceptionHandlerTest {

        // Exceptionhandler 类
    private ExceptionHandler apiExceptionHandler;

    @BeforeEach
    void setUp() {
        apiExceptionHandler = new APIExceptionHandler();
    }

    @Test
    void handleAccessDeniedException() {
        AccessDeniedException exception = new AccessDeniedException("没有权限");

        ResponseEntity<String> responseEntity = apiExceptionHandler.handleAccessDeniedException(exception);

        assertEquals(HttpStatus.FORBIDDEN, responseEntity.getStatusCode());
        assertEquals("没有权限", responseEntity.getBody());
    }

}

C. 测试Collection结果

当返回结果是Collection的时候,我们通常可以定义一个方法,遍历这个Collection从而达到检验的效果。我们可以活用集合类的stream()方法来进行测试。

assertStudentName("David", studentList);
assertStudentName("Daniel", studentList);
assertStudentName("Anna", studentList);

public void assertStudentName(String name, List studentList) {
    assertTrue(studentList.stream().anyMatch(student -> Objects.equals(student.getName(), name);
}

D. 参数化设置

我们可以使用参数化设置来用一个单元测试测试多个不同的参数。

  • 最基本的,我们可以使用@ValueSource@EnumSource@CsvSource来传递多个参数。
  • 我们也可以使用@MethodSource来构建返回类型为Stream的静态工厂方法来生产参数并进行传递。如果每次想传递多个参数进行测试可以使用Stream<Arguments>在每一个Arguments中储存多个参数并传递。

详见JUNIT 5官方文档:https://doczhcn.gitbook.io/junit5/index/index-2/parameterized-tests

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容