单元测试实践(SpringCloud+Junit5+Mockito+DataMocker)

网上看过一句话,单元测试就像早睡早起,每个人都说好,但是很少有人做到。从这么多年的项目经历亲身证明,是真的。
这次借着项目内实施单元测试的机会,记录实施的过程和一些总结经验。

项目情况

首先是背景,项目是一个较大型的项目,多个团队协作开发,采用的是SpringCloud作为基础微服务的架构,中间件涉及Redis,MySQL,MQ等等。新的起点开始起步,团队中讨论期望能够利用单元测试来提高代码质量。单元测试的优点很多,但是我觉得最终最终的目标就是质量,单元测试代码如果最终没有能够提高项目质量,说明过程是有问题或者团队没有真正接纳方法,不如放弃来节省大家的开发时间。
一说到单元测试大家肯定会先想起TDD。TDD(Test Dirven Development,测试驱动开发)是以单元测试来驱动开发的方法论。

  1. 开发一个新功能前,首先编写单元测试用例
  2. 运行单元测试,全部失败(红色)
  3. 编写业务代码,并且使对应的单元测试能够通过(绿色)
  4. 时刻维护你的单元测试,使其始终可运行

一个团队一开始就直接实施TDD的可能性是比较小的,因为适合团队的研发流程、测试底层框架封装、单元测试原则与规范都还没有敲定或者摸索出最佳的实践。直接一开始就完整实施,往往过程会变形,最终目标慢慢会偏离正轨,整个团队也不愿意再接受单元测试。所以建议是逐步开始,让团队切身能够体会到单元测试带来的收益再慢慢加码。

我们的项目基础技术架构是基于SpringCloud,做了一些基础的底层封装。项目之间的调用都是基于Feign,各个项目都是规范要提供各自的Feign接口以及Hystrix的FallbackFactory。我们将对于外部的调用都是封装在底层的service中。

单元测试范围

一个项目需要实施单元测试,首先要界定(或者说澄清)单元测试负责的范围。最常见的疑惑就是与外部系统或者其他中间件的关联,单元测试是否要实际的调用其他中间件/外部系统。
我们先来看看单元测试的定义:

Unit tests are typically automated tests written and run by software developers to ensure that a section of an application (known as the "unit") meets its design and behaves as intended.

单元测试首先应当是自动化的,由开发者编写,为了保证代码片段(最小单元)是按照预期设计实现的。我们理解就是说单元测试要保障的是项目(代码片段逻辑)自身按照设计意图正确执行,所以确认了单元测试的范围仅限于单个项目内部,因此要尽量屏蔽所有的外部系统或中间件。代码的业务逻辑覆盖80%-90%,其他部分(工具类等)不做要求。
我们项目涉及到了一些中间件(Mysql,Redis,MQ等),但是更多涉及到的内部其他支撑系统。用项目内的实际情况我们当前定义的单元测试覆盖的范围就是,单元测试从controller作为入口,尽量覆盖到controller和service所有的方法与逻辑,所有的外部接口调用全部mock,中间件尽量使用内存中间件进行mock。

单元测试基础框架

既然项目是基于SpringCloud,那测试肯定会引入基础的spring-boot-test,底层的测试框架选择是junit。
Junit主流还是junit4(Github地址)最新版本是4.12(2014年12月5日),现在最新的是junit5(JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage)。junit5正式版本的发布日期是2017年9月11日,目前最新的版本是5.5.2(2019年9月9日)。我们项目底层选择了junit5。
目前,在 Java 阵营中主要的 Mock 测试工具有 Mockito,JMock,EasyMock 等。我们选择了Mockito,这个是没有经过特别的选型。简单比较之后选择了比较容易上手并且能够满足当前需求的一款。
redis使用了redis-mock (ai.grakn:redis-mock:0.1.6)
数据库自然是使用h2(com.h2database:h2:1.4.192)(不过在一期项目我们主要服务编排,没有涉及到数据库的实例)
模拟数据生成参考了jmockdata(com.github.jsonzou:jmockdata:4.1.2),但是做了一些小小的调整增加了一些其他的类型

另外,Mockito不支持static的的方法的mock,要使用PowerMock来模拟。但是PowerMock似乎现在还不支持junit5,我们没有使用。

单元测试实施

基本框架搭建完毕,基本就进入了编码阶段。第一期的编码,我们实际上还是先写了业务代码,然后再写单元测试。接下来就详细介绍一下单元测试类的结构。这里给的示例仅仅是我们在实践过程中有使用到的,并非junit5的完整注解或者使用讲解,具体需要了解大家可以参考官网

单元测试基本结构

先看一下头部的几个注解,这些都是Junit5的

