SpringBoot 单元测试

博客强调单元测试的重要性,介绍了Spring Boot中使用mock形式进行测试的方法。提及了SpringBoot Test集成测试的局限性,重点阐述了开始使用Mockito的3种方式,还介绍了@Mock、@Spy、@Captor、@InjectMocks等注解的使用及注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在看《代码整洁之道》《重构》这两本书

里面都着重的提到过,单元测试

说来惭愧以前的测试也是应付上级检查,所以对这块的认知并不足够

但要成为一名合格的开发者,还是应该把这一块捡起来

@Test

是最简单的了,没啥好说的

快速生成测试类 https://blog.youkuaiyun.com/jj_nan/article/details/64134781

基于RestTemplate测试http接口

SpringBoot Test集成测试_springboot 集成test-优快云博客

类似的HTTP客户端有Unirest、feign等

最简单的是加俩注解

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = 启动类Application.class)

但确定很明显,需要启动整个项目,限制太多了

这个时候就需要使用mock的形式

JMockit中文网 http://jmockit.cn/index.htm(教程简单易懂,非常推荐)

==========================================================

Mockit  Baeldung中文网: Java教程, Spring Boot教程, Spring Cloud教程

由于网站有时候打不开, 所以记录一下使用过程

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

springBoot-test的依赖中, 包含了mockit

开始使用mockito的3种方式

1.类上加@ExtendWith(MockitoExtension.class)

2.手动初始化  MockitoAnnotations.openMocks(this);

3.使用注解@Rule 修饰成员变量  public MockitoRule initRule = MockitoJUnit.rule();

手动mock 或者 使用@Mock注解

// 手动
@Test
public void whenNotUseMockAnnotation_thenCorrect() {
    List mockList = Mockito.mock(ArrayList.class);

    mockList.add("one");
    Mockito.verify(mockList).add("one");
    assertEquals(0, mockList.size()); // size方法没有设置mock值, 前面add无效, 结果任然为0

    Mockito.when(mockList.size()).thenReturn(100);
    assertEquals(100, mockList.size());// mock了之后 就能返回指定的值
}

// 注解
@Mock
List<String> mockedList;

@Test
public void whenUseMockAnnotation_thenMockIsInjected() {
    mockedList.add("one");
    Mockito.verify(mockedList).add("one");
    assertEquals(0, mockedList.size());

    Mockito.when(mockedList.size()).thenReturn(100);
    assertEquals(100, mockedList.size());
}

@Spy注解

spy和mock的区别, spy只影响指定的方法, mock是影响全部的方法

// 手动
@Test
public void whenNotUseMockAnnotation_thenCorrect() {
    List mockList = Mockito.spy(ArrayList.class);

    mockList.add("one");
    Mockito.verify(mockList).add("one");
    assertEquals(1, mockList.size()); // size方法可以生效, 值为1

    Mockito.when(mockList.size()).thenReturn(100); // 只影响了size的值
    assertEquals(100, mockList.size());
}

// 注解 略

@Captor 

用于捕获方法的参数, 避免了手动写各种when来传递参数

// 手动
@Test
public void whenUseCaptorAnnotation_thenTheSame() {
    List mockList = Mockito.mock(List.class);
    ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class);

    mockList.add("one");
    Mockito.verify(mockList).add(arg.capture());

    assertEquals("one", arg.getValue());
}

// 使用注解
@Mock
List mockedList;

@Captor 
ArgumentCaptor argCaptor;

@Test
public void whenUseCaptorAnnotation_thenTheSam() {
    mockedList.add("one");
    Mockito.verify(mockedList).add(argCaptor.capture());

    assertEquals("one", argCaptor.getValue());
}

@InjectMocks

当serviceA 的方法, 依赖 serviceB时, 

 @InjectMocks能够把 用@Mock修饰的 serviceB, 自动注入到ServiceA中

注意: 被@Spy 修饰的对象, 无法自动注入, 只能手动通过构造方法传递

=========================

手动抛出异常

class MyDictionary {
    
    private Map<String, String> wordMap;

    public void add(String word, String meaning) {
        wordMap.put(word, meaning);
    }

    public String getMeaning(String word) {
        return wordMap.get(word);
    }
}


// 有返回值时
@Test
void givenNonVoidReturnType_whenUsingWhenThen_thenExceptionIsThrown() {
    MyDictionary dictMock = mock(MyDictionary.class);
    when(dictMock.getMeaning(anyString())).thenThrow(NullPointerException.class);
    
    assertThrows(NullPointerException.class, () -> dictMock.getMeaning("word"));
}


