没有嵌入式 数据库、Redis,谁会写单元测试

你是否有这样的窘境,单测执行完一次,下次再执行因为数据被污染了,只能重新构造数据,耗时耗力耗心。这篇文章将告诉你如何优雅的解决这类问题。

嵌入式中间件在单测的应用

单元测试不应该依赖真实环境的 中间件,而应该使用各种嵌入式中间件,例如嵌入式数据库,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发生率,对新同学更加友好

单元测试的原则

  1. 未经测试的代码一定会有问题,不测代码,出线上问题只是时间早晚的事。
  2. 根据业务重要程度,重要的业务代码优先添加单元测试。
  3. 其次根据业务的复杂程度, 业务复杂的代码优先添加单元测试
  4. 其次根据上线的紧急程度,决定单元测试的覆盖度。
  5. 抽象通用的模型,提炼通用的流程,保证要测试的代码足够精简,减少单元测试工作量
  6. 提炼经过充分测试的通用组件。
  7. 单元测试要在隔离的环境里,不可访问和污染测试环境。(例如使用嵌入式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;   
}
  1. 把项目中实际要被测试的类,放进到 ContextConfiguration classes 中。
  2. 把嵌入式数据库 注入进 Spring 上下文
  3. 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 和故障很多情况下都只是一行代码的问题。

相关推荐
2401_85763639几秒前
电商系统设计与实现:Spring Boot框架
数据库·spring boot·后端
码蜂窝编程官方2 分钟前
【含开题报告+文档+PPT+源码】基于Spring Boot智能综合交通出行管理平台的设计与实现
java·vue.js·spring boot·后端·spring
晚睡早起₍˄·͈༝·͈˄*₎◞ ̑̑17 分钟前
SpringBoot(三)
java·spring boot·后端
ApiHug19 分钟前
ApiSmart-QWen2.5 coder vs GPT-4o 那个更强? ApiSmart 测评
java·人工智能·ai·llm·通义千问·apihug·apismart
刘翔在线犯法22 分钟前
Scala例题
开发语言·后端·scala
anqi2722 分钟前
Scala 的List
开发语言·后端·scala
Word的妈呀23 分钟前
Scala的case class
开发语言·后端·scala
mit6.82443 分钟前
[Docker#5] 镜像仓库 | 命令 | 实验:搭建Nginx | 创建私有仓库
linux·后端·docker·云原生
XiaoLiuLB43 分钟前
Docker 指令详解:全面掌握容器化管理工具
java·tomcat·nio
武昌库里写JAVA1 小时前
mysql 几种启动和关闭mysql方法介绍
java·开发语言·算法·spring·log4j