本文的宗旨在于通过简单干净实践的方式教会读者,如何使用 Mock 进行工程的单元测试,以便于验证系统中的独立模块功能的健壮性。
从整个工程所处不同阶段的测试手段包括;单元测试、集成测试、系统测试、验收测试、性能测试、安全测试、回归测试,以及兼容、可靠、可用性测试。
而单元测试的重点在于,对工程开发中的代码,进行流程中的单元化测试。如一整个下单流程中,需要调用各项外部的接口(风控、账户、营销、试算、支付),才能完成整个下单流程。但在本地开发过程中,不太能将所有的外部接口都调试为开发环境可用状态,所有这个时候要做单元化测试,对于一些不能随时提供服务的接口进行 Mock 处理。
本文涉及的工程:
因为 Mock 单元测试的重点,主要体现在;功能流程较长、调用外部接口稳定性较差、测试过程中希望可以不启动 SpringBoot 应用就能对单个功能模块进行测试验证。
所以本章节带着这样一个案例背景的情况,小傅哥带着大家把 《HTTP 框架使用和场景实战 - 结合ChatGLM自动回帖!》 做一个小重构。来对 Mock 框架进行验证使用。
# ChatGLM SDK Config
chatglm:
sdk:
config:
# 状态;true = 开启、false 关闭
enabled: false
# 官网地址
api-host: https://open.bigmodel.cn/
# 官网申请 https://open.bigmodel.cn/usercenter/apikeys
api-secret-key: d570f7c5d289cdac2abdfdc562e39f3f.trqz1dH8ZK6E***
@Bean
@ConditionalOnProperty(value = "chatglm.sdk.config.enabled", havingValue = "true", matchIfMissing = false)
public OpenAiSession openAiSession(ChatGLMSDKConfigProperties properties) {
// 1. 配置文件
cn.bugstack.chatglm.session.Configuration configuration = new cn.bugstack.chatglm.session.Configuration();
configuration.setApiHost(properties.getApiHost());
configuration.setApiSecretKey(properties.getApiSecretKey());
// 2. 会话工厂
OpenAiSessionFactory factory = new DefaultOpenAiSessionFactory(configuration);
// 3. 开启会话
return factory.openSession();
}
源码:cn.bugstack.xfg.dev.tech.infrastructure.gateway.api.IZSXQApi
public interface IZSXQApi {
/**
* 查询知识星球帖子内容
*
* @return 帖子数据
* @throws IOException 异常
*/
ResponseDTO topics() throws IOException;
/**
* 回复帖子
*
* @param topicId 帖子ID
* @param content 回复内容
*/
void comment(long topicId, String content);
}
源码:cn.bugstack.xfg.dev.tech.infrastructure.gateway.adapter.ZSXQAdapter
public class ZSXQAdapter implements IZSXQAdapter {
@Resource
private IZSXQApi zsxqApi;
@Override
public List<TopicsItemVO> queryTopics() {
try {
ResponseDTO responseDTO = zsxqApi.topics();
RespData respData = responseDTO.getRespData();
List<TopicsItem> topics = respData.getTopics();
List<TopicsItemVO> topicsItemVOList = new ArrayList<>();
for (TopicsItem topicsItem : topics) {
TopicsItemVO topicsItemVO = TopicsItemVO.builder()
.topicId(topicsItem.getTopicId())
.talk(topicsItem.getTalk().getText())
.showCommentsItems(topicsItem.getShowComments() != null ? topicsItem.getShowComments().stream()
.map(showCommentsItem -> {
TopicsItemVO.ShowCommentsItem item = new TopicsItemVO.ShowCommentsItem();
item.setUserId(showCommentsItem.getOwner().getUserId());
return item;
})
.collect(Collectors.toList()) : new ArrayList<>())
.build();
topicsItemVOList.add(topicsItemVO);
}
return topicsItemVOList;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean comment(long topicId, String content) {
zsxqApi.comment(topicId, content);
return true;
}
}
源码:cn.bugstack.xfg.dev.tech.job.ReplyJob
public class ReplyJob {
@Resource
private IAiReply aiReply;
@Scheduled(cron = "0/10 * * * * ?")
public void exec() throws Exception {
log.info("自动回帖任务开始执行...");
aiReply.doAiReply();
}
}
@EnableScheduling
注解是开启的,否则任务不能执行。@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApiTest {
@Resource
private IAiReply aiReply;
@Test
public void test_IAiReply() {
aiReply.doAiReply();
}
}
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class MockTest {
@Resource
private IAiReply aiReply;
@MockBean
private IZSXQAdapter izsxqAdapter;
@Test
public void test_doAiReply() throws InterruptedException, JsonProcessingException {
Mockito.when(izsxqAdapter.queryTopics()).thenReturn(new ArrayList<TopicsItemVO>() {{
TopicsItemVO topicsItemVO = new TopicsItemVO();
topicsItemVO.setTopicId(10001L);
topicsItemVO.setTalk("<e type=\"mention\" uid=\"241858242255511\" title=\"%40%E5%B0%8F%E5%82%85%E5%93%A5\" /> 提问 java 冒泡排序");
add(topicsItemVO);
}});
Mockito.when(izsxqAdapter.comment(Mockito.anyLong(), Mockito.anyString())).thenReturn(true);
aiReply.doAiReply();
// 等待;ChatGLM 异步回复
new CountDownLatch(1).await();
}
}
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class ZSXQAdapterTest {
@Mock
private IZSXQApi mockZsxqApi;
@InjectMocks
private ZSXQAdapter zsxqAdapterUnderTest;
@Test
public void testQueryTopics() throws Exception {
// Setup
final List<TopicsItemVO> expectedResult = Arrays.asList(TopicsItemVO.builder()
.topicId(0L)
.talk("talk")
.showCommentsItems(Arrays.asList(TopicsItemVO.ShowCommentsItem.builder()
.userId(0L)
.build()))
.build());
// Configure IZSXQApi.topics(...).
final ResponseDTO responseDTO = new ResponseDTO();
final RespData respData = new RespData();
final TopicsItem topicsItem = new TopicsItem();
final ShowCommentsItem showCommentsItem = new ShowCommentsItem();
final Owner owner = new Owner();
owner.setUserId(0L);
showCommentsItem.setOwner(owner);
topicsItem.setShowComments(Arrays.asList(showCommentsItem));
final Talk talk = new Talk();
talk.setText("talk");
topicsItem.setTalk(talk);
topicsItem.setTopicId(0L);
respData.setTopics(Arrays.asList(topicsItem));
responseDTO.setRespData(respData);
when(mockZsxqApi.topics()).thenReturn(responseDTO);
// Run the test
final List<TopicsItemVO> result = zsxqAdapterUnderTest.queryTopics();
// Verify the results
assertEquals(expectedResult, result);
log.info("测试结果:{}", JSON.toJSONString(result));
}
}
@RunWith(MockitoJUnitRunner.class)
、@Mock
、@InjectMocks
相当于模拟了一个启动的过程,只不过都是 Mock 的信息。但你可以根据这些信息来调试你的接口。IDEA Plugin Squaretest
它能自动的帮你生成Mock单元测试。这个插件是收费的,但还好不贵。