// 无返回值时
@Test
void givenVoidReturnType_whenUsingDoThrow_thenExceptionIsThrown() {
    MyDictionary dictMock = mock(MyDictionary.class);
    doThrow(IllegalStateException.class).when(dictMock)
        .add(anyString(), anyString());
    
    assertThrows(IllegalStateException.class, () -> dictMock.add("word", "meaning"));
}

以及 手动创建异常对象

// 有返回值时
@Test
void givenNonVoidReturnType_whenUsingWhenThenAndExeceptionAsNewObject_thenExceptionIsThrown() {
    MyDictionary dictMock = mock(MyDictionary.class);
    when(dictMock.getMeaning(anyString())).thenThrow(new NullPointerException("Error occurred"));
    
    assertThrows(NullPointerException.class, () -> dictMock.getMeaning("word"));
}

// 无返回值时
@Test
void givenNonVoidReturnType_whenUsingDoThrowAndExeceptionAsNewObject_thenExceptionIsThrown() {
    MyDictionary dictMock = mock(MyDictionary.class);
    doThrow(new IllegalStateException("Error occurred")).when(dictMock)
        .add(anyString(), anyString());
    
    assertThrows(IllegalStateException.class, () -> dictMock.add("word", "meaning"));
}

======================================================================
======================================================================
======================================================================

项目中的使用记录

由于是使用了junit5, 所以不用写@RunWith

/**
 * 不启动项目, 验证接口逻辑
 */
@ExtendWith(MockitoExtension.class)
public class UserServiceImplTest {

    @InjectMocks
    private UserServiceImpl service;

    @Mock
    private userMapper userMapper;

    @Test
    public void testListByIds() throws Throwable {
        // mock结果
        UserEntity obj = new UserEntity();
        obj.setUnitId("test-unit-id");
        List<UserEntity> mockResultList = new ArrayList<>();
        mockResultList.add(obj);

        // 设定mock
        when(userMapper.selectList(ArgumentMatchers.any())).thenReturn(mockResultList);

        // 调用public方法
        // List<UserEntity> result = service.listByIds(ids);

        // 反射调用private方法
        Method privateMethod = UserServiceImpl.class.getDeclaredMethod("listByIds", List.class);
        privateMethod.setAccessible(true);
        final List<Long> ids = Arrays.stream(new Long[]{1L, 2L}).collect(Collectors.toList());
        final List<UserEntity> result = (List<UserEntity>) privateMethod.invoke(service, ids);

        // 断言
        assertEquals(mockResultList.get(0).getUid(), result.get(0).getUid(), "verify failure, id不匹配");
    }
}

/**
 * 启动项目, 调用真实数据库
 */
@ActiveProfiles("dev")
@ExtendWith(MockitoExtension.class)
@SpringBootTest
@Slf4j
public class UserServiceImplTest {

    @Autowired
    private UserServiceImpl service;

    @Test
    public void testMaxSeq() {
        MaxSeqBO result = service.getMaxSeq("area-code", "group-code");
        log.info("Database result: {}", JSONUtil.toJsonPrettyStr(result));

        // 断言
        assertEquals(30, result.getMaxSeq(), "verify failure");
    }
}

/**
 * 启动springBoot, 调用mock数据
 *
 * @Test注解是基于junit5(jupiter包路)
 * 参考
 * https://www.baeldung-cn.com/injecting-mocks-in-spring
 */
@ActiveProfiles("dev")
@ExtendWith(MockitoExtension.class)
@SpringBootTest
@Slf4j
public class UserServiceImplTest {

    @Autowired
    private UserServiceImpl service;

	/**
	 * 此处不能用 @Autowired, 要用@MockBean
	 */
    @MockBean
    private UserMapper userMapper;


	/**
	 * 需要注册一个mock的bean
	 */
    @Bean
    @Primary
    public UserMapper nameService() {
        return Mockito.mock(UserMapper.class);
    }

    @Test
    public void testMaxSeq() {
        // mock返回结果
        MaxSeqBO  mockBO = new MaxSeqBO();
        mockBO.setMoveSeq(10);

        // 设定mock
        Mockito.when(UserMapper.getMaxSeq(ArgumentMatchers.any(), ArgumentMatchers.any())).thenReturn(mockBO);

        // 调用
        MaxSeqBO  result = service.getMaxSeq("area-code", "group-code");
        log.info("Database result: {}", JSONUtil.toJsonPrettyStr(result));

        // 断言
        assertEquals(10, result.getMoveSeq(), "verify failure");
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值