手把手教你落地TDD
2.1 第一步、编写测试用例

2.2 第二步、运行测试用例
2.3 第三步、编写代码

2.4 第四步、运行测试用例
2.5 第五步、重构代码
2.6 第六步、运行测试用例
3.1 误区一、单元测试就是TDD
3.2 误区二、误把集成测试当成单元测试
-
测试用例职责不单一
-
测试用例粒度过大
-
测试用例执行太慢
判断自己写的用例是否是单元测试用例,方法很简单:只需要把开发者电脑的网络关掉,如果能正常在本地执行单元测试,那么基本写的就是单元测试,否则均为集成测试用例。
2.3 误区三、项目工期紧别写单元测试了
2.4 误区四、代码完成后再补单元测试
2.5 误区五、对单元测试覆盖率的极端要求
2.6 误区六、单元测试只需要运行一次
4.1 单元测试框架
4.2 模拟对象框架
4.3 测试覆盖率
4.4 测试报告
5.1 奇怪的计算器
5.1.1 第一次迭代
输入:输入一个int类型的参数
处理逻辑:
(1)入参大于0,计算其减1的值并返回;
(2)入参等于0,直接返回0;
(3)入参小于0,计算其加1的值并返回
-
第一步、红灯
public class StrangeCalculatorTest {
private StrangeCalculator strangeCalculator;
@BeforeEach
public void setup() {
strangeCalculator = new StrangeCalculator();
}
@Test
@DisplayName("入参大于0,将其减1并返回")
public void givenGreaterThan0() {
//大于0的入参
int input = 1;
int expected = 0;
//实际计算
int result = strangeCalculator.calculate(input);
//断言确认是否减1
Assertions.assertEquals(expected, result);
}
@Test
@DisplayName("入参小于0,将其加1并返回")
public void givenLessThan0() {
//小于0的入参
int input = -1;
int expected = 0;
//实际计算
int result = strangeCalculator.calculate(input);
//断言确认是否减1
Assertions.assertEquals(expected, result);
}
@Test
@DisplayName("入参等于0,直接返回")
public void givenEquals0() {
//等于0的入参
int input = 0;
int expected = 0;
//实际计算
int result = strangeCalculator.calculate(input);
//断言确认是否等于0
Assertions.assertEquals(expected, result);
}
}
此时StrangeCalculator类和calculate方法还没有创建,会IDE报红色提醒是正常的。
创建StrangeCalculator类和calculate方法,注意此时未实现业务逻辑,应当使测试用例不能通过,在此抛出一个UnsupportedOperationException异常。
public class StrangeCalculator {
public int calculate(int input) {
//此时未实现业务逻辑,因此抛一个不支持操作的异常,以便使测试用例不通过
throw new UnsupportedOperationException();
}
}


-
第二步、绿灯
public class StrangeCalculator {
public int calculate(int input) {
//大于0的逻辑
if (input > 0) {
return input - 1;
}
//未实现的边界依旧抛出UnsupportedOperationException异常
throw new UnsupportedOperationException();
}
}

public class StrangeCalculator {
public int calculate(int input) {
if (input > 0) {
//大于0的逻辑
return input - 1;
} else if (input < 0) {
//小于0的逻辑
return input + 1;
}
//未实现的边界依旧抛出UnsupportedOperationException异常
throw new UnsupportedOperationException();
}
}

public class StrangeCalculator {
public int calculate(int input) {
//大于0的逻辑
if (input > 0) {
return input - 1;
} else if (input < 0) {
return input + 1;
} else {
return 0;
}
}
}





-
第三步、重构
public class StrangeCalculator {
public int calculate(int input) {
//大于0的逻辑
if (input > 0) {
return doGivenGreaterThan0(input);
} else if (input < 0) {
return doGivenLessThan0(input);
} else {
return doGivenEquals0(input);
}
}
private int doGivenEquals0(int input) {
return 0;
}
private int doGivenLessThan0(int input) {
return input + 1;
}
private int doGivenGreaterThan0(int input) {
return input - 1;
}
}


5.1.2 第二次迭代
(1)针对大于0且小于100的input,不再计算其减1的值,而是计算其平方值;
(1)针对大于0且小于100的input,计算其平方值;
(2)针对大于等于100的input,计算其减去1的值;
(3)针对小于0的input,计算其加1的值;
(4)针对等于0的input,返回0
-
第一步,红灯
@Test
@DisplayName("入参大于0且小于100,计算其平方")
public void givenGreaterThan0AndLessThan100() {
int input = 3;
int expected = 9;
//实际计算
int result = strangeCalculator.calculate(input);
//断言确认是否计算了平方
Assertions.assertEquals(expected, result);
}
@Test
@DisplayName("入参大于等于100,计算其减1的值")
public void givenGreaterThanOrEquals100() {
int input = 100;
int expected = 99;
//实际计算
int result = strangeCalculator.calculate(input);
//断言确认是否计算了平方
Assertions.assertEquals(expected, result);
}

