你是否有这样的窘境,单测执行完一次,下次再执行因为数据被污染了,只能重新构造数据,耗时耗力耗心。这篇文章将告诉你如何优雅的解决这类问题。
嵌入式中间件在单测的应用
单元测试不应该依赖真实环境的 中间件,而应该使用各种嵌入式中间件,例如嵌入式数据库,Redis 等,嵌入式中间件只提供了 jdbc,redis 协议的接口实现,并不完全保证数据可靠性,也不保证性能。通过嵌入式中间件每次单测创建或修改的数据都不会污染真实环境, 数据每次都会被写入全新的数据库, 可保证每次单测执行前置条件都完全一致。(如果是测试环境,两次执行单测,数据库可能已经发生变化,导致两次测试结果不一致)
嵌入式中间件 提供了非常理想的隔离环境。保证单测的结果不会被历史数据影响。
H2 嵌入式数据库
提供了嵌入式 数据库实现,可以使用 jdbc 访问 H2,和访问 mysql 没有区别. H2 是内存数据库,当进程退出时,数据库也会被销毁,非常适合在单元测试中使用。
以下是配置 H2 数据库的步骤。其中 dataSource可以准备注入到 Spring 上下文,供 ORM框架例如 SpringJdbc,Hibernate, mybatis 使用。
配置 pom
js
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
springboot中也依赖了 H2,可以直接引用 SpringBoot,看是否存在 H2 相关的类H2AuthConfig
配置 dataSource
js
EmbeddedDatabaseBuilder builder= (new EmbeddedDatabaseBuilder())
.setType(EmbeddedDatabaseType.H2)
.setName("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;mode=MySQL")
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true);
builder.addScript("schema.sql");
DataSource dataSource = builder.build();
schema.sql
配置数据库 DML 语句,后续 mysql schema 变更都需要同步的 配置在本文件中。
js
SET MODE MySQL;
CREATE TABLE `activity` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '' COMMENT '名称',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题',
`descrip` varchar(512) NOT NULL DEFAULT '' COMMENT '描述',
`startTime` bigint(20) NOT NULL DEFAULT '0' COMMENT '活动开始时间',
`endTime` bigint(20) NOT NULL DEFAULT '0' COMMENT '活动结束时间',
`createdTime` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建时间',
`updatedTime` bigint(20) NOT NULL DEFAULT '0' COMMENT '更新时间',
`status` int(11) NOT NULL DEFAULT '0' COMMENT '状态',
`ldapId` varchar(32) NOT NULL DEFAULT '' COMMENT '创建人',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
###后续需求迭代,需要把 Alter table 等 DML语句加在后面。
嵌入式 Redis
嵌入式 Redis 也是在应用进程中启动 redis 服务,redis client 通过网络回环地址 localhost:port 访问。
配置 pom
js
<dependency>
<groupId>com.github.kstyrc</groupId>
<artifactId>embedded-redis</artifactId>
<version>0.6</version>
</dependency>
启动 Redis Server
js
@PostConstruct
public void startRedis() throws IOException {
redisServer = new RedisServer(PORT);
redisServer.start();
}
@PreDestroy
public void stopRedis() {
redisServer.stop();
}
配置 redisson Client
js
public RedissonClient createClient() throws IOException {
RedissonSingleServerConfig config = new RedissonSingleServerConfig();
config.setAddress(wrapSchemaAndPort(HOST));
setSingleServerConfig(config);
return super.createClient();
}
嵌入式 ElasticSerach
配置 pom
js
<dependency>
<groupId>pl.allegro.tech</groupId>
<artifactId>embedded-elasticsearch</artifactId>
<version>2.10.0</version>
</dependency>
创建 Es实例
js
EmbeddedElastic esCluster = EmbeddedElastic.builder()
.withElasticVersion("5.6.16")
.withSetting(PopularProperties.TRANSPORT_TCP_PORT, 19300)
.withSetting(PopularProperties.CLUSTER_NAME, "test-es")
.withSetting("http.cors.enabled", true)
.withSetting("http.cors.allow-origin", "*")
// 安装包下载路径
.withDownloadDirectory(new File("data"))
.build();
esCluster.start();
Thread.currentThread().join();
以上我们介绍了单元测试的通用工具,包括 mockito,和嵌入式工具,db, redis, es 等常见中间件。
后面聊聊为什么要写单元测试!还有如何写。
单元测试的目的和意义
单元测试对软件基本组成单元进行的测试,如函数(function、procedure)或一个类的方法(method) 进行测试。
单元测试关注的点 比较细致,和用户用例的全流程,全链路测试不同。单元测试是白盒测试,只关注于系统实现的方法 是不是正确的。
如果从服务划分上来说。单元测试测试的逻辑 仅限于本服务的业务逻辑。在假设被依赖服务的代码完全正确的情况, 测试 本服务的业务逻辑是否符合预期。
这样有以下好处
- 分工协作时,需要对代码的准确性作保证。在全链路联调之前 要测试 本团队 所负责模块的功能正确性。在全链路测不通的情况下 ,必须使用单元测试。
- 错误的代码,会导致错误的数据。为了避免测试阶段,业务逻辑的不稳定,给测试环境带来的数据污染,新加的代码要经过充分的单元测试检验,才可以发布到测试环境。
- 单元测试可以测试的更加精细,全链路测试 因为是黑盒测试,难免有很多测试分支难以覆盖,很多中间状态难以测试, 甚至会遇到两个 bug达到负负为正的尴尬场景。单元测试由研发完成,因为对代码实现足够清楚,测试会更充分,更好保证这样服务的可靠性。
- 单元测试 更好编写,比全链路测试实现更加简单。比人工测试更加高效。
- 研究表明线上问题主要发生于代码变更后,而 bug 更多是由新员工写的(因为暂时不熟悉业务)。 完整充分的单元测试可以最大程度降低新员工 bug发生率,对新同学更加友好
单元测试的原则
- 未经测试的代码一定会有问题,不测代码,出线上问题只是时间早晚的事。
- 根据业务重要程度,重要的业务代码优先添加单元测试。
- 其次根据业务的复杂程度, 业务复杂的代码优先添加单元测试
- 其次根据上线的紧急程度,决定单元测试的覆盖度。
- 抽象通用的模型,提炼通用的流程,保证要测试的代码足够精简,减少单元测试工作量
- 提炼经过充分测试的通用组件。
- 单元测试要在隔离的环境里,不可访问和污染测试环境。(例如使用嵌入式Mysql,嵌入式Redis,嵌入式 Es,等进程内运行的嵌入式中间件,不再需要访问测试环境中间件。)
单元测试的通用工具
Junit
junit 是 java 单元测试框架。可以帮助组织单元测试,集成 IDE 执行单测,定义单测执行扩展点,断言工具。
Junit常见注解
-
@Test:这个注释说明依附在 JUnit 的 public void 方法可以作为一个测试案例。
-
@Before:有些测试在运行前需要创造几个相似的对象。在 public void 方法加该注释是因为该方法需要在 test 方法前运行。
-
@After:如果你将外部资源在 Before 方法中分配,那么你需要在测试运行后释放他们。在 public void 方法加该注释是因为该方法需要在 test 方法后运行。
-
@BeforeClass:在 public void 方法加该注释是因为该方法需要在类中所有方法前运行。
-
@AfterClass:它将会使方法在所有测试结束后执行。这个可以用来进行清理活动。
-
@Ignore:这个注释是用来忽略有关不需要执行的测试的。
-
@Rule 对测试结果进行校验 最常见可以添加 timeout rule 参考 blog.csdn.net/weixin_3439...
tips: 每一个被 Test 标注的方法都是 隔离执行的。例如 以下的测试 场景,test 1,test2在同一个测试类中,同时执行两个方法时,Before 被执行了两边,list 也被初始化了两遍。 虽然两个方法在一个类中,执行时都是单独执行的。没有任何的共享数据。
js
private List<Integer> list = Lists.newArrayList();
@Before()
public void before() {
System.out.println("1");
list.add(1); // 每一次 执行到这里list 都是空的。
}
@Test
public void test1() {
ThreadUtils.sleepQuietly(6000);
}
@Test
public void test2() {
ThreadUtils.sleepQuietly(6000);
}
如何与 Spring集成
在测试和生产环境,我们要开启正式的 web 容器和 spring 容器,单元测试我们不必构造真实的环境,使用 Mock 环境即可。参考以下示例
js
@ContextConfiguration(classes = {MockWriterConfig.class, MockReaderConfig.class})
@RunWith(SpringRunner.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
ContextConfiguration 指定 Mock 的 Spring 上下文应该加载哪些类到 Spring 容器上下文中。这些类可以通过 Autowird 被加载。
Runwith 指定 SpringRunner 把 Junit 和 Spring 结合起来。
DirectiesContext 指定单测的隔离级别,一般情况下 每个单测方法都应该被隔离。使用AFTER_EACH_TEST_METHOD即可。
mockito
mockito 是非常强大的 mock 工具。可以使用 mockito mock 下游 rpc接口,其他模块接口等等,可以让开发者更加专注于测试特定的业务逻辑,不需要关心依赖的接口。
mockito 提供了三种 方法,将 java 实例注入进 Spring 上下文中。
三种 Bean 实例注入方法
- ContextConfiguration
将指定的实现类加载进 spring 上下文
- SpyBean
先对实现类创建一个实例,然后 mockito 对其进行 spy。然后加载进 spring 上下文
js
@SpyBean // 代理 OrderSerivice 实例,默认调用真实的方法, 被注入到 Spring 上下文
private OrderSerivice orderService;
- MockBean
针对该实现类或者接口 代理一个 空的实现类。然后加载进 spring 上下文。
js
@MockBean //mockBean 会 mock OrderCenterProxy 接口,提供空实现,并且被注入到 Spring 上下文
protected OrderCenterProxy orderCenterProxy;
以上三种方法都会将 bean 加载进 spring 上下文,其中 ContextConfiguration 最直接,创建实例加载进 spring。
重点说下 spyBean 和 mockBean的区别。 mockito 提供了两个方法 spy 和 mock 修饰java 对象
js
TestA tA = new TestA();
TestA tASpy = Mockito.spy(tA); // tASpy 对象是针对 tA 对象的一个代理类,该代理类将最终调用 TestA 的实现。
TestA tAMock = Mockito.mock(TestA.class); //使用 Mock 相当于把 TestA 类或接口 的真正实现 替换成虚拟方法,虚拟方法为空实现。
Mock:对函数的调用均执行mock(即虚假函数),不执行真正部分。
Spy:对函数的调用均执行真正部分。
为什么要 Spy和 Mock
spy 和 mock 后的对象,可以使用 Mockito 对代理对象 进行修饰。
js
Mockito.doAnswer(x -> {
log.info("mock 方法...");
return null;
}).when(testA).test(); // 重写 TestA 的 test方法。
如果 testA 是 真正的实现类对象 即使用 new TestA()创建的。那么将无法使用 Mockito 重写实现逻辑。
spy 和 mock 的真正区别是 实现类是调用真实函数亦或是 虚拟的空函数。 但只要使用mockito对 实例对象进行修饰就必须使用spy和 mock 修饰,使用 new Object()方法
创建的实例 ,无法被 Mockito 修饰。
常见示例
Mock 下游服务
例如我们依赖 订单服务,通常我们会依赖 getOrder, searchOrder 等接口。可以使用 mockito 返回特定的构造的订单数据。而不是访问 真实的订单服务。
js
@MockBean //mockBean 会 mock OrderCenterProxy 接口,提供空实现,并且被注入到 Spring 上下文
protected OrderCenterProxy orderCenterProxy;
Mockito.when(orderCenterProxy.queryByCondition(Mockito.any())) //Mock 订单服务的接口,该接口在正式代码里被调用。
.thenAnswer((x) -> {
TOrderFullInfoQueryReq req = x.getArgumentAt(0, TOrderFullInfoQueryReq.class); // 获取入参
TOrderFullInfoQueryResponse response = new TOrderFullInfoQueryResponse();
//返回 Mock 的数据。
Map<Long, TOrderFullInfo> map = orderId2OrderMap.entrySet().stream().
filter((entry) -> entry.getValue().getOrderInfo().getUserId() == req.getUserId())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
response.setOrderId2OrderFullInfoMap(map);
return response;
});
Mockito.when(methondCall).thenAnswer(); // 可以使用 Mockito 的 when+ thenAnswer mock 方法的返回值。
那么如何注入orderCenterProxy 呢? OrderCenterPrxoy 只是一个接口,应该使用 MockBean 注解 注入,这样一个完全空实现的 OrderCenterProxy 被注入 Spring。后续使用 Mockito 订制 OrderCenterProxy 的业务逻辑。可以看到上面的例子中 orderCenterProxy.queryByCondition 方法 被 mockito 重写了一遍。
Spy:代理并 Mock自身服务的组件
设想以下场景 OrderService 是 本服务的组件,提供订单相关操作
js
orderService.validate()
orderService.create();
orderService.postCreate(); // postCreate 负责发布消息,刷新 Redis, Es,Hbase 等操作。
订单服务在创建订单后,postCreate 执行了一系列操作,此时我们只想测试 valiate 和 create ,不关心postCreate的业务逻辑,我们需要 mock 掉 postCreate
Spy 可以对 OrderService 进行 mock, 例如 Mockito.doNothing().when().methodCall(),把方法调用变成空实现。 默认情况,spy 后的实例会调用 实际方法。
js
@SpyBean // 代理 OrderSerivice 实例, 被注入到 Spring 上下文
private OrderSerivice orderService;
@Before
public void init(){
Mockito.doNothing().when(orderService).postCreate()
}
使用 Mockito.doNothing().when().methodCall() 之后,orderService.postCreate 方法会变成空方法。不会再影响测试流程。
使用 SpyBean 在测试 自身组件时,可以保证大部分调用都是实际方法调用,当需要订制某个方法时,通过 mockito 订制。(使用 ContextConfiguration注解加载的类实例,不能被 Mockito 修饰)
后续介绍一下单元测试的代码组织。
单元测试的代码组织
单元测试是模拟 使用方或者客户端 调用 被测试方法。需注意以下部分
单元测试代码的组成
处理数据依赖
- 单测的输入数据,例如被测试方法的输入参数。
- 被测试方法隐式依赖的数据, 例如被测试方法 依赖了订单, 商品,等模型的数据,例如被测试方法 消费订单支付事件,消费逻辑里包含查询订单和商品信息,所以在执行测试方法前,需要构造订单和商品
- 配置信息,例如营销系统中配置了很多活动信息等等。
接口Mock
- 例如查询订单和商品信息 需要 mock 订单和商品中心的接口,可以使用 Mockito.when(proxy.getOrder()).thenAnswer(....);等进行处理,参考 mockito 部分
单测核心流程
- 单测的核心流程代码是可以复用的,当入参不同时,构造多个测试 case, 核心通用流程可以考虑代码复用。( 一般情况下, 单元测试需要覆盖多个测试样例,例如方法入参有变化,隐形数据依赖有变化,此时需要构造多个测试 case)
结果验证
- 验证测试结果是否符合预期
如何Mock 数据 和 Mock 接口
mock 数据是单元测试必不可少的工作,考虑电商场景中,需要依赖订单和商品信息。
js
Order order=orderProxy.getOrderById(xx)
Product product = productProxy.getProductById(order.getProductId())) //瞎编的代码,非实际业务场景,不用纠结 order 和 product 的关系。
以上代码再项目中经常需要用到,这时就需要构造数据并且 Mock实现下游服务的接口
使用 MockBean 提供下游服务接口 Mock空实现
js
//把项目中实际要被测试的和依赖的类,放进到 classes 中。
@ContextConfiguration(classes = {
MockWriterConfig.class, MockReaderConfig.class,//把嵌入式数据库 注入进 Spring 上下文
ActivityService.class, UserSourceServiceImpl.class,
RetryRecommendService.class, OrderSearchService.class, OrderEventHandler.class,
})
@RunWith(SpringRunner.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public abstract class BaseTest {
@MockBean // OrderProxy是订单下游服务提供的接口,需要使用 MockBean 提供空实现。
protected OrderProxy proxy;
}
- 把项目中实际要被测试的类,放进到 ContextConfiguration classes 中。
- 把嵌入式数据库 注入进 Spring 上下文
- OrderProxy是订单下游服务提供的接口,需要使用 MockBean 提供空实现。
BaseTest可以作为所有单测类的基类
构造数据并 Mock 接口
可以把构造数据和 mock 接口的代码放到一个类里, 方便组织。
例如提供 MockService 类
js
@Service //在 maven/java/test包下,可以自动被注入进 Spring,不需要 ContextConfiguration指定
public class MockService {
@Autowired
private OrderProxy orderProxy;
前面提到了 BaseTest 中使用 @MockBean 注入, 在 MockService 中就可以使用 autowired 注入了。
提供创建订单和商品的TestUtil 方法
js
public TOrderFullInfo createOrder(long userId, List<Integer> productIds) {
TOrderFullInfo orderFullInfo = new TOrderFullInfo();
orderFullInfo.setOrderInfo(new TOrderInfo().setId(ID_GENERATOR.incrementAndGet())
.setUserId(userId));
List<TOrderItemInfo> itemInfos = productIds.stream().map(productIds -> {
TOrderItemInfo itemInfo = new TOrderItemInfo();
itemInfo.setId(ID_GENERATOR.incrementAndGet());
itemInfo.setOrderId(orderFullInfo.getOrderInfo().getId());
itemInfo.setProductId(productId);
itemInfo.setUserId(userId);
return itemInfo;
}).collect(Collectors.toList());
for (TOrderItemInfo itemInfo : itemInfos) {
itemInfoMap.put(itemInfo.getId(), itemInfo);
}
orderFullInfo.setOrderItemList(itemInfos);
return orderFullInfo;
}
- 提供 createOrder 工具方法 构造订单数据。
- 对于订单项,由于项目中需要使用订单项 id 查询订单项,可以使用 Map结构存储 OrderItemId->OrderItem的映射。
使用 Map,List 等容器提供存储实现。
使用 Map,List,Set等常见的容器,存储资源的 映射关系。
js
@Service //在 maven/java/test包下,可以自动被注入进 Spring,不需要 ContextConfiguration指定
public class MockService {
@Autowired
private OrderProxy orderProxy;
@PostConstruct// 实例化后被执行。
public void mockOrderProxy() {
Mockito.when(orderProxy.getOrderItemsV2ByItemIds(Mockito.anyList()))
.thenAnswer((x) -> {
List<Long> itemIds = x.getArgumentAt(0, List.class);
//使用 Map,List等作为单测存储数据的容器。createOrder方法想 map 中插入数据,mock 方法负责从 map 中取数据
return itemIds.stream().map(itemInfoMap::get).collect(Collectors.toMap(OrderItemV2::getId, Function.identity()));
});
}
createOrder 可以把订单信息存储进 map, Mockito可以对具体的接口进行 mock, 从 map中拿出相应的订单信息。用来模拟从RPC 接口中获取。
复用单测核心流程代码
例如营销减系统在 计算优惠时,客户端调用时的处理流程都是固定的,但是依赖的数据(订单商品信息,活动信息)都是变化的。此时可以复用统一的调用流程
新建了一个 CommonRecommendFlow, 输入参数包括 要下单的商品,订单是否支付等参数
run()方法定义了客户端调用推荐方法的通用流程
- 根据商品推荐优惠信息
- 校验推荐信息是否符合预期(hookRecommend)
- 用户下单
- 再次确认 优惠信息没有变更(hookConfirm)
- 用户支付订单
- 返回订单信息
通用流程中的扩展点 hookRecommend, hookConfirm等方法会校验 优惠信息,由于每次推荐结果都是不同的,所以校验逻辑也是不同的。所以子类实现这两个方法,实现自己的校验逻辑。
这是一个标准的 模板方法设计模式的应用。
js
//通用抽象类,定义了客户端调用被测试方法的 通用流程。
abstract class CommonRecommendFlow {
List<LessonDigest> lessonDigests;
boolean pay;
boolean confirm;
private List<TMatchedActivityDetailDTO> details;
public CommonRecommendFlow(List<LessonDigest> lessonDigests, boolean confirm, boolean pay) {
this.lessonDigests = lessonDigests;
this.pay = pay;
this.confirm = confirm;
}//通用流程
public TOrderFullInfo run() {
details = recommendService.recommend(USER_ID, toProductIds(lessonDigests)); //推荐优惠信息
hookRecommend(details);// 校验优惠信息,需要子类实现。
//创建订单,存储进 Map 中。mockito mock 订单下游的getOrder 接口,从 Map 中取数据。
TOrderFullInfo order = mockRecommend.createOrder(USER_ID, CollectionUtilsEx.mapToList(lessonDigests, LessonDigest::getId));
if (!confirm) {
return order;
}
if (CollectionUtilsEx.isNotEmpty(details)) {
List<TOrderDiscountInfoDTO> preDiscountInfos = buildDiscountInfo(details, order);
//confirm 优惠信息,(用户下单后到支付成功的这个时间段,优惠信息可以发生了变化,在用户支付前需要确认优惠信息是否变更。
//如果变更了则终止支付流程 )
List<Long> unmatchedProductIds = confirmRecommendService.confirmRecommend(USER_ID, preDiscountInfos, true);
hookConfirm(unmatchedProductIds);
}
if (!pay) {
return order;
}
//支付订单,payOrder 里会调用 OrderPaidConsumer,模拟消息被消费者消费,同时测试了消息消费逻辑。
mockRecommend.payOrder(order);
return order;
}
public abstract void hookRecommend(List<TMatchedActivityDetailDTO> details);
protected void hookConfirm(List<Long> unmatchedProductIds) {
Assert.assertTrue(CollectionUtilsEx.isEmpty(unmatchedProductIds));
}
public TMatchedActivityDetailDTO getDiscountsByActivityId(long activityId) {
return details.stream().filter(d -> d.getActivity().getId() == activityId).findAny().orElse(null);
}
}
单元测试中如何使用呢?
js
List<LessonDigest> lessonDigests = ImmutableList.of(lessonDigest3, lessonDigest4, lessonDigest5);
TOrderFullInfo order = new CommonRecommendFlow(lessonDigests) {
@Override
public void hookRecommend(List<TMatchedActivityDetailDTO> details) {
//命中第二个 level
assertRecommendSuccess(details, activity, 2);
//测试第二个梯度不可以 满足
assertRecommendFail(details, activity, 3);
}
@Override
public void hookConfirm(List<Long> unmatchedProductIds) {
Assert.assertTrue(CollectionUtilsEx.isEmpty(unmatchedProductIds));
}
}.run();
通用模板类 只需要输入购买的商品信息。
- 通用模板类 只需要输入购买的商品信息。
- 实现hookRecommend,hookConfirm 校验优惠信息
代码不好测试 怎么办
想尽一切办法测试
代码不好测试 是正常现象,一般遇到这种情况可以考虑以下解决办法
- 在正式代码中提供一些方法 专门给测试方法用
- 例如营销某些推荐流程里 包含库存扣减流程,虽然下单可以成功,但我们不清楚库存是否扣减成功。此时就会让库存接口提供单独的查询接口 供单测方法测试库存是否扣减
- 缩小测试范围。测试一个大的,服务的业务流程非常复杂,我们可以先测试小模块。先解决小问题
- 写正式代码的时候,就要写好如何测试,组件的边界要定义清晰。避免业务逻辑杂糅在一起,导致 测试时无法 单独分割出来进行测试。
没有时间写单测
要形成写单测的习惯,这样每次上线的质量会得到有效保证。(千万不要完全依赖 QA,线上出了问题还是 RD 背锅哦~)
没有时间写单测也是一个实际问题。这就要求我们在排期时一定 要顶住压力,在排期时把单测的时间加进去。(告诉他们,没有单测,就没有质量,不写测试,线上出了问题,谁负责?)
一般开发和单测的时间 比例 至少在 3:1,即三天开发的代码,至少要有 1 天用来写单测。
态度最重要,当你想尽一切办法写单元测试时, 问题一定可以被解决!单测绝不是 不可完成的任务
写在后面
单元测试的最终目的是为了保证代码质量,避免代码 bug 对业务系统造成重大影响。
当新同学修改代码,代码修改量比较小,单元测试能让代码修改 更加安全,可靠。每次上线也会更加有信心。
在实践中,单元测试会遇到各种各样的困难
-
没有时间写单测
-
很难构造数据
-
依赖了特别多的下游服务
-
业务逻辑非常复杂,很难测试
无论有多少困难,针对稳定性要求高的核心服务,核心业务,开发人员应该不择手段,想尽一切办法 充分测试。
办法总会有。
任何一行代码都应被重视,都应被测试覆盖。线上的 bug 和故障很多情况下都只是一行代码的问题。