Spring Boot web项目的TDD流程

目录
  • 概述
  • 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: 单元测试的最后异步,检查并验证结果与预期的结果是否一致。

    以上就是Spring Boot web项目的TDD流程的详细内容,更多关于Spring Boot web项目TDD的资料请关注其它相关文章!

    本文转自网络,如有侵权请联系客服删除。