// 替换了Junit4中的RunWith和Rule
@ExtendWith(SpringExtension.class)
//提供spring依赖注入
@SpringBootTest 
// 运行单元测试时显示的名称
@DisplayName("Test MerchantController")
// 单元测试时基于的配置文件
@TestPropertySource(locations = "classpath:ut-bootstrap.yml")
class MerchantControllerTest{
    private static RedisServer server = null;

    // 下面三个mock对象是由spring提供的
    @Resource
    MockHttpServletRequest request;

    @Resource
    MockHttpSession session;

    @Resource
    MockHttpServletResponse response;

    // junit4中 @BeforeClass
    @BeforeAll
    static void initAll() throws IOException {
        server = RedisServer.newRedisServer(9379); 
        server.start();
    }


    // junit4中@Before
    @BeforeEach
    void init() {
        request.addHeader("token", "test_token");
    }

    // junit4中@After
    @AfterEach
    void tearDown() {
    }

    // junit4中@AfterClass
    @AfterAll
    static void tearDownAll() {
        server.stop();
        server = null;
    }

}

这些都是比较基础的注解,基本也和junit4一一对应。这里没有太多可说的,可以看到我们在初始化方法中加载了虚拟的redis服务器,在前置方法中设置了Header的值

单元测试的主体方法

我们测试的主要的就是MerchantController这个类,这个类下面还有一层service方法。先看一下大概的代码印象。

    @Resource
    MerchantController merchantController;

    @MockBean
    private IOrderClient orderClient;

    @Test
    void getStoreInfoById() {
        MockConfig mockConfig = new MockConfig();
        mockConfig.setEnabledCircle(true);
        mockConfig.sizeRange(2, 5);
        MerchantOrderQueryVO merchantOrderQueryVO = Mock.mock(MerchantOrderQueryVO.class);
        StoreInfoDTO storeInfoDTO = Mock.mock(StoreInfoDTO.class,mockConfig);

        Mockito.when(orderClient.bizInfoV3(Mockito.any())).thenReturn(R.data(storeInfoDTO));
        Mockito.when(orderClient.getOrderCount(Mockito.any())).thenReturn(R.data(merchantOrderQueryVO));

        R<StoreInfoBizVO> r = merchantController.getStoreInfoById();

        assertEquals(r.getData().getAvailableOrderCount(), merchantOrderQueryVO.getOrderNum());
        assertEquals(r.getData().getId(), storeInfoDTO.getId());
        assertEquals(r.getData().getBranchName(), storeInfoDTO.getBranchName());
    }

    @ParameterizedTest
    @ValueSource(ints = {1, 0})
    void logoutCheck(Integer onlineValue) {
        MockConfig mockConfig = new MockConfig();
        mockConfig.setEnabledCircle(true);
        mockConfig.sizeRange(2, 5);
        MerchantOrderQueryVO merchantOrderQueryVO = Mock.mock(MerchantOrderQueryVO.class);
        StoreInfoDTO storeInfoDTO = Mock.mock(StoreInfoDTO.class,mockConfig);
        storeInfoDTO.setOnline(onlineValue);
        Mockito.when(orderClient.bizInfoV3(Mockito.any())).thenReturn(R.data(storeInfoDTO));
        Mockito.when(orderClient.getOrderCount(Mockito.any())).thenReturn(R.data(merchantOrderQueryVO));

        R r = merchantController.logoutCheck();

        if (1==onlineValue) {
            assertEquals(ResourceAccessor.getResourceMessage(
                    MerchantbizConstant.USER_LOGOUT_CHECK_ONLINE), r.getMsg());
        } else {
            assertEquals(ResourceAccessor.getResourceMessage(
                    MerchantbizConstant.USER_LOGOUT_CHECK_UNCOMPLETED), r.getMsg());
        }
    }

    @ParameterizedTest
    @CsvSource({"1,Selma,true", "2,Lisa,true", "3,Tim,false"})
    void forTest(int id,String name,boolean t) {
        System.out.println("id="+id+" name="+name+" tORf="+t);
        merchantController.forTest(null);
    }

首先看变量的部分,这里给了两个例子,一个注解是@Resource,这个是让spring来注入的。另外一个是@MockBean,这就是Mockito提供的,并且结合下面的Mockito.when方法。
接下来看方法体,我将方法主体分为三部分:

  1. Mock数据与方法
    使用Mock拦截底层的外部接口方法,并且返回随机的Mock数据(大部分数据可以使用DataMocker生成,有一些特殊有限制的,可以手动生成)。
  2. 测试方法执行
    执行目标测试方法(基本都是一行,直接调用目标方法并且返回结果)
  3. 结果断言
    根据业务逻辑预期进行断言的编写(这部分基本上没有自动化的方式,因为断言的条件和业务逻辑相关只能手动编写)

