Spring Boot web项目的TDD流程

作者:瞩目学码 时间:2023-07-06 16:10:10 

目录
  • 概述

  • 1 技术工具

  • 2 构建Spring Boot工程

  • 3 开始编写测试和代码

    • 1 Controller

    • 2 Service

    • 3 Repository

  • 4 总结

    概述

    测试驱动开发可以分为三个周期,周而复始,红灯-绿灯-重构。由以下几个步骤构成:

    1. 编写测试

    2. 运行所有测试

    3. 编写代码

    4. 运行所有测试

    5. 重构

    6. 运行所有测试

    一开始编写测试,肯定通不过,红灯状态,进行代码编写,然后运行测试,测试通不过,测试通过,即变成绿灯。

    测试不通过,或者需要重构代码,再次运行所有测试代码...

    接下来通过一个简单的,一个RESTful请求的Spring boot web项目,演示和说明TDD的过程。

    这个功能大致是这样的,一个simple元素有id和desc两个属性

    用户发送GET请求http接口 http://localhost:8080/simples 返回所有的simple元素的json数组

    1 技术工具

    1. JDK8+

    2. Spring Boot 2.1+

    3. maven or Gradle

    4. JPA

    5. JUnit 5+

    6. Mockito

    7. Hamcrest

    一个常见的RESTful请求处理的MVC架构:

    1. 用户访问http url

    2. 通过Controller层接口

    3. Controller层调用Service的实现

    4. Service接口通过Repsoitory层访问数据库,并最终返回数据给用户

    2 构建Spring Boot工程

    构建一个Spring Boot Maven工程,并添加所需的依赖

    参考依赖如下


       <properties>
           <java.version>1.8</java.version>
           <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
           <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
           <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
       </properties>

    <dependencies>
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-data-jpa</artifactId>
           </dependency>
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-web</artifactId>
           </dependency>
           <dependency>
               <groupId>com.h2database</groupId>
               <artifactId>h2</artifactId>
               <scope>runtime</scope>
           </dependency>
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-test</artifactId>
               <scope>test</scope>
               <exclusions>
                   <exclusion>
                       <groupId>org.junit.vintage</groupId>
                       <artifactId>junit-vintage-engine</artifactId>
                   </exclusion>
               </exclusions>
           </dependency>
       </dependencies>
       <dependencyManagement>
           <dependencies>
               <dependency>
                   <groupId>org.springframework.boot</groupId>
                   <artifactId>spring-boot-dependencies</artifactId>
                   <version>${spring-boot.version}</version>
                   <type>pom</type>
                   <scope>import</scope>
               </dependency>
           </dependencies>
       </dependencyManagement>

    3 开始编写测试和代码

    1 Controller

    首先编写测试Controller层的测试,test代码区创建一个测试类,SimpleControllerTest

    添加两个注解 @ExtendWith和@WebMvcTest。

    然后添加一个MockMvc对象,用来模拟mvc的请求。单元测试中,每个模块应当独立的测试,实际调用链中,Controller依赖Service层,因为当前测的是Controller层,对于Service层的代码则进行mock,这可以使用一个注解

    @MockBean

    整个代码如下


    @ExtendWith({SpringExtension.class})
    @WebMvcTest
    public class SimpleControllerTest {

    @Autowired
       MockMvc mockMvc;

    @MockBean
       private SimpleService simpleService;

    }

    SimpleService不存在,编译不通过,红灯,则创建它。

    如是创建一个SimpleService作为Service层的Spring bean。


    @Service
    public class SimpleService {

    }

    然后编写请求/simples http请求的测试代码


       @Test
       void testFindAllSimples() throws Exception {
           List<Simple> simpleList = new ArrayList<>();
           simpleList.add(new Simple(1L,"one"));
           simpleList.add(new Simple(2L,"two"));
           when(simpleService.findAll()).thenReturn(simpleList);

    mockMvc.perform(MockMvcRequestBuilders.get("/simples")
                   .contentType(MediaType.APPLICATION_JSON)).andExpect(jsonPath("$", hasSize(2))).andDo(print());
       }

    when then结构来自Mockito框架,when表示了执行的条件,then用于执行验证,这里的操作对simpleService.findAll方法结果进行了mock,这里 在这一层不需关心的simpleService的真实实现。后面perform方法 mock了 /simples的请求。

    这里报错,红灯,接下来编写Simple类的实现。


    @Entity
    public class Simple {
       private Long id;
       private String desc;

    public Simple(String desc) {
           this.desc = desc;
       }

    }

    因为simpleService.findAll方法未定义,所以还是报错的,红灯。接下来保持简单,给SimpleService创建一个findAll方法。


       public List<Simple> findAll() {
           return new ArrayList<>();
       }

    编译问题都解决了,下面开始运行测试代码。

    报错,


    java.lang.AssertionError: No value at JSON path “$”

    还是红灯,这是因为我们mock的perform 没有存在。接下来创建一个SimpleController类作为RestController,并编写/simples请求的接口。


    @RestController
    public class SimpleController {

    @Autowired
       private SimpleService simpleService;

    @GetMapping("/simples")
       public ResponseEntity<List<Simple>> getAllSimples() {
           return new ResponseEntity<>(simpleService.findAll(), HttpStatus.OK);

    }
    }

    再次运行测试用例,测试都通过了,绿灯。

    2 Service

    接下来让我们关注Service层的代码测试,test代码区创建一个SimpleServiceTest类。该类对下一层Repository依赖,同样的,创建一个Repository的mock对象。


    @SpringBootTest
    public class SimpleServiceTest {

    @MockBean
       private SimpleRepository simpleRepository;

    }

    编译报错,红灯,需要创建一个SimpleRepository。


    @Repository
    public interface SimpleRepository extends JpaRepository<Simple,Long> {
    }

    以上,创建SimpleRepository作为实体Simple类对象的JPA存储服务。

    编写测试代码


       @Test
       void testFindAll() {
           Simple simple = new Simple("one");
           simpleRepository.save(simple);
           SimpleService simpleService = new SimpleService(simpleRepository);
           List<Simple> simples = simpleService.findAll();
           Simple entity = simples.get(simples.size() - 1);
           assertEquals(simple.getDesc(),entity.getDesc());
           assertEquals(simple.getId(),entity.getId());
       }

    继续解决编译报错的问题,SimpleService没有构造方法。添加Repository 并注入bean。


    @Service
    public class SimpleService {

    private SimpleRepository simpleRepository;

    public SimpleService(SimpleRepository simpleRepository) {
           this.simpleRepository = simpleRepository;
       }

    public List<Simple> findAll() {
           return new ArrayList<>();
       }
    }

    这里插播一个题外话,为啥Spring推荐通过构造方法的方式注入bean, 方便编写可测试代码是个重要原因。

    运行测试用例,会继续报错,这里是因为JPA hibernate没有和实体类对象交互,需要添加主键注解,默认构造函数 getter/setter 重新编写实体类的代码。


    @Entity
    public class Simple {
       @Id
       @GeneratedValue(strategy = GenerationType.IDENTITY)
       private Long id;
       private String desc;

    public Simple() {
       }

    public Simple(String desc) {
           this.desc = desc;
       }

    // 省略 getter/setter ...

    }

    修改完毕之后 运行测试用例 依然失败,findAll方法测试未通过,修改SimpleService的findAll方法,调用 jpa repository的findAll方法


       public List<Simple> findAll() {
           return simpleRepository.findAll();
       }

    现在再次运行测试用例,测试通过。

    3 Repository

    前面已经通过了TDD去实现Controller层和Service层的代码,理论上Repository实现了JPA的接口,我们没有做任何代码的编写,应该不需要进行测试,但是我们不确定数据是否通过数据库进行了存储和查询。为了保证数据库存储,将真正的JPA respoitory实例注入的Service对象中。修改@MockBean 为@Autowired。


    @SpringBootTest
    public class SimpleServiceTest {

    @Autowired
       private SimpleRepository simpleRepository;

    @Test
       void testFindAll() {
           Simple simple = new Simple("one");
           simpleRepository.save(simple);
           SimpleService simpleService = new SimpleService(simpleRepository);
           List<Simple> simpleEntities = simpleService.findAll();
           Simple entity = simpleEntities.get(simpleEntities.size() - 1);
           assertEquals(simple.getDesc(),entity.getDesc());
           assertEquals(simple.getId(),entity.getId());
       }
    }

    创建H2 database配置。

    classpath下 创建schema.sql和data.sql,创建表和插入一点数据。


    #************H2  Begin****************
    #创建表的MySql语句位置
    spring.datasource.schema=classpath:schema.sql
    #插入数据的MySql语句的位置
    spring.datasource.data=classpath:data.sql
    # 禁止自动根据entity创建表结构,表结构由schema.sql控制
    spring.jpa.hibernate.ddl-auto=none

    spring.jpa.show-sql=true

    schema.sql


    DROP TABLE IF EXISTS simple;

    CREATE TABLE `simple` (
    id  BIGINT(20) auto_increment,
    desc varchar(255)
    );

    data.sql


    INSERT INTO `simple`(`desc`) VALUES ('test1');
    INSERT INTO `simple`(`desc`) VALUES ('test2');

    继续运行测试用例,所有用例都测试通过,浏览器直接访问localhost:8080/simples

    返回data.sql插入的数据


    [
       {
    "id": 1,
    "desc": "test1"
    },
    {
    "id": 2,
    "desc": "test2"
    }
    ]

    4 总结

    以上是一个完整的TDD开发流程的演示,每一个模块的测试具备独立性,当前模块中,可以mock其他模块的数据。关于测试用例的结构,遵循的是AAA模式。

    1. Arrange: 单元测试的第一步,需要进行必要的测试设置,譬如创建目标类对象,必要时,创建mock对象和其他变量初始化等等

    2. Action: 调用要测试的目标方法

    3. Assert: 单元测试的最后异步,检查并验证结果与预期的结果是否一致。

    来源:https://juejin.cn/post/6967590865896226852

    标签:Spring,Boot,TDD
    0
    投稿

    猜你喜欢

  • 基于Ant路径匹配规则AntPathMatcher的注意事项

    2021-11-19 03:58:16
  • 基于jstl 标签的使用介绍

    2021-10-01 13:48:36
  • 使用 C# 动态编译代码和执行的代码

    2023-07-22 23:46:27
  • Java中this关键字的用法详解

    2023-10-04 05:05:53
  • 详解Java异常处理中finally子句的运用

    2023-11-29 10:10:30
  • 面试官:Java中new Object()到底占用几个字节

    2022-02-09 19:04:00
  • Android 类似UC浏览器的效果:向上滑动地址栏隐藏功能

    2023-01-29 05:01:41
  • AJAX中Get请求报错404的原因以及解决办法

    2021-07-03 05:41:07
  • 深入浅析Android消息机制

    2023-07-26 03:20:02
  • C#如何通过T4自动生成代码详解

    2021-12-21 15:43:46
  • Springboot Mybatis Plus自动生成工具类详解代码

    2022-09-17 12:01:57
  • java线程并发控制同步工具CountDownLatch

    2022-09-02 12:18:06
  • SuperSocket封装成C#类库的步骤

    2023-09-05 02:36:33
  • Java开发实现猜拳游戏

    2023-09-27 03:21:46
  • C#中的程序集和反射介绍

    2021-09-22 12:17:51
  • Java中notify和notifyAll的区别及何时使用

    2022-07-03 02:08:43
  • C#微信公众号开发之用户管理

    2023-04-13 02:40:12
  • Java基于享元模式实现五子棋游戏功能实例详解

    2023-07-23 16:20:18
  • C#分布式事务的超时处理实例分析

    2022-06-16 03:11:28
  • BeanDefinition基础信息讲解

    2022-03-23 23:48:37
  • asp之家 软件编程 m.aspxhome.com