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的资料请关注我们其它相关文章!

(0)

相关推荐

  • 浅谈测试驱动开发TDD之争

    前言 在历史上有很多精彩绝伦的神仙打架,比如数学界的牛顿和莱布尼茨关于微积分的旷世之争:比如量子物理中的爱因斯坦和波尔的紫禁之巅:比如足球里的梅西和C罗的旗鼓相当难分高下:又比如滴滴和快滴之间触目惊心的烧钱大战--而在软件行业中,也同样有神仙打架的名场面,那就不得不提的是2014年的那场--测试驱动开发(TDD)之争. 比赛的红方是David Heinemeier Hansson,蓝方是Kent Beck.David Heinemeier Hansson 由于名字较长简写成DHH,Ruby on

  • SpringBoot生产环境和测试环境配置分离的教程详解

    第一步:项目中资源配置文件夹(resources文件夹)下先新增测试环境application-dev.yml和application-prod.yml两个配置文件,分别代表测试环境配置和生产环境配置 第二步:在application.yml配置文件中设置如下配置(PS:active后定义的名字要和配置文件-后的名字一致,如下则系统执行application-dev.yml) spring: profiles: active: dev 第三步:启动项目 启动方式一:idea中 springboo

  • Springboot Mybatis-Plus数据库单元测试实战(三种方式)

      单元测试长久以来是热门话题,本文不会讨论需不需要写单测,可以看看参考资料1,我个人认为写好单测应该是每个优秀开发者必备的技能,关于写单测的好处在这里我就不展开讨论了,快速进入本文着重讨论的话题,如何写好数据库单测.   为什么要写数据库单测? 相信大家是不是有这样类似的经历,在写完复杂的sql语句后,自信满满的提测,发现很大一部分Bug都是因为sql语句出现问题了,要么少写逗号,要么漏了字段,悔不当初哇,为啥写完不多测测呢!   没关系!这就教你如何写数据库单测,让你轻松告别数据库相关bug

  • SpringBoot+redis配置及测试的方法

    1.创建项目时选择redis依赖 2.修改配置文件,使用SpringBoot就避免了之前很多的xml文件 2.1学过redis的同学都知道这个东西有集群版也有单机版,无论哪个版本配置起来都很简单 2.1.1首先找到配置文件 2.1.2然后配置集群版,直接在配置文件内编辑即可 2.1.3配置单机版 3.测试 找到测试文件夹,自动注入redis模板 4.分别测试操作String和Hash类型的数据 4.1操作String @Test public void testString(){ //操作Str

  • 解决SpringBoot 测试类无法自动注入@Autowired的问题

    原来的测试类的注解: @RunWith(SpringRunner.class) @SpringBootTest 一直没法自动注入,后来在@SpringBootTest, 加入启动类Application后就可以了 @RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class) 补充:spring boot项目单元测试时,@Autowired无法注入Service解决方式 首先确认: 测试类所在包名要和启动类一致

  • SpringBoot对Controller进行单元测试的实现代码 附乱码解决方案

    Controller代码 package com.keafmd.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; /** * Keafmd * * @ClassName: Hel

  • QUnit jQuery的TDD框架

    在讨论jQuery TDD之前,我们先来了解下什么才是一个标准的TDD框架.作为标准的TDD框架,必须满足这么几个要求: 1. 即使测试脚本出错了也要能继续运行接下来的脚本 2. 能够不依赖被测试代码写测试用例,即使代码没有实现也可以先写测试用例 3. 能够显示详细的错误信息和位置 4. 能够统计通过和未通过的用例的数量 5. 有专门的可视化界面用于统计和跟踪测试用例 6. 易于上手,通过一些简单的指导就可以马上开始写测试代码. 以上这些要求QUnit都做到了, 这也是我推荐QUnit的原因.

  • 使用@SpringBootTest注解进行单元测试

    概述 @SpringBootTest注解是SpringBoot自1.4.0版本开始引入的一个用于测试的注解.基本用法如下: 1. 添加Maven依赖 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <parent> <groupId>org.springframework.boot</gro

  • SpringBoot内置tomcat调优测试优化

    问题 怎么配置springBoot 内置tomcat,才能使得自己的服务效率更高呢? 基础配置 Spring Boot 能支持的最大并发量主要看其对Tomcat的设置,可以在配置文件中对其进行更改.我们可以看到默认设置中,Tomcat的最大线程数是200,最大连接数是10000. 这个不同SpringBoot 版本可能有所细微差别.本文测试基于Springboot 2.0.7.RELEASE 默认配置 /** * Maximum amount of worker threads. */ priv

  • Springboot集成JUnit5优雅进行单元测试的示例

    为什么使用JUnit5 JUnit4被广泛使用,但是许多场景下使用起来语法较为繁琐,JUnit5中支持lambda表达式,语法简单且代码不冗余. JUnit5易扩展,包容性强,可以接入其他的测试引擎. 功能更强大提供了新的断言机制.参数化测试.重复性测试等新功能. ps:开发人员为什么还要测试,单测写这么规范有必要吗?其实单测是开发人员必备技能,只不过很多开发人员开发任务太重导致调试完就不管了,没有系统化得单元测试,单元测试在系统重构时能发挥巨大的作用,可以在重构后快速测试新的接口是否与重构前有

  • Android开发笔记之:对实践TDD的一些建议说明

    最近部分采用了TDD的方法来开发一个模块,小有收获特此总结一下:1. TDD的基本原则TDD的最核心思想就是先明确需求,且用代码的方式量化,明确需求标准,然后进行编码实现以达成由代码测试来衡量的标准.那么它要求,先把需要标准写出来,每次只写一个.编码实现通过达到,并刚好满足这个标准.这样一点一点的迭代.这样有三个好处:一个是先明确标准,不至于我们迷失主题,偏离方向.有标准在检测,保证代码是正确的.仅满足当前测试,不至于过早优化和过度设计.2. TDD的难点难点在于如何设计这个测试标准,1)让它足

  • 详解SpringBoot项目的创建与单元测试

    前言   Spring Boot 设计之初就是为了用最少的配置,以最快的速度来启动和运行 Spring 项目.Spring Boot使用特定的配置来构建生产就绪型的项目. Hello World 1.可以在 Spring Initializr上面添加,也可以手动在 pom.xml中添加如下代码∶ <dependency> <groupId>org.springframework.boot</groupId> <artifactId>Spring-boot-s

随机推荐