网站首页 > 技术文章 正文
概述
TDD(Test-driven development) 测试驱动开发,简单点说就是编写测试,再编写代码。这是首要一条,不可动摇的一条,先写代码后写测试的都是假TDD。
测试驱动开发可以分为三个周期,周而复始,红灯-绿灯-重构。由以下几个步骤构成:
- 编写测试
- 运行所有测试
- 编写代码
- 运行所有测试
- 重构
- 运行所有测试
一开始编写测试,肯定通不过,红灯状态,进行代码编写,然后运行测试,测试通不过,测试通过,即变成绿灯。
测试不通过,或者需要重构代码,再次运行所有测试代码...
接下来通过一个简单的,一个RESTful请求的Spring boot web项目,演示和说明TDD的过程。
这个功能大致是这样的,一个simple元素有id和desc两个属性
用户发送GET请求http接口 http://localhost:8080/simples 返回所有的simple元素的json数组
1 技术工具
- JDK8+
- Spring Boot 2.1+
- maven or Gradle
- JPA
- JUnit 5+
- Mockito
- Hamcrest
一个常见的RESTful请求处理的MVC架构:
- 用户访问http url
- 通过Controller层接口
- Controller层调用Service的实现
- 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("#34;, 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模式。
- Arrange: 单元测试的第一步,需要进行必要的测试设置,譬如创建目标类对象,必要时,创建mock对象和其他变量初始化等等
- Action: 调用要测试的目标方法
- Assert: 单元测试的最后异步,检查并验证结果与预期的结果是否一致。
我整理的《最全Java高级架构面试知识点整理》已升级为2.0版本,200个知识点,178页。私信我“Java”获取。
猜你喜欢
- 2024-10-17 使用 Spock 编写高效简洁的单元测试
- 2024-10-17 程序员有福了!万字长文带你掌握SpringBoot所提供的测试解决方案
- 2024-10-17 如何使用Spring Boot提供的测试工具和注解。
- 2024-10-17 单元测试实践(Spring-boot+Junbit5+Mockito)
- 2024-10-17 Spring云原生实战指南:8 弹性和可扩展性
- 2024-10-17 SpringBoot 太强了,这些优势你需要了解
- 2024-10-17 如何使用Spring Cloud Contract(如何使用朋友的山姆卡)
- 2024-10-17 Spring Boot 开发离不开这些注解,快来学习啦!
- 2024-10-17 Spring Boot中文参考指南46.3.11、自动配置的Spring WebFlux测试
- 2024-10-17 一台不容错过的Java单元测试代码“永动机”
- 最近发表
- 标签列表
-
- cmd/c (64)
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- sqlset (64)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- chromepost (65)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- linux删除一个文件夹 (65)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)