没有嵌入式 数据库、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 和故障很多情况下都只是一行代码的问题。

相关推荐
冒泡的肥皂7 分钟前
JAVA-WEB系统问题排查闲扯
java·spring boot·后端
yuhaiqiang8 分钟前
聊聊我的开源经历——先做个垃圾出来
后端
茂桑17 分钟前
Idea集成AI:CodeGeeX开发
java·ai·intellij-idea
jackson凌31 分钟前
【Java学习笔记】运算符
java·笔记·学习
追逐时光者40 分钟前
6种流行的 API 架构风格,你知道几种?
后端
咸鱼求放生44 分钟前
网络请求只到前端页面接口报200并到不到后端接口
java
只会AI搜索得coder1 小时前
sqlite3 sqlcipher加密,解密,集成springboot,读取sqlcipher加密工具
java·spring boot·sqlite
小麦果汁吨吨吨1 小时前
Flask快速入门
后端·python·flask
kinlon.liu1 小时前
SpringBoot整合Redis限流
spring boot·redis·后端
cg50171 小时前
Spring Boot 中的自动配置原理
java·前端·数据库