1.测试驱动开发的好处
测试驱动开发(Test-Driven Development,TDD)是一种不同于传统软件开发流程的新型的开发方法,它要求在编写某个功能的代码之前,先编写测试用例。TDD 的过程通常遵循以下几个步骤:
A.红阶段(编写测试用例):开发人员首先根据当前要解决的问题,编写一个或多个测试用例,描述代码应该如何工作。这些测试用例通常包含预期的输入和预期的输出。这个阶段被称为“红阶段”,因为此时的测试是失败的,因为尚未有实际代码来满足测试要求。
B.绿阶段(编写最小代码以通过测试) :开发人员现在开始编写最少量的代码,以满足刚刚编写的测试用例。目标是使测试用例通过,即测试变为绿色。这可能涉及编写基本的骨架代码或最小功能实现。
C.重构阶段(重构代码): 在测试用例通过之后,开发人员会进行代码重构,以改进代码的结构、可读性和性能,同时保持测试用例的通过状态。重构是一种保证代码质量的实践。
这个过程就是 TDD 的循环。重复这个循环,每次新增一个小的功能或修复一个问题,都要先编写一个测试用例,然后编写足够的代码使其通过,最后再进行代码重构。这样的迭代过程可以帮助开发人员逐步构建出功能齐全且质量高的代码。
TDD 的目标是通过持续的测试和重构,确保代码在开发过程中始终保持可靠、健壮和易于维护。同时,TDD 也强调了编写可测试代码的重要性,因为每个新功能都需要通过测试用例进行验证。
测试驱动开发有下面这些好处:
更高的代码质量: TDD鼓励开发人员思考各种不同情况下的代码行为,因此编写的代码更加健壮、可靠。由于测试用例覆盖了各种情况,减少了潜在的错误。
更快的反馈循环: 在TDD中,测试用例会先于实际代码编写,这使得开发人员能够迅速获得关于代码正确性的反馈。如果测试用例失败,开发人员可以立即修复问题,确保代码在早期阶段就被纠正。
设计改进: TDD 鼓励开发人员将代码划分为更小、更可测试的模块,这有助于更好的软件设计。同时,通过持续编写测试用例,可以促使开发人员思考如何更好地组织代码结构和逻辑。
防止代码腐化: 随着时间推移,代码库可能会变得复杂且难以维护。TDD 有助于防止代码腐化,因为每次更改都伴随着相应的测试用例,确保新的更改不会破坏现有的功能。
减少调试时间: TDD 可以帮助开发人员在编写代码时捕捉错误,而不是在代码已经完成后再进行调试。这减少了调试的时间和成本。
持续集成和部署: TDD 支持持续集成和持续部署流程,因为有了全面的测试套件,可以更自信地自动化测试和部署代码。
更好的文档: 测试用例本身充当了代码的文档,说明了代码应该如何工作和被使用。这有助于新的开发人员理解和维护代码。
总之,测试驱动开发可以提供更高的代码质量、更快的反馈循环、更好的设计、更少的错误和更快的开发速度。
2.测试驱动的痛点
测试驱动开发(TDD)尽管在很多情况下被认为是一种有益的软件开发方法,但在实际应用中可能会面临一些挑战,这些挑战可能是导致它在某些公司中没有流行起来的原因之一:
文化和习惯: 很多公司已经有了一套固定的开发流程和文化,转变到TDD可能需要改变开发团队的习惯和工作方式,这可能会遇到抵抗。
学习曲线: TDD 对开发人员来说可能需要一些时间来适应,因为他们需要学会如何编写测试用例、如何设计可测试的代码结构以及如何在开发过程中持续进行测试。
项目时间压力: 在一些项目中,时间可能会非常紧迫,开发人员可能会感到没有足够的时间来编写测试用例,这可能导致他们选择跳过TDD步骤。
管理层的期望: 有时管理层可能更关注项目的交付时间,而不是代码质量。这可能导致开发人员在追求速度的同时忽略了测试的重要性。
团队技能水平: 如果开发团队在TDD方面的技能水平较低,他们可能会感到不自信,担心测试用例的编写会增加工作难度。
项目类型: 有些项目可能更注重快速原型、实验性开发,TDD 可能不适用于这些场景。
已有代码库: 对于已经存在的庞大代码库,引入TDD可能会面临一些困难,需要在现有代码基础上添加测试用例。
团队规模: 在小规模的团队中,可能更容易推行TDD,因为协调和沟通相对较容易。但在大规模团队中,可能需要更多的协调和培训。
尽管存在这些挑战,许多公司仍然认识到TDD的好处,并在逐步采用或融入其中。成功采用TDD需要团队的合作、培训、适应时间和管理支持。在一些公司中,可能会结合TDD的核心原则,例如持续集成、持续测试和自动化测试,以达到更好的代码质量和开发效率。
3.两全其美
从测试驱动开发的好处和痛点可以看出,测试驱动开发确实是个好东西,但往往因为编写测试代码的代价导致开发团队没有很好的实践它。有没有两全其美的办法,在不增加开发负担的同时很好的践行测试驱动开发呢?
在码农网的领域类详细设计中,我们可以轻松添加各种测试用例值,快速生成测试代码。如下图所示:
生成测试代码如下:
package com.bamasmiles.goods;
import io.quarkus.test.junit.QuarkusTest;
import jdk.jfr.Description;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.*;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import static io.restassured.RestAssured.given;
import static io.restassured.http.ContentType.JSON;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
/**
* API资源(ProductResource)的接口测试
* @author 码农云创
* @date 2023/08/15
*/
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ProductResourceTest {
@ConfigProperty(name = "api_gateway")
String API_GATEWAY;
@Inject
TokenUtils tokenUtils;
ExpectedDTO expectedDTO;
List<ExpectedDTO> expectedDTOS;
HashMap map;
static List<HashMap> list;
static String access_token = "";
/**
* 测试预备数据
*/
public void setPrepareData(){
}
/**
* 提交新增商品信息的请求-测试用例
*/
@Test
@Description("测试:提交新增商品信息的请求")
@DisplayName("测试:提交新增商品信息的请求")
@Order(1)
public void testCreate() {
if(access_token.isEmpty()){
access_token = tokenUtils.getAccessToken();
}
// 创建测试预备数据
this.setPrepareData();
/**
* =================== 合法用例 ===================
*/
list = new ArrayList<>();
// 合法用例
map = new HashMap();
map.put("name", "君东君乖垂岂软龙帘畅");
map.put("price", "2167.15");
map.put("quantity", "9530");
list.add(map);
// 合法用例
map = new HashMap();
map.put("name", "试社设还迈呢伶线纽汁详固屈肯治狐灶");
map.put("price", "6325.55");
map.put("quantity", "8183");
list.add(map);
// 合法用例
map = new HashMap();
map.put("name", "邻坝式财庆服底尖朽汤");
map.put("price", "1822.17");
map.put("quantity", "3420");
list.add(map);
for (int i = 0; i < list.size(); i++) {
given().
contentType(JSON).
body(list.get(i)).
header("Authorization", "Bearer " + access_token).
when().
//log().all().
post(API_GATEWAY + "/api/admin/product/create").
then().
log().all().
statusCode(201);
}
/**
* =================== 非法用例(数据校验不合格) ===================
*/
expectedDTOS = new ArrayList<>();
// 商品信息ID:必须为null [非法值:主键ID故意传参]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("productId", "0");
map.put("name", "君东君乖垂岂软龙帘畅");
map.put("price", "2167.15");
map.put("quantity", "9530");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品信息ID:必须为null";
expectedDTOS.add(expectedDTO);
// 商品名称:不能为空 [非法值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("name", "");
map.put("price", "2167.15");
map.put("quantity", "9530");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品名称:不能为空";
expectedDTOS.add(expectedDTO);
// 商品名称:长度需要在0和50之间 [非法边界值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("name", "eibretelihkkubzjcpfydywwgedqapxzkhudjyfqbhsnztcjors");
map.put("price", "2167.15");
map.put("quantity", "9530");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品名称:长度需要在0和50之间";
expectedDTOS.add(expectedDTO);
// 商品价格:不能为null [非法值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("name", "君东君乖垂岂软龙帘畅");
map.put("quantity", "9530");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品价格:不能为null";
expectedDTOS.add(expectedDTO);
// 商品价格:必须大于或等于0.00 [非法边界值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("name", "君东君乖垂岂软龙帘畅");
map.put("price", "-1.0");
map.put("quantity", "9530");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品价格:必须大于或等于0.00";
expectedDTOS.add(expectedDTO);
// 商品价格:必须小于或等于100.00 [非法边界值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("name", "君东君乖垂岂软龙帘畅");
map.put("price", "101.0");
map.put("quantity", "9530");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品价格:必须小于或等于100.00";
expectedDTOS.add(expectedDTO);
// 商品价格:数字的值超出了允许范围(只允许在10位整数和2位小数范围内) [非法值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("name", "君东君乖垂岂软龙帘畅");
map.put("price", "10000000000.00");
map.put("quantity", "9530");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品价格:数字的值超出了允许范围(只允许在10位整数和2位小数范围内)";
expectedDTOS.add(expectedDTO);
// 数量:不能为null [非法值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("name", "君东君乖垂岂软龙帘畅");
map.put("price", "2167.15");
expectedDTO.jsonParam = map;
expectedDTO.expected = "数量:不能为null";
expectedDTOS.add(expectedDTO);
// 数量:最小不能小于1 [非法边界值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("name", "君东君乖垂岂软龙帘畅");
map.put("price", "2167.15");
map.put("quantity", "0");
expectedDTO.jsonParam = map;
expectedDTO.expected = "数量:最小不能小于1";
expectedDTOS.add(expectedDTO);
// 数量:最大不能超过10000 [非法边界值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("name", "君东君乖垂岂软龙帘畅");
map.put("price", "2167.15");
map.put("quantity", "10001");
expectedDTO.jsonParam = map;
expectedDTO.expected = "数量:最大不能超过10000";
expectedDTOS.add(expectedDTO);
for (ExpectedDTO item:
expectedDTOS) {
given().
contentType(JSON).
body(item.jsonParam).
header("Authorization", "Bearer " + access_token).
when().
//log().all().
post(API_GATEWAY + "/api/admin/product/create").
then().
log().all().
statusCode(422).
body(containsString(item.expected));
}
}
/**
* 商品信息列表-测试用例
*/
@Test
@Description("测试:商品信息列表")
@DisplayName("测试:商品信息列表")
@Order(2)
public void testList() {
if(access_token.isEmpty()){
access_token = tokenUtils.getAccessToken();
}
// 合法用例:自然搜索
map = new HashMap();
given().
queryParams(map).
header("Authorization","Bearer " + access_token).
when().
//log().all().
get(API_GATEWAY + "/api/admin/product/list").
then().
log().all().
statusCode(200).
body("total", is(3));
// 合法用例:关键字搜索
map = new HashMap();
map.put("keyword", "");
map.put("page", 0);
map.put("size", 10);
map.put("sortBy", "productId desc");
given().
queryParams(map).
header("Authorization","Bearer " + access_token).
when().
//log().all().
get(API_GATEWAY + "/api/admin/product/list").
then().
log().all().
statusCode(200).
body("total", is(3));
}
/**
* 显示一条商品信息的信息详情-测试用例
*/
@Test
@Description("测试:显示一条商品信息的信息详情")
@DisplayName("测试:显示一条商品信息的信息详情")
@Order(3)
public void testDetail() {
if(access_token.isEmpty()){
access_token = tokenUtils.getAccessToken();
}
// 合法用例
map = new HashMap();
given().
pathParam("productId", "1").
contentType(JSON).
body(map).
header("Authorization","Bearer " + access_token).
when().
//log().all().
get(API_GATEWAY + "/api/admin/product/detail/{productId}").
then().
log().all().
statusCode(200);
// 非法用例:未找到该资源
map = new HashMap();
given().
pathParam("productId", "100").
contentType(JSON).
body(map).
header("Authorization", "Bearer " + access_token).
when().
//log().all().
get(API_GATEWAY + "/api/admin/product/detail/{productId}").
then().
log().all().
statusCode(404);
}
/**
* 提交修改商品信息的请求-测试用例
*/
@Test
@Description("测试:提交修改商品信息的请求")
@DisplayName("测试:提交修改商品信息的请求")
@Order(4)
public void testModify() {
if(access_token.isEmpty()){
access_token = tokenUtils.getAccessToken();
}
/**
* =================== 合法用例 ===================
*/
// 合法用例
map = new HashMap();
map.put("productId", "1");
map.put("name", "纪巧写若场昏阻坑园舌未幼");
map.put("price", "8668.09");
map.put("quantity", "5728");
given().
contentType(JSON).
body(map).
header("Authorization","Bearer " + access_token).
when().
//log().all().
put(API_GATEWAY + "/api/admin/product/modify").
then().
log().all().
statusCode(200);
/**
* =================== 非法用例(数据校验不合格) ===================
*/
expectedDTOS = new ArrayList<>();
// 商品信息ID:不能为null [非法值:主键ID故意不传参]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("name", "纪巧写若场昏阻坑园舌未幼");
map.put("price", "8668.09");
map.put("quantity", "5728");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品信息ID:不能为null";
expectedDTOS.add(expectedDTO);
// 商品信息ID:最小不能小于1 [非法边界值,低于允许的最小值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("productId", "0");
map.put("name", "纪巧写若场昏阻坑园舌未幼");
map.put("price", "8668.09");
map.put("quantity", "5728");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品信息ID:最小不能小于1";
expectedDTOS.add(expectedDTO);
// 商品名称:不能为空 [非法值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("productId", "1");
map.put("name", "");
map.put("price", "8668.09");
map.put("quantity", "5728");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品名称:不能为空";
expectedDTOS.add(expectedDTO);
// 商品名称:长度需要在0和50之间 [非法边界值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("productId", "1");
map.put("name", "ivtbknbhbnqjaexlrrsoaoysyedbyaqxtwgofelxmnflgssnfhu");
map.put("price", "8668.09");
map.put("quantity", "5728");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品名称:长度需要在0和50之间";
expectedDTOS.add(expectedDTO);
// 商品价格:不能为null [非法值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("productId", "1");
map.put("name", "纪巧写若场昏阻坑园舌未幼");
map.put("quantity", "5728");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品价格:不能为null";
expectedDTOS.add(expectedDTO);
// 商品价格:必须大于或等于0.00 [非法边界值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("productId", "1");
map.put("name", "纪巧写若场昏阻坑园舌未幼");
map.put("price", "-1.0");
map.put("quantity", "5728");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品价格:必须大于或等于0.00";
expectedDTOS.add(expectedDTO);
// 商品价格:必须小于或等于100.00 [非法边界值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("productId", "1");
map.put("name", "纪巧写若场昏阻坑园舌未幼");
map.put("price", "101.0");
map.put("quantity", "5728");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品价格:必须小于或等于100.00";
expectedDTOS.add(expectedDTO);
// 商品价格:数字的值超出了允许范围(只允许在10位整数和2位小数范围内) [非法值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("productId", "1");
map.put("name", "纪巧写若场昏阻坑园舌未幼");
map.put("price", "10000000000.00");
map.put("quantity", "5728");
expectedDTO.jsonParam = map;
expectedDTO.expected = "商品价格:数字的值超出了允许范围(只允许在10位整数和2位小数范围内)";
expectedDTOS.add(expectedDTO);
// 数量:不能为null [非法值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("productId", "1");
map.put("name", "纪巧写若场昏阻坑园舌未幼");
map.put("price", "8668.09");
expectedDTO.jsonParam = map;
expectedDTO.expected = "数量:不能为null";
expectedDTOS.add(expectedDTO);
// 数量:最小不能小于1 [非法边界值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("productId", "1");
map.put("name", "纪巧写若场昏阻坑园舌未幼");
map.put("price", "8668.09");
map.put("quantity", "0");
expectedDTO.jsonParam = map;
expectedDTO.expected = "数量:最小不能小于1";
expectedDTOS.add(expectedDTO);
// 数量:最大不能超过10000 [非法边界值]
expectedDTO = new ExpectedDTO();
map = new HashMap();
map.put("productId", "1");
map.put("name", "纪巧写若场昏阻坑园舌未幼");
map.put("price", "8668.09");
map.put("quantity", "10001");
expectedDTO.jsonParam = map;
expectedDTO.expected = "数量:最大不能超过10000";
expectedDTOS.add(expectedDTO);
for (ExpectedDTO item:
expectedDTOS) {
given().
contentType(JSON).
body(item.jsonParam).
header("Authorization", "Bearer " + access_token).
when().
//log().all().
put(API_GATEWAY + "/api/admin/product/modify").
then().
log().all().
statusCode(422).
body(containsString(item.expected));
}
}
/**
* 删除一条商品信息信息(非物理删除)-测试用例
*/
@Test
@Description("测试:删除一条商品信息信息(非物理删除)")
@DisplayName("测试:删除一条商品信息信息(非物理删除)")
@Order(5)
public void testDelete() {
if(access_token.isEmpty()){
access_token = tokenUtils.getAccessToken();
}
// 合法用例
map = new HashMap();
given().
pathParam("productId", "1").
contentType(JSON).
body(map).
header("Authorization","Bearer " + access_token).
when().
//log().all().
delete(API_GATEWAY + "/api/admin/product/delete/{productId}").
then().
log().all().
statusCode(204);
}
}