-
第二步、绿灯
public class StrangeCalculator {
public int calculate(int input) {
if (input >= 100) {
//第二次迭代时,大于等于100的区间还是走老逻辑
return doGivenGreaterThan0(input);
} else if (input > 0) {
//第二次迭代的业务逻辑
return input * input;
} else if (input < 0) {
return doGivenLessThan0(input);
} else {
return doGivenEquals0(input);
}
}
private int doGivenEquals0(int input) {
return 0;
}
private int doGivenLessThan0(int input) {
return input + 1;
}
private int doGivenGreaterThan0(int input) {
return input - 1;
}
}

@Test
@DisplayName("入参大于0,将其减1并返回")
public void givenGreaterThan0() {
int input = 1;
int expected = 0;
int result = strangeCalculator.calculate(input);
Assertions.assertEquals(expected, result);
}
@Test
@DisplayName("入参大于0且小于100,计算其平方")
public void givenGreaterThan0AndLessThan100() {
//于0且小于100的入参
int input = 3;
int expected = 9;
//实际计算
int result = strangeCalculator.calculate(input);
//断言确认是否计算了平方
Assertions.assertEquals(expected, result);
}
@Test
@DisplayName("入参大于等于100,计算其减1的值")
public void givenGreaterThanOrEquals100() {
//于0且小于100的入参
int input = 100;
int expected = 99;
//实际计算
int result = strangeCalculator.calculate(input);
//断言确认是否计算了平方
Assertions.assertEquals(expected, result);
}

-
第三步、重构
public class StrangeCalculator {
public int calculate(int input) {
if (input >= 100) {
//第二次迭代时,大于等于100的区间还是走老逻辑
// return doGivenGreaterThan0(input);
return doGivenGreaterThanOrEquals100(input);
} else if (input > 0) {
//第二次迭代的业务逻辑
return doGivenGreaterThan0AndLessThan100(input);
} else if (input < 0) {
return doGivenLessThan0(input);
} else {
return doGivenEquals0(input);
}
}
private int doGivenGreaterThan0AndLessThan100(int input) {
return input * input;
}
private int doGivenEquals0(int input) {
return 0;
}
private int doGivenGreaterThanOrEquals100(int input) {
return input + 1;
}
private int doGivenGreaterThan100(int input) {
return input - 1;
}
}
5.1.3 第三次迭代
5.2 贫血模型三层架构的TDD实战
5.2.1 Dao层单元测试用例
public interface CmsArticleMapper {
int deleteByPrimaryKey(Long id);
int insert(CmsArticle record);
CmsArticle selectByPrimaryKey(Long id);
List<CmsArticle> selectAll();
int updateByPrimaryKey(CmsArticle record);
}
@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureTestDatabase
public class CmsArticleMapperTest {
@Resource
private CmsArticleMapper mapper;
@Test
public void testInsert() {
CmsArticle article = new CmsArticle();
article.setId(0L);
article.setArticleId("ABC123");
article.setContent("content");
article.setTitle("title");
article.setVersion(1L);
article.setModifiedTime(new Date());
article.setDeleted(0);
article.setPublishState(0);
int inserted = mapper.insert(article);
Assertions.assertEquals(1, inserted);
}
@Test
public void testUpdateByPrimaryKey() {
CmsArticle article = new CmsArticle();
article.setId(1L);
article.setArticleId("ABC123");
article.setContent("content");
article.setTitle("title");
article.setVersion(1L);
article.setModifiedTime(new Date());
article.setDeleted(0);
article.setPublishState(0);
int updated = mapper.updateByPrimaryKey(article);
Assertions.assertEquals(1, updated);
}
@Test
public void testSelectByPrimaryKey() {
CmsArticle article = mapper.selectByPrimaryKey(2L);
Assertions.assertNotNull(article);
Assertions.assertNotNull(article.getTitle());
Assertions.assertNotNull(article.getContent());
}
}
5.2.2 Service层单元测试用例
@Service
public class ArticleServiceImpl implements ArticleService {
@Resource
private CmsArticleMapper mapper;
@Resource
private IdServiceGateway idServiceGateway;
@Override
public void createDraft(CreateDraftCmd cmd) {
CmsArticle article = new CmsArticle();
article.setArticleId(idServiceGateway.nextId());
article.setContent(cmd.getContent());
article.setTitle(cmd.getTitle());
article.setPublishState(0);
article.setVersion(1L);
article.setCreatedTime(new Date());
article.setModifiedTime(new Date());
article.setDeleted(0);
mapper.insert(article);
}
@Override
public CmsArticle getById(Long id) {
return mapper.selectByPrimaryKey(id);
}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
classes = {ArticleServiceImpl.class})
@ExtendWith(SpringExtension.class)
public class ArticleServiceImplTest {
@Resource
private ArticleService articleService;
@MockBean
IdServiceGateway idServiceGateway;
@MockBean
private CmsArticleMapper cmsArticleMapper;
@Test
public void testCreateDraft() {
Mockito.when(idServiceGateway.nextId()).thenReturn("123");
Mockito.when(cmsArticleMapper.insert(Mockito.any())).thenReturn(1);
CreateDraftCmd createDraftCmd = new CreateDraftCmd();
createDraftCmd.setTitle("test-title");
createDraftCmd.setContent("test-content");
articleService.createDraft(createDraftCmd);
Mockito.verify(idServiceGateway, Mockito.times(1)).nextId();
Mockito.verify(cmsArticleMapper, Mockito.times(1)).insert(Mockito.any());
}
@Test
public void testGetById() {
CmsArticle article = new CmsArticle();
article.setId(1L);
article.setTitle("testGetById");
Mockito.when(cmsArticleMapper.selectByPrimaryKey(Mockito.any())).thenReturn(article);
CmsArticle byId = articleService.getById(1L);
Assertions.assertNotNull(byId);
Assertions.assertEquals(1L,byId.getId());
Assertions.assertEquals("testGetById",byId.getTitle());
}
}

