笔者的文章同时发布于 kubeclub
云原生技术社区,一个分享云原生生产经验,同时提供技术问答的平台,前往查看
以下所有的样例代码都是基于 junit5 + jmockit(推荐的单测基础框架选型),大部分是基于项目的真实代码片段
POM 依赖说明
// 主要 pom 说明
<!-- 自己封装的工具类-->
cn.kubeclub
fastjunit
1.0.1-release
org.jmockit
jmockit
1.36
test
org.junit.jupiter
junit-jupiter-api
5.3.0
test
org.junit.jupiter
junit-jupiter-engine
5.3.0
test
<!-- redis mock-->
com.github.fppt
jedis-mock
0.1.22
test
<!-- mq mock-->
org.springframework.kafka
spring-kafka-test
test
/**
* 被测试对象,一般结合Injectable进行参数注入
*/
@Tested
private TaskIssueRelServiceImpl taskIssueRelService;
/**
* 同mocked类似,但是区别在于它只影响当前实例
*/
@Injectable
private TestIssueService testIssueService;
@Injectable
private TaskIssueRelDaoImpl taskIssueRelDao;
@Test
void issueNamesByTaskId() {
List taskIssueRels = Lists.newLinkedList();
/**
* 用来实现mock对象的假设结果
*/
new Expectations() {
{
taskIssueRelDao.listByParams((Map) any);
result = taskIssueRels;
}
};
Assertions.assertTrue(taskIssueRelService.issueNamesByTaskId("111") == null);
/**
* 验证方法被调用次数,一般不会使用,通常都用Assert来判断结果,
* 无返回结果的方法可以确认调用情况
*/
new Verifications() {
{
taskIssueRelDao.listByParams((Map) any);
times = 1;
}
};
TaskIssueRelBO taskIssueRelBO = new TaskIssueRelBO();
taskIssueRelBO.setIssueNo("1");
taskIssueRelBO.setTopic("test");
taskIssueRels.add(taskIssueRelBO);
Assertions.assertTrue(taskIssueRelService.issueNamesByTaskId("1").equals("【1】test"));
}
/**
* mocked的方式所有方法都被mock掉了,包含静态方法
* jmockit会自己实例化,不用像tested结合injected来完成初始化
* mocked对象为接口或抽象类时会返回默认值
* 一般用来mock其他服务的对象
*
* @param taskIssueRelService
*/
@Test
void testMock(@Mocked TaskIssueRelService taskIssueRelService, @Mocked Locale locale) {
Assertions.assertTrue(taskIssueRelService.getMaxNumber() == 0L);
Assertions.assertTrue(taskIssueRelService.getIssueNosByTaskId("20210410112332838019002108000001").isEmpty());
Assertions.assertTrue(locale.getDefault() == null);
Assertions.assertTrue(locale.getCountry() == null);
Locale locale1 = new Locale("zh", "cn");
Assertions.assertTrue(locale1.getCountry() == null);
}
/**
* 进行局部方法模拟,不能mock接口
* 只会影响到部分方法,其余方法正常访问
*/
@Test
void testMockUp() {
List result = Lists.newArrayList("1111");
Assertions.assertTrue(taskIssueRelService.getMaxNumber().equals(0L));
new MockUp(TaskIssueRelServiceImpl.class) {
@Mock
List getIssueNosByTaskId(String taskId) {
return result;
}
@Mock
Long getMaxNumber() {
return 1L;
}
};
Assertions.assertTrue(taskIssueRelService.getIssueNosByTaskId("test").equals(result));
Assertions.assertFalse(taskIssueRelService.getMaxNumber().equals(0L));
}
class TeamDelayCountHandlerTest {
@Tested
private FlowDelayCountHandle flowDelayCountHandle;
@Injectable
private TaskDaoImpl taskDao;
@Injectable
private TeamDelayCountDaoImpl teamDelayCountDao;
/**
* 不仅能实现mock,还会将其所关联的子类的结果都影响到
* 一般用于我们只知道父类或接口时,又想控制所有子类的行为时可以使用
*
* @param delayCountHandle
*/
@Test
void testCapturing(@Capturing ITeamDelayCountHandle delayCountHandle) {
new Expectations() {
{
delayCountHandle.getDelayTypeEnum();
result = DelayTypeEnum.JOB_DELAY;
}
};
Assertions.assertFalse(flowDelayCountHandle.getDelayTypeEnum().equals(DelayTypeEnum.FLOW_FINISH_DELAY));
Assertions.assertTrue(flowDelayCountHandle.getDelayTypeEnum().equals(DelayTypeEnum.JOB_DELAY));
}
@Test
void testNoCapturing() {
Assertions.assertTrue(flowDelayCountHandle.getDelayTypeEnum().equals(DelayTypeEnum.FLOW_FINISH_DELAY));
}
}
controller调用service,service 调用 mapper,以 controller mock mapper 为例,主要用到反射的工具类 ReflectionTestUtils。
@GetMapping(value = "/sdkVersionCheck/{ciPipelineId}/{gitPipelineId}/{serviceName}/{env}")
public AjaxResult sdkVersionCheck(@PathVariable("ciPipelineId") Long ciPipelineId,
@PathVariable("gitPipelineId") Long gitPipelineId,
@PathVariable("serviceName") String serviceName,
@PathVariable("env") String env) {
// sdk版本校验
SdkDependencyCheckDTO sdkDependencyCheckDTO = new SdkDependencyCheckDTO(serviceName, env);
SdkPipelineCheckDTO sdkPipelineCheckDTO = sdkDependencyCheckService.checkPipelineSdk(sdkDependencyCheckDTO);
// 插入步骤记录
Long ciRecordId = ciPipelineRecordService.getCiRecordIdByCiIdAndGitPipelineId(ciPipelineId, gitPipelineId);
ciPipelineRecordThresholdService.insertSdkCheckResult(ciRecordId, sdkPipelineCheckDTO);
boolean checkResult = sdkPipelineCheckDTO.getCheckResult();
return AjaxResult.success(checkResult ? "SDK 版本校验通过" : "SDK 检验不通过", sdkPipelineCheckDTO);
}
@Service
public class CiPipelineRecordService{
@Override
public Long getCiRecordIdByCiIdAndGitPipelineId(Long ciPipelineId, Long gitPipelineId) {
return ciPipelineRecordMapper.getCiRecordIdByCiIdAndGitPipelineId(ciPipelineId, gitPipelineId);
}
}
/**
* @author yongzhe.dong
* @date 2021/9/14
*/
class CallbackCiPipelineControllerTest {
@Tested
private CallbackCiPipelineController callbackCiPipelineController;
@Injectable
private ICiPipelineRecordThresholdService ciPipelineRecordThresholdService;
@Injectable
private ISdkDependencyCheckService sdkDependencyCheckService;
@DisplayName("测试 sdk 版本校验")
@Test
void testSdkVersionCheck(@Capturing CiPipelineRecordMapper ciPipelineRecordMapper) {
Long ciPipelineId = 1L;
Long gitPipelineId = 1L;
String serviceName = "ucp";
String env = "test";
Long ciRecordId = 1L;
SdkPipelineCheckDTO sdkPipelineCheckDTO = DataProvider.anyObject(SdkPipelineCheckDTO.class);
sdkPipelineCheckDTO.setCheckResult(Boolean.TRUE);
new Expectations() {
{
sdkDependencyCheckService.checkPipelineSdk(withNotNull());
result = sdkPipelineCheckDTO;
ciPipelineRecordThresholdService.insertSdkCheckResult(ciRecordId, sdkPipelineCheckDTO);
times = 1;
ciPipelineRecordMapper.getCiRecordIdByCiIdAndGitPipelineId(ciPipelineId, gitPipelineId);
result = 1L;
}
};
ICiPipelineRecordService ciPipelineRecordService = new CiPipelineRecordServiceImpl();
// 利用反射工具注入
ReflectionTestUtils.setField(ciPipelineRecordService, "ciPipelineRecordMapper", ciPipelineRecordMapper);
ReflectionTestUtils.setField(callbackCiPipelineController, "ciPipelineRecordService", ciPipelineRecordService);
AjaxResult ajaxResult = callbackCiPipelineController.sdkVersionCheck(ciPipelineId, gitPipelineId, serviceName, env);
Assertions.assertThat(ajaxResult)
.hasFieldOrPropertyWithValue("code", 200)
.hasFieldOrPropertyWithValue("msg", "SDK 版本校验通过");
}
}
@Test
public void sayHello1() {
// 录制(Record)
new Expectations() {
{
helloJMockit.sayHello();
// 期待上述调用的返回是"hello,david",而不是返回实际返回值
result = "hello david";
}
};
// 重放(Replay)
String msg = helloJMockit.sayHello();
Assert.assertTrue(msg.equals("hello david"));
// 验证(Verification)
new Verifications() {
{
helloJMockit.sayHello();
// 验证helloJMockit.sayHello()这个方法调用了1次
times = 1;
}
};
}
//利用Mockito.atLeastOnce()判断方法被调用的次数
Mockito.verify(orderService, Mockito.atLeastOnce()).getOrder(1L);
Mockito.verify(orderService, Mockito.times(1)).getOrder(1L);
Mockito.verify(orderService, Mockito.never()).toString();
//也可以利用Matchers判断入参是否按照预期
Mockito.verify(orderService).getOrder( Matchers.eq( 1L ) );
//捕获Mock方法的入参,判断该入参是否符合预期
ArgumentCaptor argument = ArgumentCaptor.forClass(Long.class);
Mockito.verify(orderService).getOrder(argument.capture());
Assert.assertEquals((Long)1L, argument.getValue());
Expectations 可以mock静态方法、普通方法、final方法,不能mock私有方法、native方法
MockUp 可以mock静态方法、普通方法、final方法、私有方法、native方法
DemoService
public class DemoService {
// 静态方法
public static String getString() {
return "static method";
}
public Integer getInt() {
return randomInt();
}
// 私有方法
private int randomInt() {
Random random = new Random();
return random.nextInt(100);
}
}
DemoServiceTest
public class DemoTest {
@Tested
DemoService demoService;
@Test
void staticMethodTest() {
Assertions.assertEquals("static method", DemoService.getString());
// mock 静态方法
new Expectations(DemoService.class) {
{
DemoService.getString(); result = "mocked";
}
};
Assertions.assertEquals("mocked", DemoService.getString());
}
@Test
void privateMethodTest() {
new DemoServiceMockUp();
Assertions.assertEquals(99, demoService.getInt().intValue());
}
// 继承 MockUp 类 mock 私有方法
class DemoServiceMockUp extends MockUp {
@Mock
int randomInt() {
return 99;
}
}
}
/**
* List将对象转为JSONArray然后进行比较
*
* @param actual 实际数据
* @param expected 期望数据
*/
public static void assertArray(Object actual, Object expected, String... key) {
// 对实际数据和期望数据进行统一的日期格式化处理
String actualJsonStr = JSON.toJSONStringWithDateFormat(actual,
"yyyy-MM-dd HH:mm:ss", SerializerFeature.PrettyFormat);
String expectedJsonStr = JSON.toJSONStringWithDateFormat(expected,
"yyyy-MM-dd HH:mm:ss", SerializerFeature.PrettyFormat);
// 将需要校验的数组类型的数据转为JSONArray
JSONArray actualArray = JSON.parseArray(actualJsonStr);
JSONArray expectedArray = JSON.parseArray(expectedJsonStr);
// 如果有传入字段名则使用该字段名,未传入,则取"data"
String filedName = key.length <= 0 ? StrUtil.C_BRACKET_START + "data" + StrUtil.C_BRACKET_END : StrUtil.C_BRACKET_START + key[NUM_0] + StrUtil.C_BRACKET_END;
// 如果实际结果的长度和期望结果的长度不一致,校验失败
if (actualArray.size() != expectedArray.size()) {
log.info("返回的" + filedName + "数据为:" + StrUtil.CRLF + actualJsonStr);
log.info("断言的" + filedName + "数据为:" + StrUtil.CRLF + expectedJsonStr);
throw new AssertionError("返回的" + filedName + "字段,数组长度与断言数组的长度不一致,请检查!!!!");
}
// 如果实际结果中不包含期望结果中的所有数据,校验失败
if (!actualArray.containsAll(expectedArray)) {
log.info("返回的" + filedName + "数据为:" + StrUtil.CRLF + actualJsonStr);
log.info("断言的" + filedName + "数据为:" + StrUtil.CRLF + expectedJsonStr);
throw new AssertionError("返回数据中存在断言数据中不存在的元素,请检查!!!!");
}
// 如果期望结果中不包含实际结果中的所有数据,校验失败
if (!expectedArray.containsAll(actualArray)) {
log.info("返回的" + filedName + "数据为:" + StrUtil.CRLF + actualJsonStr);
log.info("断言的" + filedName + "数据为:" + StrUtil.CRLF + expectedJsonStr);
throw new AssertionError("断言数据中存在返回数据中不存在的元素,请检查!!!!");
}
}
/**
* Map将对象转为JSONObject然后进行比较
*
* @param actual 实际数据
* @param expected 期望数据
*/
public static void assertMap(Object actual, Object expected) {
//将传入的响应数据解析为JSONObject
JSONObject actualObject = (JSONObject) JSON.toJSON(expected);
//将期望数据转为JSONObject
JSONObject expectedObject = (JSONObject) JSON.toJSON(expected);
//对于期望的每个字段进行比较
for (String key : expectedObject.keySet()) {
//如果字段对应的value为JSONArray,调用列表比较方法
if (expectedObject.get(key) instanceof JSONArray) {
assertArray(actualObject.get(key), expectedObject.get(key), key);
continue;
}
// 如果字段对应的value为JSONObject,调用对象比较方法
if (expectedObject.get(key) instanceof JSONObject) {
assertObject(actualObject.get(key), expectedObject.get(key));
continue;
}
//进行字段的比较
Assert.assertEquals(actualObject.get(key), expectedObject.get(key), "字段[" + key + "]比较不一致,请检查!!!");
}
}
@Injectable 注入一个接口实例,mock只对该实例有效
@Capturing 捕获接口所有实例,mock对接口的所有实现类都有效
DemoInterface
public interface DemoInterface {
String getString();
}
DemoInterfaceTest
public class DemoTest2 {
@Test
void interfaceTest(@Injectable DemoInterface demoInterface) {
// 该实现类没有被 mock
DemoInterface myDemoInterfaceImpl = new DemoInterface() {
@Override
public String getString() {
return "myDemoIntefraceImpl";
}
};
Assertions.assertEquals("myDemoIntefraceImpl", myDemoInterfaceImpl.getString());
Assertions.assertEquals(null, demoInterface.getString());
new Expectations() {
{
demoInterface.getString(); result = "mock string";
}
};
Assertions.assertEquals("mock string", demoInterface.getString());
Assertions.assertEquals("myDemoIntefraceImpl", myDemoInterfaceImpl.getString());
}
@Test
void interfaceTest2(@Capturing DemoInterface demoInterface // 捕获所有实现类) {
DemoInterface myDemoInterfaceImpl = new DemoInterface() {
@Override
public String getString() {
return "myDemoIntefraceImpl";
}
};
DemoInterface myDemoIntefraceImpl2 = new DemoInterface() {
@Override
public String getString() {
return "myDemoIntefraceImpl2";
}
};
// 这两个实现类都被 mock 了
Assertions.assertEquals(null, myDemoInterfaceImpl.getString());
Assertions.assertEquals(null, myDemoIntefraceImpl2.getString());
new Expectations() {
{
demoInterface.getString(); result = "mock string";
}
};
Assertions.assertEquals("mock string", myDemoInterfaceImpl.getString());
Assertions.assertEquals("mock string", myDemoIntefraceImpl2.getString());
}
}
主要针对 mybatis 和 H2 内存数据库实现对数据库的单元测试
单元测试中对 service 和 dao 层测试时,主要存在以下问题:
/**
* dao 测试工具类
*
* @author yongzhe.dong
* @date 2020/09/27
*/
public class BaseDaoTestUtil {
private static final Logger logger = LoggerFactory.getLogger(BaseDaoTestUtil.class);
public final static String DRIVER = "org.h2.Driver";
public final static String DB_URL = "jdbc:h2:mem:test;MODE=MySql;DB_CLOSE_DELAY=-1";
public final static String USER = "root";
public final static String PASSWORD = "123456";
/**
* 配置
*/
private static Configuration configuration;
private static final ThreadLocal localSessions = new ThreadLocal<>();
static {
try {
// 定义 configuration
configuration = new Configuration();
// 不使用全局缓存
configuration.setCacheEnabled(false);
configuration.setLazyLoadingEnabled(false);
configuration.setAggressiveLazyLoading(true);
configuration.setDefaultStatementTimeout(20);
} catch (Exception e) {
logger.error("实例化 configuration 失败!", e);
}
}
/**
* 创建 sqlsession 会话
*
* @return
*/
public static SqlSession getSqlSession() {
SqlSession sqlSession = localSessions.get();
if (sqlSession == null) {
//设置数据库链接
UnpooledDataSource dataSource = new UnpooledDataSource();
dataSource.setDriver(DRIVER);
dataSource.setUrl(DB_URL);
dataSource.setUsername(USER);
dataSource.setPassword(PASSWORD);
//设置事务,使用 JDBC 的事务管理方式(测试设置事务不提交false)
Transaction transaction = new JdbcTransaction(dataSource, TransactionIsolationLevel.READ_UNCOMMITTED, false);
//设置执行
Executor executor = configuration.newExecutor(transaction);
//直接实例化一个默认的sqlSession
//是做单元测试,那么没必要通过SqlSessionFactoryBuilder构造SqlSessionFactory,再来获取SqlSession
sqlSession = new DefaultSqlSession(configuration, executor, false);
localSessions.set(sqlSession);
}
return sqlSession;
}
/**
* 获取 mapper 对象
*
* @param mapperXmlPath mapper xml 文件所在路径
* @param mapperClass mapper
* @param
* @return
*/
public static T getMapper(String mapperXmlPath, Class mapperClass) {
try {
//解析 mapper.xml 文件
Resource mapperResource = new ClassPathResource(mapperXmlPath);
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperResource.getInputStream(), configuration, mapperResource.toString(), configuration.getSqlFragments());
xmlMapperBuilder.parse();
// 返回 mapper 实例
return getSqlSession().getMapper(mapperClass);
} catch (Exception e) {
logger.error("实例化 mapper 失败!mapperXmlPath >>>>>{}", mapperXmlPath, e);
}
return null;
}
/**
* 销毁会话
*/
public static void closeSqlSession() {
SqlSession sqlSession = localSessions.get();
if (sqlSession != null) {
sqlSession.close();
localSessions.remove();
}
logger.info("#Close Session successfully");
}
}
/**
* TemplateCi dao 测试
* @author yongzhe.dong
* @date 2021/9/28
*/
class TemplateCiDaoTest {
private static SqlSession sqlSession;
private static TemplateCiMapper templateCiMapper;
@BeforeAll
static void setUpAll() throws IOException {
// 获取 mapper 实例
templateCiMapper = BaseDaoTestUtil.getMapper("mapper/ci/TemplateCiMapper.xml", TemplateCiMapper.class);
sqlSession = BaseDaoTestUtil.getSqlSession();
Connection connection = sqlSession.getConnection();
// 创建 ScriptRunner,读取 SQL 脚本并执行
ScriptRunner runner = new ScriptRunner(connection);
runner.setErrorLogWriter(null);
runner.setLogWriter(null);
// 执行初始化SQL脚本
runner.runScript(Resources.getResourceAsReader("sql/TemplateCi.sql"));
}
@AfterEach
void teardown() {
sqlSession.commit();
}
@AfterAll
static void teardownAll() {
BaseDaoTestUtil.closeSqlSession();
}
@DisplayName("Test insert and query list")
@Test
void testSelectTemplateCiList() {
Long templateId = 1L;
TemplateCi templateCi = DataProvider.anyObject(TemplateCi.class);
templateCi.setId(templateId);
templateCi.setNeedSubmodules(1);
templateCi.setNeedBranch(1);
templateCi.setCmdbServiceName(1);
// 插入
templateCiMapper.insertTemplateCi(templateCi);
// 查询
List templateCis = templateCiMapper.selectTemplateCiList(new TemplateCi());
// 断言
Assertions.assertThat(templateCis)
.isNotEmpty().containsExactly(templateCi);
}
}
Fastjunit 包的工具方法
BeanObject beanObject = .anyObject(BeanObject.class);
System.out.println("anyObject:" + JsonUtils.writeJsonStr(beanObject));
BeanObject[] beanObjectArray = DataProvider.anyArray(BeanObject.class);
System.out.println("数组:" + JsonUtils.writeJsonStr(beanObjectArray));
BeanObject[] beanObjectArray = DataProvider.anyArray(BeanObject.class);
System.out.println("数组:" + JsonUtils.writeJsonStr(beanObjectArray));
业务中很多是普通的增删改查,针对这种场景,你一个个方法去测意义不大,为了覆盖率能达标且用例是有意义的,建议直接从 controler api 入口到 mapper 数据库直接单测执行下去。这种严格算来是属于集成测试的,等到后续如果业务变复杂了,可以再拆开测试。
单测代码最好不要启动 spring 容器,如果必要的话就要尽可能小范围的加载 spring 相关的东西,千万不用启动整个 spring 容器,这样会非常慢。
package tech.yummy.devops.hotwheel.business.controller.app;
import cn.kubeclub.core.data.DataProvider;
import mockit.Expectations;
import org.assertj.core.api.Assertions;
import org.junit.FixMethodOrder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.runners.MethodSorters;
import org.springframework.context.annotation.Profile;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.util.ReflectionTestUtils;
import tech.yummy.devops.hotwheel.base.BaseDatabaseMock;
import tech.yummy.devops.hotwheel.business.domain.app.App;
import tech.yummy.devops.hotwheel.business.service.app.impl.AppServiceImpl;
import tech.yummy.devops.hotwheel.core.page.TableDataInfo;
import tech.yummy.devops.hotwheel.core.page.TableSupport;
import tech.yummy.devops.hotwheel.infrastructure.dao.app.AppMapper;
import tech.yummy.devops.hotwheel.utils.ServletUtils;
import javax.annotation.Resource;
/**
* @Author: [email protected]
* @Description:
* @Date: 2021/9/27 18:53
*/
/**
* 贫血操作的增删改查
*/
@Sql({"classpath:sql/app.sql"})
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class AppControllerTest extends BaseDatabaseMock {
AppController appController = new AppController();
@Resource
private AppMapper appMapper;
AppServiceImpl appService = new AppServiceImpl();
App app = DataProvider.anyObject(App.class);
@BeforeEach
void setup(){
// 将 mapper 对象反射注入 service 中
ReflectionTestUtils.setField(appService, "appMapper", appMapper);
}
/**
* 从 controller 层直接测试到数据库层,因为几乎没有业务逻辑,都是贫血操作。
* 下面相当于 4 个 case 写一起了
* 但凡有些业务逻辑的,不建议这样测。因为这相当于集成测试了。
*/
@Test
@Rollback
void crudTest() {
// 新增
appService.insertApp(app);
// 查列表
bList();
// 查询 ID
selectAppById();
// 删除
deleteAppById();
}
/**
* 查询列表
*/
void bList() {
new Expectations(ServletUtils.class) {
{
ServletUtils.getParameterToInt(TableSupport.PAGE_NUM); result = 1;
ServletUtils.getParameterToInt(TableSupport.PAGE_SIZE); result = 10;
ServletUtils.getParameter(TableSupport.ORDER_BY_COLUMN); result = null;
ServletUtils.getParameter(TableSupport.IS_ASC); result = null;
}
};
ReflectionTestUtils.setField(appController, "appService", appService);
App param = new App();
TableDataInfo result = appController.list(param);
Assertions.assertThat(result).as("app 列表查询验证")
.extracting(TableDataInfo::getRows)
.asList()
.hasSize(1)
.first()
// 校验所有非空字段
.hasFieldOrPropertyWithValue("name",app.getName())
.hasFieldOrPropertyWithValue("clusterId",app.getClusterId())
.hasFieldOrPropertyWithValue("namespace",app.getNamespace());
}
/**
* 查询详情
*/
void selectAppById() {
App result = appService.selectAppById(app.getId());
Assertions.assertThat(result).as("app ID查询验证")
.hasFieldOrPropertyWithValue("name",app.getName())
.hasFieldOrPropertyWithValue("clusterId",app.getClusterId())
.hasFieldOrPropertyWithValue("namespace",app.getNamespace());
}
/**
* 删除
*/
void deleteAppById() {
appService.deleteAppById(app.getId());
}
}
复杂业务的一大困惑点是 一大堆需要 mock 的上下文,可以通过以下方式梳理:
了解 mock 跟 mockup 的区别
从整个业务去考虑单测的上下文,而不是每个方法各自考虑
上下文的封装抽取,不要杂在用例里面。
mock 跟 mockup 的区别
在之前的分享里面有谈单替身对象根据场景是有很多区分的:单元测试 - 交流分享
在 jmockit 的语法里面:mock 的作用是替换,mockup 的作用是伪装(不是简单的给你代替了,而是要伪装成类似本来的你)
从整个业务去考虑单测的上下文
复杂业务里面需要 mock 的上下文很多,如果每个用例单独进行,重复 mock 的对象会有很多。可以全局考虑,统一 mock。
上下文封装抽取
用例代码
package tech.yummy.devops.hotwheel.business.service.kubernetes;
import cn.kubeclub.core.data.DataProvider;
import lombok.extern.slf4j.Slf4j;
import mockit.Injectable;
import mockit.Tested;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import tech.yummy.devops.hotwheel.business.controller.app.vo.AppBriefVo;
import tech.yummy.devops.hotwheel.business.controller.app.vo.PodVo;
import tech.yummy.devops.hotwheel.business.service.cd.IAppClusterCdService;
import tech.yummy.devops.hotwheel.business.service.cluster.IClusterService;
import tech.yummy.devops.hotwheel.business.service.kubernetes.mockup.MockInformerService;
import tech.yummy.devops.hotwheel.infrastructure.dao.cd.CdPipelineMapper;
import tech.yummy.devops.hotwheel.infrastructure.dao.ci.CiPipelineRecordImageMapper;
import tech.yummy.devops.hotwheel.infrastructure.k8s.ApiClientFactory;
import tech.yummy.devops.hotwheel.infrastructure.util.AppUserRoleUtil;
import java.util.List;
/**
* @Author: [email protected]
* @Description:
* @Date: 2021/9/29 10:33
*/
@Slf4j
class K8sDeploymentServiceTest {
@Tested
K8sDeploymentService k8sDeploymentService;
@Injectable
ApiClientFactory apiClientFactory;
@Injectable
IClusterService clusterService;
@Injectable
CdPipelineMapper cdPipelineMapper;
@Injectable
IAppClusterCdService appClusterCdService;
@Injectable
CiPipelineRecordImageMapper ciPipelineRecordImageMapper;
@Injectable
private AppUserRoleUtil appUserRoleUtil;
@BeforeAll
static void setup(){
// 启动 informer 伪装类
new MockInformerService();
}
@Test
void selectPodsTest(){
// 执行测试
List selectPods = k8sDeploymentService.selectPods("dev-cluster","default", "hello-spring");
//下面校验的参数是伪装类 MockInformerService 里面写死的了
Assertions.assertThat(selectPods)
.hasSize(1)
.first()
.hasFieldOrPropertyWithValue("name","hello-spring-6bd8c5bfbf-zpwdh")
.hasFieldOrPropertyWithValue("namespace","default")
.hasFieldOrPropertyWithValue("clusterName","dev-cluster")
.hasFieldOrPropertyWithValue("resourceVersion","78131792")
.hasFieldOrPropertyWithValue("showStatus","Running")
.hasFieldOrPropertyWithValue("conditionType","Ready");
}
@Test
void getDeploymentInfoTest(){
// 查询负载详情
AppBriefVo appBriefVo = k8sDeploymentService.getDeploymentInfo("dev-cluster","default", "hello-spring");
// 下面校验的参数是伪装类 MockInformerService 里面写死的了
Assertions.assertThat(appBriefVo)
.isNotNull()
.hasFieldOrPropertyWithValue("name","hello-spring")
.hasFieldOrPropertyWithValue("namespace","default")
.hasFieldOrPropertyWithValue("clusterName","dev-cluster");
}
}
Mock 对象
package tech.yummy.devops.hotwheel.business.service.kubernetes.mockup;
import io.kubernetes.client.informer.cache.Indexer;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1Pod;
import io.kubernetes.client.util.Yaml;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* @Author: [email protected]
* @Description:
* @Date: 2021/9/30 15:59
*/
@Slf4j
public class MockIndexerObject implements Indexer {
public Class class2;
public static String DEPLOYMENT_CLASS_NAME = "io.kubernetes.client.openapi.models.V1Deployment";
public MockIndexerObject(Class class2) {
this.class2 = class2;
}
public static V1Pod getV1Pod(){
V1Pod pod = null;
try {
pod = (V1Pod) Yaml.load(HELLO_SPRING_POD);
} catch (IOException e) {
log.warn("伪造 pod 对象失败",e);
}
return pod;
}
public static V1Deployment getV1Deployment(){
V1Deployment deployment = null;
try {
deployment = (V1Deployment) Yaml.load(HELLO_SPRING_DEPLOYMENT);
} catch (IOException e) {
log.warn("伪造 deployment 对象失败",e);
}
return deployment;
}
@Override
public void add(Object obj) {
}
@Override
public void update(Object obj) {
}
@Override
public void delete(Object obj) {
}
@Override
public void replace(List list, String resourceVersion) {
}
@Override
public void resync() {}
@Override
public List listKeys() {
return null;
}
@Override
public Object get(Object obj) {
if (POD_CLASS_NAME.equalsIgnoreCase(class2.getName())) {
return getV1Pod();
}
if (DEPLOYMENT_CLASS_NAME.equalsIgnoreCase(class2.getName())) {
return getV1Deployment();
}
return null;
}
@Override
public Object getByKey(String key) {
return get(key);
}
@Override
public List list() {
return null;
}
@Override
public List index(String indexName, Object obj) {
return null;
}
@Override
public List indexKeys(String indexName, String indexKey) {
return null;
}
@Override
public List byIndex(String indexName, String indexKey) {
if (POD_CLASS_NAME.equalsIgnoreCase(class2.getName())) {
List v1PodList = Lists.newArrayList();
v1PodList.add(getV1Pod());
return v1PodList;
}
if (DEPLOYMENT_CLASS_NAME.equalsIgnoreCase(class2.getName())) {
List v1Deployment = Lists.newArrayList();
v1Deployment.add(getV1Deployment());
return v1Deployment;
}
return null;
}
@Override
public Map>> getIndexers() {
return null;
}
@Override
public void addIndexers(Map indexers) {
}
public static final String HELLO_SPRING_DEPLOYMENT = "kind: Deployment\n" +
"apiVersion: apps/v1\n" +
"metadata:\n" +
" name: hello-spring\n" +
" namespace: default\n" +
" selfLink: /apis/apps/v1/namespaces/default/deployments/hello-spring\n" +
" uid: 765c5a76-7799-4cc4-9742-8bc8aec5e2c7\n" +
" resourceVersion: '78135683'\n" +
" generation: 235\n" +
" creationTimestamp: '2021-08-18T08:58:01Z'\n" +
" labels:\n" +
" app: hello-spring\n" +
" cdPipelineId: '84'\n" +
" cdPipelineName: hello-spring\n" +
" cdRecordId: '1293'\n" +
" createEmp: yuqin.zhao\n" +
" annotations:\n" +
" deployment.kubernetes.io/revision: '227'\n" +
" kubectl.kubernetes.io/restartedAt: '2021-09-17T11:06:13+08:00'\n" +
"spec:\n" +
" replicas: 2\n" +
" selector:\n" +
" matchLabels:\n" +
" app: hello-spring\n" +
" template:\n" +
" metadata:\n" +
" creationTimestamp: null\n" +
" labels:\n" +
" app: hello-spring\n" +
" annotations:\n" +
" kubectl.kubernetes.io/restartedAt: '2021-09-17T11:22:34+08:00'\n" +
" spec:\n" +
" volumes:\n" +
" - name: secret-1\n" +
" secret:\n" +
" secretName: hello-spring-secret\n" +
" items:\n" +
" - key: test-secret.txt\n" +
" path: test-secret.txt\n" +
" defaultMode: 420\n" +
" containers:\n" +
" - name: hello-spring\n" +
" image: 'swr.cn-north-4.myhuaweicloud.com/yummy-default/hello-spring-develop:77963'\n" +
" env:\n" +
" - name: SPRING_PROFILES_ACTIVE\n" +
" value: dev\n" +
" - name: EGG_SERVER_ENV\n" +
" value: dev\n" +
" - name: SERVER_SOURCE\n" +
" value: CCE\n" +
" - name: SWKAC_ENABLE\n" +
" value: 'true'\n" +
" - name: CCE_POD_NAME\n" +
" valueFrom:\n" +
" fieldRef:\n" +
" apiVersion: v1\n" +
" fieldPath: metadata.name\n" +
" - name: CCE_NAMESPACE\n" +
" valueFrom:\n" +
" fieldRef:\n" +
" apiVersion: v1\n" +
" fieldPath: metadata.namespace\n" +
" - name: CCE_POD_IP\n" +
" valueFrom:\n" +
" fieldRef:\n" +
" apiVersion: v1\n" +
" fieldPath: status.podIP\n" +
" - name: CCE_NODE_NAME\n" +
" valueFrom:\n" +
" fieldRef:\n" +
" apiVersion: v1\n" +
" fieldPath: spec.nodeName\n" +
" - name: test\n" +
" value: test\n" +
" resources:\n" +
" limits:\n" +
" cpu: '1'\n" +
" memory: 2Gi\n" +
" requests:\n" +
" cpu: 100m\n" +
" memory: 107374182400m\n" +
" volumeMounts:\n" +
" - name: secret-1\n" +
" mountPath: /test/test-secret.txt\n" +
" subPath: test-secret.txt\n" +
" livenessProbe:\n" +
" httpGet:\n" +
" path: /health\n" +
" port: 8080\n" +
" scheme: HTTP\n" +
" initialDelaySeconds: 5\n" +
" timeoutSeconds: 2\n" +
" periodSeconds: 10\n" +
" successThreshold: 1\n" +
" failureThreshold: 20\n" +
" readinessProbe:\n" +
" tcpSocket:\n" +
" port: 8080\n" +
" initialDelaySeconds: 10\n" +
" timeoutSeconds: 2\n" +
" periodSeconds: 30\n" +
" successThreshold: 1\n" +
" failureThreshold: 5\n" +
" lifecycle:\n" +
" preStop:\n" +
" exec:\n" +
" command:\n" +
" - /bin/sh\n" +
" - '-c'\n" +
" - java -jar /home/appuser/java_pre_stop-1.0.jar '/home/appuser/gc.hprof' >> upload_obs.log\n" +
" terminationMessagePath: /dev/termination-log\n" +
" terminationMessagePolicy: File\n" +
" imagePullPolicy: Always\n" +
" securityContext:\n" +
" capabilities:\n" +
" add:\n" +
" - NET_BIND_SERVICE\n" +
" drop:\n" +
" - ALL\n" +
" allowPrivilegeEscalation: false\n" +
" restartPolicy: Always\n" +
" terminationGracePeriodSeconds: 15\n" +
" dnsPolicy: ClusterFirst\n" +
" automountServiceAccountToken: false\n" +
" securityContext:\n" +
" runAsUser: 1000\n" +
" runAsGroup: 1000\n" +
" runAsNonRoot: true\n" +
" fsGroup: 1000\n" +
" imagePullSecrets:\n" +
" - name: default-secret\n" +
" affinity:\n" +
" nodeAffinity:\n" +
" requiredDuringSchedulingIgnoredDuringExecution:\n" +
" nodeSelectorTerms:\n" +
" - matchExpressions:\n" +
" - key: nodename\n" +
" operator: NotIn\n" +
" values:\n" +
" - gitlab-runner\n" +
" podAntiAffinity:\n" +
" preferredDuringSchedulingIgnoredDuringExecution:\n" +
" - weight: 1\n" +
" podAffinityTerm:\n" +
" labelSelector:\n" +
" matchExpressions:\n" +
" - key: app\n" +
" operator: In\n" +
" values:\n" +
" - hello-spring\n" +
" topologyKey: kubernetes.io/hostname\n" +
" schedulerName: default-scheduler\n" +
" strategy:\n" +
" type: RollingUpdate\n" +
" rollingUpdate:\n" +
" maxUnavailable: 1\n" +
" maxSurge: 2\n" +
" minReadySeconds: 5\n" +
" revisionHistoryLimit: 10\n" +
" progressDeadlineSeconds: 600\n" +
"status:\n" +
" observedGeneration: 235\n" +
" replicas: 2\n" +
" updatedReplicas: 2\n" +
" readyReplicas: 2\n" +
" availableReplicas: 2\n" +
" conditions:\n" +
" - type: Available\n" +
" status: 'True'\n" +
" lastUpdateTime: '2021-09-29T06:09:47Z'\n" +
" lastTransitionTime: '2021-09-29T06:09:47Z'\n" +
" reason: MinimumReplicasAvailable\n" +
" message: Deployment has minimum availability.\n" +
" - type: Progressing\n" +
" status: 'True'\n" +
" lastUpdateTime: '2021-09-30T03:26:48Z'\n" +
" lastTransitionTime: '2021-09-29T06:29:55Z'\n" +
" reason: NewReplicaSetAvailable\n" +
" message: ReplicaSet \"hello-spring-fdd9cf587\" has successfully progressed.\n";
public static final String HELLO_SPRING_POD = "apiVersion: v1\n" +
"kind: Pod\n" +
"metadata:\n" +
" name: hello-spring-6bd8c5bfbf-zpwdh\n" +
" generateName: hello-spring-6bd8c5bfbf-\n" +
" namespace: default\n" +
" selfLink: /api/v1/namespaces/default/pods/hello-spring-6bd8c5bfbf-zpwdh\n" +
" uid: 953b0fc8-0365-49f4-b509-73533f1069c4\n" +
" resourceVersion: '78131792'\n" +
" creationTimestamp: '2021-09-30T03:18:15Z'\n" +
" labels:\n" +
" app: hello-spring\n" +
" pod-template-hash: 6bd8c5bfbf\n" +
" skywalking: enabled\n" +
" skywalking-volume: skywalking-8bf807be\n" +
"spec:\n" +
" volumes:\n" +
" - name: secret-1\n" +
" secret:\n" +
" secretName: hello-spring-secret\n" +
" items:\n" +
" - key: test-secret.txt\n" +
" path: test-secret.txt\n" +
" defaultMode: 420\n" +
" - name: skywalking-8bf807be\n" +
" emptyDir:\n" +
" sizeLimit: 200Mi\n" +
" initContainers:\n" +
" - name: skywalking-init-8bf807be\n" +
" image: 'swr.cn-north-4.myhuaweicloud.com/joymo-archcenter/skywalking-agent:77743'\n" +
" env:\n" +
" - name: AGENT_HOME\n" +
" value: /opt/skywalking\n" +
" resources: {}\n" +
" volumeMounts:\n" +
" - name: skywalking-8bf807be\n" +
" mountPath: /opt/skywalking\n" +
" terminationMessagePath: /dev/termination-log\n" +
" terminationMessagePolicy: File\n" +
" imagePullPolicy: IfNotPresent\n" +
" containers:\n" +
" - name: hello-spring\n" +
" image: 'swr.cn-north-4.myhuaweicloud.com/yummy-default/hello-spring-develop:77954'\n" +
" env:\n" +
" - name: SPRING_PROFILES_ACTIVE\n" +
" value: dev\n" +
" - name: EGG_SERVER_ENV\n" +
" value: dev\n" +
" - name: SERVER_SOURCE\n" +
" value: CCE\n" +
" - name: SWKAC_ENABLE\n" +
" value: 'true'\n" +
" - name: CCE_POD_NAME\n" +
" valueFrom:\n" +
" fieldRef:\n" +
" apiVersion: v1\n" +
" fieldPath: metadata.name\n" +
" - name: CCE_NAMESPACE\n" +
" valueFrom:\n" +
" fieldRef:\n" +
" apiVersion: v1\n" +
" fieldPath: metadata.namespace\n" +
" - name: CCE_POD_IP\n" +
" valueFrom:\n" +
" fieldRef:\n" +
" apiVersion: v1\n" +
" fieldPath: status.podIP\n" +
" - name: CCE_NODE_NAME\n" +
" valueFrom:\n" +
" fieldRef:\n" +
" apiVersion: v1\n" +
" fieldPath: spec.nodeName\n" +
" - name: test\n" +
" value: test\n" +
" - name: JAVA_TOOL_OPTIONS\n" +
" value: '-javaagent:/opt/skywalking/skywalking-agent.jar'\n" +
" - name: SW_AGENT_COLLECTOR_BACKEND_SERVICES\n" +
" value: 'skywalking-oap-rpc.default.svc.cluster.local:11800'\n" +
" - name: SW_AGENT_NAME\n" +
" value: hello-spring\n" +
" - name: SW_JDBC_TRACE_SQL_PARAMETERS\n" +
" value: 'true'\n" +
" - name: SW_DUBBO_COLLECT_CONSUMER_ARGUMENTS\n" +
" value: 'true'\n" +
" - name: SW_DUBBO_COLLECT_PROVIDER_ARGUMENTS\n" +
" value: 'true'\n" +
" resources:\n" +
" limits:\n" +
" cpu: '1'\n" +
" memory: 2Gi\n" +
" requests:\n" +
" cpu: 100m\n" +
" memory: 107374182400m\n" +
" volumeMounts:\n" +
" - name: secret-1\n" +
" mountPath: /test/test-secret.txt\n" +
" subPath: test-secret.txt\n" +
" - name: skywalking-8bf807be\n" +
" mountPath: /opt/skywalking\n" +
" livenessProbe:\n" +
" httpGet:\n" +
" path: /health\n" +
" port: 8080\n" +
" scheme: HTTP\n" +
" initialDelaySeconds: 5\n" +
" timeoutSeconds: 2\n" +
" periodSeconds: 10\n" +
" successThreshold: 1\n" +
" failureThreshold: 20\n" +
" readinessProbe:\n" +
" tcpSocket:\n" +
" port: 8080\n" +
" initialDelaySeconds: 10\n" +
" timeoutSeconds: 2\n" +
" periodSeconds: 30\n" +
" successThreshold: 1\n" +
" failureThreshold: 5\n" +
" lifecycle:\n" +
" preStop:\n" +
" exec:\n" +
" command:\n" +
" - /bin/sh\n" +
" - '-c'\n" +
" - java -jar /home/appuser/java_pre_stop-1.0.jar '/home/appuser/gc.hprof' >> upload_obs.log\n" +
" terminationMessagePath: /dev/termination-log\n" +
" terminationMessagePolicy: File\n" +
" imagePullPolicy: Always\n" +
" securityContext:\n" +
" capabilities:\n" +
" add:\n" +
" - NET_BIND_SERVICE\n" +
" drop:\n" +
" - ALL\n" +
" allowPrivilegeEscalation: false\n" +
" restartPolicy: Always\n" +
" terminationGracePeriodSeconds: 15\n" +
" dnsPolicy: ClusterFirst\n" +
" serviceAccountName: default\n" +
" serviceAccount: default\n" +
" automountServiceAccountToken: false\n" +
" nodeName: 10.88.171.106\n" +
" securityContext:\n" +
" runAsUser: 1000\n" +
" runAsGroup: 1000\n" +
" runAsNonRoot: true\n" +
" fsGroup: 1000\n" +
" imagePullSecrets:\n" +
" - name: default-secret\n" +
" affinity:\n" +
" nodeAffinity:\n" +
" requiredDuringSchedulingIgnoredDuringExecution:\n" +
" nodeSelectorTerms:\n" +
" - matchExpressions:\n" +
" - key: nodename\n" +
" operator: NotIn\n" +
" values:\n" +
" - gitlab-runner\n" +
" podAntiAffinity:\n" +
" preferredDuringSchedulingIgnoredDuringExecution:\n" +
" - weight: 1\n" +
" podAffinityTerm:\n" +
" labelSelector:\n" +
" matchExpressions:\n" +
" - key: app\n" +
" operator: In\n" +
" values:\n" +
" - hello-spring\n" +
" topologyKey: kubernetes.io/hostname\n" +
" schedulerName: default-scheduler\n" +
" tolerations:\n" +
" - key: node.kubernetes.io/not-ready\n" +
" operator: Exists\n" +
" effect: NoExecute\n" +
" tolerationSeconds: 300\n" +
" - key: node.kubernetes.io/unreachable\n" +
" operator: Exists\n" +
" effect: NoExecute\n" +
" tolerationSeconds: 300\n" +
" priority: 0\n" +
" dnsConfig:\n" +
" options:\n" +
" - name: single-request-reopen\n" +
" value: ''\n" +
" - name: timeout\n" +
" value: '2'\n" +
" enableServiceLinks: true\n" +
"status:\n" +
" phase: Running\n" +
" conditions:\n" +
" - type: Initialized\n" +
" status: 'True'\n" +
" lastProbeTime: null\n" +
" lastTransitionTime: '2021-09-30T03:18:17Z'\n" +
" - type: Ready\n" +
" status: 'True'\n" +
" lastProbeTime: null\n" +
" lastTransitionTime: '2021-09-30T03:18:45Z'\n" +
" - type: ContainersReady\n" +
" status: 'True'\n" +
" lastProbeTime: null\n" +
" lastTransitionTime: '2021-09-30T03:18:45Z'\n" +
" - type: PodScheduled\n" +
" status: 'True'\n" +
" lastProbeTime: null\n" +
" lastTransitionTime: '2021-09-30T03:18:15Z'\n" +
" hostIP: 10.88.171.106\n" +
" podIP: 172.20.3.52\n" +
" podIPs:\n" +
" - ip: 172.20.3.52\n" +
" startTime: '2021-09-30T03:18:15Z'\n" +
" initContainerStatuses:\n" +
" - name: skywalking-init-8bf807be\n" +
" state:\n" +
" terminated:\n" +
" exitCode: 0\n" +
" reason: Completed\n" +
" startedAt: '2021-09-30T03:18:17Z'\n" +
" finishedAt: '2021-09-30T03:18:17Z'\n" +
" containerID: 'docker://a1c314484630d088c2d89b0e36bcfb4a9d349bfaf29563b4b4b59d1fe63da611'\n" +
" lastState: {}\n" +
" ready: true\n" +
" restartCount: 0\n" +
" image: 'swr.cn-north-4.myhuaweicloud.com/joymo-archcenter/skywalking-agent:77743'\n" +
" imageID: 'docker-pullable://swr.cn-north-4.myhuaweicloud.com/joymo-archcenter/skywalking-agent@sha256:1952a45b4ad3aeb718c0bf849af10b576012d210ca28d40084b40c9fcf0b5d26'\n" +
" containerID: 'docker://a1c314484630d088c2d89b0e36bcfb4a9d349bfaf29563b4b4b59d1fe63da611'\n" +
" containerStatuses:\n" +
" - name: hello-spring\n" +
" state:\n" +
" running:\n" +
" startedAt: '2021-09-30T03:18:20Z'\n" +
" lastState: {}\n" +
" ready: true\n" +
" restartCount: 0\n" +
" image: 'swr.cn-north-4.myhuaweicloud.com/yummy-default/hello-spring-develop:77954'\n" +
" imageID: 'docker-pullable://swr.cn-north-4.myhuaweicloud.com/yummy-default/hello-spring-develop@sha256:3e492c06a40f8259ed6a4a37de136c1b9da9d6a260da854b93bb8c50bd4c95e1'\n" +
" containerID: 'docker://63c4c222224caf2f25de57d784baa64b5045ff8ba945508a525a27131d9ce9f7'\n" +
" started: true\n" +
" qosClass: Burstable\n";
}
package tech.yummy.devops.hotwheel.business.service.kubernetes.mockup;
import io.kubernetes.client.informer.SharedIndexInformer;
import io.kubernetes.client.informer.cache.Lister;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1Pod;
import lombok.extern.slf4j.Slf4j;
import mockit.Mock;
import mockit.MockUp;
import tech.yummy.devops.hotwheel.business.service.kubernetes.AppPodListLister;
import tech.yummy.devops.hotwheel.business.service.kubernetes.InformerService;
import static tech.yummy.devops.hotwheel.business.service.kubernetes.InformerService.APP_PODLIST_INDEXER;
/**
* @Author: [email protected]
* @Description: k8s informerservice 伪装类
* @Date: 2021/9/29 18:06
*/
@Slf4j
public final class MockInformerService extends MockUp {
@Mock
public static AppPodListLister getAppPodListLister(String clusterName, String namespace) {
MockSharedIndexInformer indexInformer = new MockSharedIndexInformer(V1Pod.class);
return new AppPodListLister<>(indexInformer.getIndexer(), namespace, APP_PODLIST_INDEXER);
}
@Mock
public static Lister getDeploymentLister(String clusterName, String namespace) {
MockSharedIndexInformer indexInformer = new MockSharedIndexInformer(V1Deployment.class);
return new Lister<>(indexInformer.getIndexer(), namespace);
}
}
package tech.yummy.devops.hotwheel.business.service.kubernetes.mockup;
import io.kubernetes.client.informer.ResourceEventHandler;
import io.kubernetes.client.informer.SharedIndexInformer;
import io.kubernetes.client.informer.cache.Indexer;
import io.kubernetes.client.openapi.models.V1Deployment;
import io.kubernetes.client.openapi.models.V1Pod;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
/**
* @Author: [email protected]
* @Description: shareIndex 伪造
* @Date: 2021/9/30 15:56
*/
public class MockSharedIndexInformer implements SharedIndexInformer {
public Class class2;
public MockSharedIndexInformer(Class class2) {
this.class2 = class2;
}
@Override
public void addIndexers(Map indexers) {
}
@Override
public Indexer getIndexer() {
Indexer indexer = new MockIndexerObject(class2);
return indexer;
}
@Override
public void addEventHandler(ResourceEventHandler handler) {
}
@Override
public void addEventHandlerWithResyncPeriod(ResourceEventHandler handler, long resyncPeriod) {
}
@Override
public void run() {
}
@Override
public void stop() {
}
@Override
public boolean hasSynced() {
return false;
}
@Override
public String lastSyncResourceVersion() {
return null;
}
}
利用 mockMvc 起了 http 服务真实触发调用
//业务代码
/**
* 文件上传
*
* @param file文件
* @return CommonAttachmentBO 附件BO对象
*/
@PostMapping(value = "/upload")
public ResultInfo upload(@RequestParam MultipartFile file) throws IOException {
ParameterCheckUtil.notNull(file, RetMsgConsts.JOB_SQL_FILE_EMPTY);
UploadFileDTO uploadFileDTO = CommonFileTools.transferToUploadFileDTO(file);
return ResultInfo.success(taskSqlFileService.upload(uploadFileDTO));
}
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
/**
* 在每次测试执行前构建mvc环境
*/
@BeforeEach
public void before() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
/**
* 文件上传
*/
@Test
void upload() {
try {
String url = "/axure/attachment/upload";
File file = File.createTempFile("test", ".txt");
MockMultipartFile multipartFile = new MockMultipartFile("uploadTest", file.getName(),
MediaType.TEXT_PLAIN_VALUE, new FileInputStream(file));
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.fileUpload(url)
.file(multipartFile).header(CommonConstant.AUTH_TOKEN, "123456"))
.andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
int status = response.getStatus();
String contentAsString = response.getContentAsString();
ResultInfo resultInfo = JSON.parseObject(contentAsString, ResultInfo.class);
log.info("状态码::" + status);
log.info("返回结果:" + JsonUtils.format(contentAsString));
Assertions.assertEquals(Boolean.TRUE, resultInfo.getSuccess());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 文件下载
*/
@Test
void download(){
try {
String requestUrl = "/common/file/get/20211004114755717168124020000001";
String outputFileUrl = "E:\\test2.sql";
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(requestUrl)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.header(CommonConstant.AUTH_TOKEN, "123456")
.accept(MediaType.MULTIPART_FORM_DATA_VALUE))
.andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
FileOutputStream file = new FileOutputStream(outputFileUrl);
file.write(response.getContentAsByteArray());
Assertions.assertNotNull(file);
} catch (Exception e) {
e.printStackTrace();
}
}
默认情况下,junit测试是在一个线程中串行执行的,从5.3开始支持并行测试。
首先需要在resources创建配置文件junit-platform.properties,添加配置如下参数:
#是否允许存在并发测试
junit.jupiter.execution.parallel.enabled = true
#设置单元测试默认执行方式:concurrent并发执行,same_thread 串行执行,默认same_thread
unit.jupiter.execution.parallel.mode.default = concurrent
#设置单元测试类执行方式:concurrent并发执行,same_thread 串行执行,
#不设置时根据junit.jupiter.execution.parallel.mode.default同值
junit.jupiter.execution.parallel.mode.classes.default = same_thread
配置的默认执行模式会作用于测试树上的每个节点,但是有几个需要主要的例外。
首先就是针对于测试声明周期的配置(默认是方法级别的)如果设置为类级别,则必须保证测试类是线程安全的,另外对于设置测试方法的执行顺序的话,这个与并发测试是互相矛盾的。
在以上这两种情况下,必须明确在测试类或方法上面添加注解@Execution(CONCURRENT),否则也不会按照并发模式进行测试的。
采用以上的模式,所有的测试类和测试方法都是并发来执行的。
单元测试类
@Slf4j
public class Service1Test {
@Test
public void method1(){
log.info("执行测试:Service1Test.method1,线程:"+Thread.currentThread().getName());
//empExtService.initEmpExtList();
Thread.sleep(3000)
Assert.assertTrue(true);
}
@Test
public void method2(){
log.info("执行测试:Service1Test.method2,线程:"+Thread.currentThread().getName());
//empExtService.initEmpExtList();
Assert.assertTrue(true);
}
}
当不开启并行测试时,执行结果为:
。。。.method1(Service1Test.java:26) - 执行测试:Service1Test.method1,线程:main
。。。.method2(Service1Test.java:33) - 执行测试:Service1Test.method2,线程:main
当开启后,方法并行时执行结果:(可看到执行的线程不是同个)
method1(Service1Test.java:26) - 执行测试:Service1Test.method1,线程:ForkJoinPool-1-worker-2
method2(Service1Test.java:33) - 执行测试:Service1Test.method2,线程:ForkJoinPool-1-worker-1
Maven-surefire-plugin 是一个用于mvn 生命周期的测试阶段的插件,可以通过一些参数设置方便的在junit下对测试阶段进行自定义。
Maven运行测试用例时,是通过调用maven的surefire插件并fork一个子进程来执行用例的。
在surefire版本2.14之前使用forkMode控制多进程,现在使用forkCount控制。
在surefire插件配置中添加如下配置:
需要注意的是,这边是以测试类为执行单位,并发执行。
(目前的执行环境:jmockit:1.46;junit-jupiter-api:5.4.0;junit-jupiter-engine:5.4.0;maven-surefire-plugin:3.0.0-M5)
<!--forkCount启动的fork的进程数 -->
3
<!--表示一个测试进程执行完了之后是杀掉还是重用来继续执行后续的测试 -->
true
和Junit4相比,Junit5框架更多在向测试平台演进。其核心组成也从以前的一个Junit的jar包更换成由多个模块组成。
参数源:
如果待测试的输入和输出是一组数据: 可以把测试数据组织起来 用不同的测试数据调用相同的测试方法
参数化测试和普通测试稍微不同的地方在于,一个测试方法需要接收至少一个参数,然后,传入一组参数反复运行。
JUnit5提供了一个@ParameterizedTest注解,用来进行参数化测试。
假设我们想对Math.abs()进行测试,先用一组正数进行测试:
@ParameterizedTest
@ValueSource(ints = { 0, 1, 5, 100 })
void testAbs(int x) {
assertEquals(x, Math.abs(x));
}
再用一组负数进行测试:
@ParameterizedTest
@ValueSource(ints = { -1, -5, -100 })
void testAbsNegative(int x) {
assertEquals(-x, Math.abs(x));
}
参数化测试的注解是@ParameterizedTest,而不是普通的@Test。
假设我们自己编写了一个StringUtils.capitalize()方法,它会把字符串的第一个字母变为大写,后续字母变为小写:
public class StringUtils {
public static String capitalize(String s) {
if (s.length() == 0) {
return s;
}
return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase();
}
}
要用参数化测试的方法来测试,我们不但要给出输入,还要给出预期输出。因此,测试方法至少需要接收两个参数:
@ParameterizedTest
void testCapitalize(String input, String result) {
assertEquals(result, StringUtils.capitalize(input));
}
现在问题来了:参数如何传入?
最简单的方法是通过 @MethodSource
注解,它允许我们编写一个同名的静态方法来提供测试参数:
@ParameterizedTest
@MethodSource()
void testCapitalize(String input, String result) {
assertEquals(result, StringUtils.capitalize(input));
}
static List testCapitalize() {
return List.of( // arguments:
Arguments.arguments("abc", "Abc"), //
Arguments.arguments("APPLE", "Apple"), //
Arguments.arguments("gooD", "Good"));
}
上面的代码很容易理解:静态方法testCapitalize()返回了一组测试参数,每个参数都包含两个String,正好作为测试方法的两个参数传入。
Junit5 支持对提供给 @ParameterizedTest 的参数进行扩展的原始转换。
例如,使用 @ValueSource(ints ={1,2,3}) 注释的参数化测试可以声明为不仅接受int类型的参数,而且接受long、float或double类型的参数。
例如,如果 @ParameterizedTest 声明类型TimeUnit的参数,而声明的源提供的实际类型是字符串,
则该字符串将自动转换为相应的 TimeUnit enum常量。
@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(TimeUnit argument) {
assertNotNull(argument.name());
}
与使用隐式参数转换不同,您可以使用@ConvertWith注释显式地指定一个ArgumentConverter来使用,如下面的示例所示。
@ParameterizedTest
@EnumSource(TimeUnit.class)
void testWithExplicitArgumentConversion(
@ConvertWith(ToStringArgumentConverter.class) String argument) {
assertNotNull(TimeUnit.valueOf(argument));
}
public class ToStringArgumentConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) {
assertEquals(String.class, targetType, "Can only convert to String");
return String.valueOf(source);
}
}
参数聚合(Argument Aggregation)
默认情况下,提供给@ParameterizedTest方法的每个参数都对应于单个方法参数。
因此,期望提供大量参数的参数源可能导致大方法签名。
在这种情况下,可以使用ArgumentsAccessor而不是使用多个参数。
使用这个API,可以通过传递给测试方法的单个参数访问提供的参数
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
Person person = new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, Gender.class),
arguments.get(3, LocalDate.class));
if (person.getFirstName().equals("Jane")) {
assertEquals(Gender.F, person.getGender());
}
else {
assertEquals(Gender.M, person.getGender());
}
assertEquals("Doe", person.getLastName());
assertEquals(1990, person.getDateOfBirth().getYear());
}
@RestController
@RequestMapping("/user")
public class UserController {
//读取session
@GetMapping("/get")
public String getsess(HttpServletRequest request) {
HttpSession session=request.getSession();
String username = (String)session.getAttribute("username");
System.out.println("session username:"+username);
if (username == null) {
return "";
} else {
return username;
}
}
//设置session
@GetMapping("/set")
public String setSess(@RequestParam("userName")String userName, HttpServletRequest request) {
HttpSession session=request.getSession();
session.setAttribute("username", userName);
return userName;
}
}
@AutoConfigureMockMvc
@SpringBootTest
class UserControllerTest {
@Autowired
private UserController userController;
@Autowired
private MockMvc mockMvc;
private static MockHttpSession session;
@BeforeAll
public static void setupMockMvc() {
session = new MockHttpSession();
session.setAttribute("username", "刘新漳");
}
@Test
@DisplayName("测试get用户名,有session")
void getTest() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/user/get")
.session(session)
.contentType(new MediaType("application", "x-www-form-urlencoded")))
.andReturn();
String content = mvcResult.getResponse().getContentAsString();
assertThat(content, equalTo("刘新漳"));
}
@Test
@DisplayName("测试get用户名,无session")
void getTestFail() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/user/get")
.contentType(new MediaType("application", "x-www-form-urlencoded")))
.andReturn();
String content = mvcResult.getResponse().getContentAsString();
assertThat(content, equalTo(""));
}
@Test
@DisplayName("测试set session")
void setTest() throws Exception {
String name="liu";
MvcResult mvcResult = mockMvc.perform(get("/user/set?userName="+name)
.session(session)
.contentType(new MediaType("application", "x-www-form-urlencoded")))
.andReturn();
String content = mvcResult.getResponse().getContentAsString();
assertThat(content, equalTo("liu"));
}
}
主要思路为启动一个本地Redis Server,模拟远程Redis服务。
由于依赖开源项目, 具体支持的指令如下,若执行不支持,可以使用MockUp方法或者自行扩展开源项目
https://github.com/fppt/jedis-mock/tree/master/src/main/java/com/github/fppt/jedismock/operations
添加Maven依赖
<!-- https://github.com/fppt/jedis-mock -->
com.github.fppt
jedis-mock
0.1.22
test
基类代码
package tech.yummy.devops.hotwheel.base;
import com.github.fppt.jedismock.RedisServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import tech.yummy.devops.hotwheel.infrastructure.redis.RedisCache;
import java.io.IOException;
import java.net.ServerSocket;
/**
* @Description: RedisServer 全局
* @Author: xxwu
* @Date: 2021/9/28 14:32
*/
@Slf4j
public class RedisServerHolder {
private static RedisServer redisServer;
private static RedisConnectionFactory redisConnectionFactory;
private static RedisTemplate redisTemplate;
private static RedisCache redisCache;
public static RedisServer getRedisServer() {
if(redisServer == null){
synchronized (RedisServerHolder.class){
if(redisServer == null){
// 随机获取未被占用端口
ServerSocket socket = null;
try {
long st = System.currentTimeMillis();
socket = new ServerSocket(0);
int port = socket.getLocalPort();
socket.close();
// 启动一个RedisServer
redisServer = new RedisServer(port);
redisServer.start();
log.info(" ======= 启动RedisServer成功, 耗时{}ms ======= ", System.currentTimeMillis() - st);
} catch (IOException e) {
log.error(" ======= 启动RedisServer出错 ======= ", e);
}
}
}
}
return redisServer;
}
public static RedisConnectionFactory getRedisConnectionFactory() {
if(redisConnectionFactory == null){
synchronized (RedisServerHolder.class){
if(redisConnectionFactory == null){
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
RedisServer redisServer = getRedisServer();
jedisConnectionFactory.setHostName(redisServer.getHost());
jedisConnectionFactory.setPort(redisServer.getBindPort());
redisConnectionFactory = jedisConnectionFactory;
}
}
}
return redisConnectionFactory;
}
public static RedisTemplate getRedisTemplate() {
if(redisTemplate == null) {
synchronized (RedisServerHolder.class) {
if (redisTemplate == null) {
RedisTemplate stringObjectRedisTemplate = new RedisTemplate<>();
// 配置连接工厂
stringObjectRedisTemplate.setConnectionFactory(getRedisConnectionFactory());
StringRedisSerializer serializer = new StringRedisSerializer();
stringObjectRedisTemplate.setHashKeySerializer(serializer);
stringObjectRedisTemplate.setKeySerializer(serializer);
redisTemplate = stringObjectRedisTemplate;
redisTemplate.afterPropertiesSet();
// 首次执行会消耗一些时间, 算预热
redisTemplate.opsForValue().set("xxxx","xxxx");
redisTemplate.opsForValue().get("xxxx");
}
}
}
return redisTemplate;
}
public static RedisCache getRedisCache() {
if(redisCache == null) {
synchronized (RedisServerHolder.class) {
if (redisCache == null) {
redisCache = new RedisCache();
redisCache.redisTemplate = getRedisTemplate();
}
}
}
return redisCache;
}
}
使用方法
package tech.yummy.devops.hotwheel.business.controller.app;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import tech.yummy.devops.hotwheel.base.RedisMockConfig;
import tech.yummy.devops.hotwheel.base.RedisServerHolder;
import tech.yummy.devops.hotwheel.infrastructure.redis.RedisCache;
/**
* @author jinjia.wu
* @date 2021/9/25
*/
@Slf4j
class RedisTest {
private RedisTemplate redisTemplate = RedisServerHolder.getRedisTemplate();
private RedisCache redisCache = RedisServerHolder.getRedisCache();
@Test
void templateTest() {
String key = "xxwu";
String setValue = "20210925";
// =========== value ==========
redisTemplate.opsForValue().set(key, setValue);
String getValue = (String) redisTemplate.opsForValue().get(key);
Assertions.assertThat(getValue)
.isNotBlank()
.isEqualTo(setValue);
// 删除key, 否则key已存在影响下面的测试
redisTemplate.delete(key);
// =========== hash ==========
redisTemplate.opsForHash().put(key, key, setValue);
getValue = (String) redisTemplate.opsForHash().get(key, key);
Assertions.assertThat(getValue)
.isNotBlank()
.isEqualTo(setValue);
redisTemplate.delete(key);
// =========== set ==========
redisTemplate.opsForSet().add(key, setValue);
Boolean member = redisTemplate.opsForSet().isMember(key, setValue);
Assertions.assertThat(member)
.isNotNull()
.isEqualTo(Boolean.TRUE);
redisTemplate.delete(key);
}
@Test
void redisCacheTest() {
String key = "xxwu";
String setValue = "20210925";
redisCache.setCacheObject(key, setValue);
String getValue = redisCache.getCacheObject(key);
Assertions.assertThat(getValue)
.isNotBlank()
.isEqualTo(setValue);
// 删除key, 否则key已存在影响下面的测试
redisCache.deleteObject(key);
// =========== hash ==========
redisCache.setCacheMapValue(key, key, setValue);
getValue = (String) redisCache.getCacheMapValue(key, key);
Assertions.assertThat(getValue)
.isNotBlank()
.isEqualTo(setValue);
redisCache.deleteObject(key);
}
}
package tech.yummy.devops.hotwheel.business.service.system.impl;
import cn.kubeclub.core.data.DataProvider;
import mockit.Injectable;
import mockit.Mock;
import mockit.MockUp;
import mockit.Tested;
import mockit.Verifications;
import org.assertj.core.api.Assertions;
import org.elasticsearch.search.aggregations.Aggregations;
import org.junit.jupiter.api.Test;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.TotalHitsRelation;
import org.springframework.data.elasticsearch.core.query.Query;
import tech.yummy.devops.hotwheel.business.pojo.ao.system.OperateLogAO;
import tech.yummy.devops.hotwheel.business.pojo.ao.system.OperateLogSearchAO;
import tech.yummy.devops.hotwheel.business.pojo.vo.system.OperateLogVO;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 操作日志 服务层处理 - ES 实现
*
* @author ruoyi
*/
public class SysOperLogEsServiceImplTest {
@Tested
private SysOperLogEsServiceImpl sysOperLogEsService;
@Injectable
private ElasticsearchRestTemplate elasticsearchTemplate;
@Test
void insertOperlog() {
OperateLogAO operateLogAO = DataProvider.anyObject(OperateLogAO.class);
sysOperLogEsService.insertOperlog(operateLogAO);
new Verifications() {
{
elasticsearchTemplate.save(operateLogAO);
times = 1;
}
};
}
@Test
void selectOperLogList() {
// 录制返回
new Expectations(){{
elasticsearchTemplate.search((NativeSearchQuery)any, OperateLogAO.class);
result = geneData();
}};
OperateLogSearchAO searchAO = DataProvider.anyObject(OperateLogSearchAO.class);
List list = sysOperLogEsService.selectOperLogList(searchAO);
Assertions.assertThat(list)
.isNotNull();
}
public static SearchHits geneData(){
// 随机返回0~3条
int max = 3;
int size = new Random().nextInt(max);
List> list = new ArrayList<>();
for(int i=0;i hit = new SearchHit<>(
DataProvider.anyObject(String.class),
0F,
null,
null,
ao
);
list.add(hit);
}
SearchHits hits = new SearchHits() {
@Override
public Aggregations getAggregations() {
return new Aggregations(new ArrayList<>());
}
@Override
public float getMaxScore() {
return 0;
}
@Override
public SearchHit getSearchHit(int index) {
return getSearchHits().get(index);
}
@Override
public List> getSearchHits() {
return list;
}
@Override
public long getTotalHits() {
return list.size();
}
@Override
public TotalHitsRelation getTotalHitsRelation() {
return TotalHitsRelation.EQUAL_TO;
}
};
return hits;
}
}
kafka-junit
spring-kafka-test
使用 spring-kafka-test
@EmbeddedKafka 注解会帮我们实例化一个EmbeddedKafkaBroker对象放到spring容器中
通过参数spring.kafka.bootstrap-servers确定要连接的kafka broker列表,初始化kafka配置时主要是拿到这个连接地址,即启动的broker的连接地址,该地址可用EmbeddedKafkaBroker对象的getBrokersAsString()方法获取。其他具体的配置可在kafka配置类中视项目情况指定。
依赖引入
org.springframework.kafka
spring-kafka-test
test
配置类
@ActiveProfiles("junit-test")
@Configuration
public class KafkaMockConfig {
@Autowired
// 获取broker对象,主要是通过该对象获取broker连接地址用于生产者、消费者配置。该配置是使用 @EmbeddedKafka 实例化并放到容器中的。
EmbeddedKafkaBroker embeddedKafkaBroker;
// 生产者配置 可以识别容器中的消费者,所以需要将测试的消费者也放到容器中
@Bean
public KafkaListenerAnnotationBeanPostProcessor kafkaListenerAnnotationProcessor() {
return new KafkaListenerAnnotationBeanPostProcessor();
}
@Bean(name = KafkaListenerConfigUtils.KAFKA_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME)
public KafkaListenerEndpointRegistry defaultKafkaListenerEndpointRegistry() {
return new KafkaListenerEndpointRegistry();
}
@Bean
ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
@Bean
public ConsumerFactory consumerFactory() {
Map props = new HashMap(8);
props.put("bootstrap.servers", embeddedKafkaBroker.getBrokersAsString());
props.put("group.id", "demoGroup");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "10");
props.put("session.timeout.ms", "60000");
props.put("key.deserializer", StringDeserializer.class);
props.put("value.deserializer", JsonDeserializer.class);
props.put("auto.offset.reset", "earliest");
JsonDeserializer deserializer = new JsonDeserializer();
// 添加授信包,防止消费消息反序列化时报错:包不被信任
deserializer.addTrustedPackages("*");
return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), deserializer);
}
@Bean
public KafkaTemplate kafkaTemplate() {
Map props = new HashMap(8);
props.put("bootstrap.servers", embeddedKafkaBroker.getBrokersAsString());
props.put("retries", 0);
props.put("batch.size", "16384");
props.put("linger.ms", 1);
props.put("buffer.memory", "33554432");
props.put("key.serializer", StringSerializer.class);
props.put("value.serializer", JsonSerializer.class);
return new KafkaTemplate(new DefaultKafkaProducerFactory<>(props));
}
// 消费者、生产者及其依赖都需要在容器中
@Bean
public DemoProducer demoProducer() {
return new DemoProducer();
}
@Bean
public CmdbMemberChangeConsumer cmdbMemberChangeConsumer() {
return new CmdbMemberChangeConsumer();
}
@Bean
public ICmdbUserRoleService cmdbUserRoleService() {
return new CmdbUserRoleServiceImpl();
}
@Bean
public CmdbUserRoleMapper cmdbUserRoleMapper() throws IOException {
CmdbUserRoleMapper cmdbUserRoleMapper = BaseDaoTestUtil.getMapper("mapper/cmdb/CmdbUserRoleMapper.xml", CmdbUserRoleMapper.class);
SqlSession sqlSession = BaseDaoTestUtil.getSqlSession();
Connection connection = sqlSession.getConnection();
// 创建 ScriptRunner,读取 SQL 脚本并执行
ScriptRunner runner = new ScriptRunner(connection);
runner.setErrorLogWriter(null);
runner.setLogWriter(null);
// 初始化SQL脚本
runner.runScript(Resources.getResourceAsReader("sql/CmdbUserRole.sql"));
return cmdbUserRoleMapper;
}
@Bean
public RedisCache redisCache() {
return RedisServerHolder.getRedisCache();
}
@Bean(name = "devopsHotwheelRedisTemplate")
public RedisTemplate redisTemplate() {
return RedisServerHolder.getRedisTemplate();
}
}
辅助类
public class DemoProducer {
@Autowired
KafkaTemplate kafkaTemplate;
public void sendCmdServiceChangeMessage() {
ServiceEventBO eventBO = new ServiceEventBO();
eventBO.setServiceName("hello-spring");
kafkaTemplate.send("devops-cmdb-service-event", eventBO);
}
}
测试类
// SpringExtension是JUnit多个可拓展API的一个实现,提供了对现存Spring TestContext Framework的支持
@ExtendWith(SpringExtension.class)
@EmbeddedKafka(topics = {"demoTopic"}) // 启动broker
@ImportAutoConfiguration(classes = KafkaMockConfig.class) // 只加载该配置类中的bean
@Slf4j
public class DemoMqTest {
@Autowired
DemoProducer demoProducer;
@Test
void cmdbMemberChangeConsumerTest() throws InterruptedException {
demoProducer.sendCmdServiceChangeMessage();
/**
* 休眠1秒,防止消息还没消费broker就shutdown了
* 这里仅为了测试,正常不应该这样去使用,会影响单测耗时
* 可改写EmbeddedKafkaCondition.afterAll方法,等待消息被消费后再销毁broker
*/
Thread.sleep(1000 * 1);
}
}
笔者的文章同时发布于 kubeclub
云原生技术社区,一个分享云原生生产经验,同时提供技术问答的平台,前往查看