这样写下来是基本逻辑的验证,还有内部有分支逻辑,如何验证?
代码当中实际上也提到了,就是junit5提供的@ParameterizedTest注解,配合@ValueSource, @CsvSource来使用,分别可以设置指定类型或者复杂类型到单元测试中,使用方法的参数接受,定义测试不同的分支。

单元测试的执行

单元测试的执行实际上分成2部分:

  1. IDE中我们要去验证单元测试是否能够成功执行
  2. CI/CD作为执行的先决条件保障

IDE可以直接指定测试框架,我们选择junit5直接生成单元测试代码,可以直接在测试包或者类上右键执行单元测试。这个方法可以作为我们开发过程中验证待遇测试有效性的手段。但是真正要能在生产开发流程中更好的体现单元测试的价值,还是需要持续集成的支持,我们项目使用的是jenkins。依赖是Maven,以及maven-surefire-plugin插件。要特别注意一点,由于junit5还比较新,所以maven-surefire-plugin插件支持junit5还是稍微有点特殊的,参考官网说明。我们需要引入插件:

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0-M3</version>
            <configuration>
                <excludes>
                    <exclude>some test to exclude here</exclude>
                </excludes>
            </configuration>
        </plugin>

这样在jenkins构建时就会执行单元测试,如果单元测试失败,不会触发构建后操作(Post Steps)。

总结

目前我们的项目中,单元测试的应用还在第一期,但是投入在上面的时间和精力,实际上到实际开发时间的2-3倍。因为涉及到基础框架的搭建,新框架的引入整合,底层开发编写测试代码的审核,团队的培训等等。我预计在后期,成熟的框架和流程支持下,覆盖核心业务代码的单元测试耗时应该能到实际开发工时的50%-80%左右。但是这部分的投入是能够减少测试以及线上的问题发生的概率,节省了修复的时间。
团队目前还不能完全习惯单元测试的节奏,目前带来的直接益处还不够明显,但是一个好的习惯的养成,还是需要管理者投入精力同时从上而下的推动的。
后期应该对于单元测试的执行还有一些调整或改进,而且对其概念、流程等方面应该也会有更深入和实际的理解。届时还会再次整理,并且分享给大家。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Sure, here's an outline of how you can implement the shipping REST APIs using Spring Boot, Spring MVC, JPA/Batis, MySQL, and JWT: 1. Set up a Spring Boot project with the necessary dependencies for Spring MVC, JPA/Batis, MySQL, and JWT. You can use Spring Initializr to generate the project structure and add the dependencies. 2. Define the domain model for the shipping application, including entities such as Order, Product, Customer, and ShippingAddress. Map the entities to database tables using JPA annotations or MyBatis mapper XML files. 3. Implement the repository layer to perform CRUD operations on the database using JPA or MyBatis. You can use Spring Data JPA or MyBatis-Spring to simplify the implementation. 4. Define the REST API endpoints for the shipping application using Spring MVC annotations. Use JWT for authentication and authorization of the API endpoints. 5. Implement the service layer to perform business logic operations such as calculating shipping costs, validating orders, and processing payments. Use dependency injection to inject the repository and other services into the service layer. 6. Write unit tests to ensure that the application logic is working correctly. You can use JUnit and Mockito to write the tests. 7. Deploy the application to a server and test the API endpoints using a tool such as Postman. Here's some example code to get you started: ```java @RestController @RequestMapping("/api/orders") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/") public ResponseEntity<Order> createOrder(@RequestBody Order order) { Order createdOrder = orderService.createOrder(order); return ResponseEntity.ok(createdOrder); } @GetMapping("/{id}") public ResponseEntity<Order> getOrder(@PathVariable("id") Long orderId) { Order order = orderService.getOrder(orderId); return ResponseEntity.ok(order); } // Other API endpoints for updating and deleting orders } @Service public class OrderService { @Autowired private OrderRepository orderRepository; public Order createOrder(Order order) { // Calculate shipping costs and validate the order order.setShippingCosts(10.0); order.setTotalPrice(order.getProducts().stream() .mapToDouble(Product::getPrice) .sum() + order.getShippingCosts()); return orderRepository.save(order); } public Order getOrder(Long orderId) { return orderRepository.findById(orderId) .orElseThrow(() -> new NotFoundException("Order not found")); } // Other service methods for updating and deleting orders } @Repository public interface OrderRepository extends JpaRepository<Order, Long> { } ``` This code defines a REST API endpoint for creating orders and getting orders by ID. The order creation logic is implemented in the OrderService class, which calculates shipping costs and saves the order to the database using the OrderRepository interface. JWT authentication and authorization can be added to the API endpoints using Spring Security.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值