5.2.3 Controller层单元测试用例
@RestController
@RequestMapping("/article")
public class ArticleController {
@Resource
private ArticleService articleService;
@RequestMapping("/createDraft")
public void createDraft(@RequestBody CreateDraftCmd cmd) {
articleService.createDraft(cmd);
}
@RequestMapping("/get")
public CmsArticle get(Long id) {
CmsArticle article = articleService.getById(id);
return article;
}
}
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK,
classes = {ArticleController.class})
@EnableWebMvc
public class ArticleControllerTest {
@Resource
WebApplicationContext webApplicationContext;
MockMvc mockMvc;
@MockBean
ArticleService articleService;
//初始化mockmvc
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
void testCreateDraft() throws Exception {
CreateDraftCmd cmd = new CreateDraftCmd();
cmd.setTitle("test-controller-title");
cmd.setContent("test-controller-content");
ObjectMapper mapper = new ObjectMapper();
String valueAsString = mapper.writeValueAsString(cmd);
Mockito.doNothing().when(articleService).createDraft(Mockito.any());
mockMvc.perform(MockMvcRequestBuilders
//访问的URL和参数
.post("/article/createDraft")
.content(valueAsString)
.contentType(MediaType.APPLICATION_JSON))
//期望返回的状态码
.andExpect(MockMvcResultMatchers.status().isOk())
//输出请求和响应结果
.andDo(MockMvcResultHandlers.print()).andReturn();
}
@Test
void testGet() throws Exception {
CmsArticle article = new CmsArticle();
article.setId(1L);
article.setTitle("testGetById");
Mockito.when(articleService.getById(Mockito.any())).thenReturn(article);
mockMvc.perform(MockMvcRequestBuilders
//访问的URL和参数
.get("/article/get").param("id","1"))
//期望返回的状态码
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1L))
//输出请求和响应结果
.andDo(MockMvcResultHandlers.print()).andReturn();
}
}


5.3 DDD下的TDD实战
5.3.1 实体的单元测试
Data
public class ArticleEntity extends AbstractDomainMask {
/**
* article业务主键
*/
private ArticleId articleId;
/**
* 标题
*/
private ArticleTitle title;
/**
* 内容
*/
private ArticleContent content;
/**
* 发布状态,[0-待发布;1-已发布]
*/
private Integer publishState;
/**
* 创建草稿
*/
public void createDraft() {
this.publishState = PublishState.TO_PUBLISH.getCode();
}
/**
* 修改标题
*
* @param articleTitle
*/
public void modifyTitle(ArticleTitle articleTitle) {
this.title = articleTitle;
}
/**
* 修改正文
*
* @param articleContent
*/
public void modifyContent(ArticleContent articleContent) {
this.content = articleContent;
}
/**
* 发布
*/
public void publishArticle() {
this.publishState = PublishState.PUBLISHED.getCode();
}
}
public class ArticleEntityTest {
@Test
@DisplayName("创建草稿")
public void testCreateDraft() {
ArticleEntity entity = new ArticleEntity();
entity.setTitle(new ArticleTitle("title"));
entity.setContent(new ArticleContent("content12345677890"));
entity.createDraft();
Assertions.assertEquals(PublishState.TO_PUBLISH.getCode(), entity.getPublishState());
}
@Test
@DisplayName("修改标题")
public void testModifyTitle() {
ArticleEntity entity = new ArticleEntity();
entity.setTitle(new ArticleTitle("title"));
entity.setContent(new ArticleContent("content12345677890"));
ArticleTitle articleTitle = new ArticleTitle("new-title");
entity.modifyTitle(articleTitle);
Assertions.assertEquals(articleTitle.getValue(), entity.getTitle().getValue());
}
@Test
@DisplayName("修改正文")
public void testModifyContent() {
ArticleEntity entity = new ArticleEntity();
entity.setTitle(new ArticleTitle("title"));
entity.setContent(new ArticleContent("content12345677890"));
ArticleContent articleContent = new ArticleContent("new-content12345677890");
entity.modifyContent(articleContent);
Assertions.assertEquals(articleContent.getValue(), entity.getContent().getValue());
}
@Test
@DisplayName("发布")
public void testPublishArticle() {
ArticleEntity entity = new ArticleEntity();
entity.setTitle(new ArticleTitle("title"));
entity.setContent(new ArticleContent("content12345677890"));
entity.publishArticle();
Assertions.assertEquals(PublishState.PUBLISHED.getCode(), entity.getPublishState());
}
}
5.3.2 值对象的单元测试
public class ArticleTitle implements ValueObject<String> {
private final String value;
public ArticleTitle(String value) {
this.check(value);
this.value = value;
}
private void check(String value) {
Objects.requireNonNull(value, "标题不能为空");
if (value.length() > 64) {
throw new IllegalArgumentException("标题过长");
}
}
@Override
public String getValue() {
return this.value;
}
}
public class ArticleTitleTest {
@Test
@DisplayName("测试业务规则,ArticleTitle为空抛异常")
public void whenGivenNull() {
Assertions.assertThrows(NullPointerException.class, () -> {
new ArticleTitle(null);
});
}
@Test
@DisplayName("测试业务规则,ArticleTitle值长度大于64抛异常")
public void whenGivenLengthGreaterThan64() {
Assertions.assertThrows(IllegalArgumentException.class, () -> {
new ArticleTitle("11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111");
});
}
@Test
@DisplayName("测试业务规则,ArticleTitle小于等于64正常创建")
public void whenGivenLengthEquals64() {
ArticleTitle articleTitle = new ArticleTitle("1111111111111111111111111111111111111111111111111111111111111111");
Assertions.assertEquals(64, articleTitle.getValue().length());
}
}
5.3.3 Factory的单元测试
@Component
public class ArticleDomainFactoryImpl implements ArticleFactory {
@Override
public ArticleEntity newInstance(ArticleTitle title, ArticleContent content) {
ArticleEntity entity = new ArticleEntity();
entity.setTitle(title);
entity.setContent(content);
entity.setArticleId(new ArticleId(UUID.randomUUID().toString()));
entity.setPublishState(PublishState.TO_PUBLISH.getCode());
entity.setDeleted(0);
Date date = new Date();
entity.setCreatedTime(date);
entity.setModifiedTime(date);
return entity;
}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
classes = {ArticleDomainFactoryImpl.class})
@ExtendWith(SpringExtension.class)
public class ArticleDomainFactoryImplTest {
@Resource
private ArticleFactory articleFactory;
@Test
@DisplayName("Factory创建新实体")
public void testNewInstance() {
ArticleTitle articleTitle = new ArticleTitle("title");
ArticleContent articleContent = new ArticleContent("content1234567890");
ArticleEntity instance = articleFactory.newInstance(articleTitle, articleContent);
// 创建新实体
Assertions.assertNotNull(instance);
// 唯一标识正确赋值
Assertions.assertNotNull(instance.getArticleId());
}
}
本文来自博客园,作者:古道轻风,转载请注明原文链接:https://www.cnblogs.com/88223100/p/Hands-teach-you-how-to-land-TDD.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验
2022-07-22 记一次 JMeter 压测 HTTPS 性能问题
2022-07-22 关于接口测试自动化的总结与思考
2022-07-22 从业务开发中学习和理解架构设计