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;public void setup() {strangeCalculator = new StrangeCalculator();}("入参大于0,将其减1并返回")public void givenGreaterThan0() {//大于0的入参int input = 1;int expected = 0;//实际计算int result = strangeCalculator.calculate(input);//断言确认是否减1Assertions.assertEquals(expected, result);}("入参小于0,将其加1并返回")public void givenLessThan0() {//小于0的入参int input = -1;int expected = 0;//实际计算int result = strangeCalculator.calculate(input);//断言确认是否减1Assertions.assertEquals(expected, result);}("入参等于0,直接返回")public void givenEquals0() {//等于0的入参int input = 0;int expected = 0;//实际计算int result = strangeCalculator.calculate(input);//断言确认是否等于0Assertions.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
-
第一步,红灯
("入参大于0且小于100,计算其平方")public void givenGreaterThan0AndLessThan100() {int input = 3;int expected = 9;//实际计算int result = strangeCalculator.calculate(input);//断言确认是否计算了平方Assertions.assertEquals(expected, result);}("入参大于等于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;}}
("入参大于0,将其减1并返回")public void givenGreaterThan0() {int input = 1;int expected = 0;int result = strangeCalculator.calculate(input);Assertions.assertEquals(expected, result);}("入参大于0且小于100,计算其平方")public void givenGreaterThan0AndLessThan100() {//于0且小于100的入参int input = 3;int expected = 9;//实际计算int result = strangeCalculator.calculate(input);//断言确认是否计算了平方Assertions.assertEquals(expected, result);}("入参大于等于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);}
(SpringExtension.class)public class CmsArticleMapperTest {private CmsArticleMapper mapper;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);}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);}public void testSelectByPrimaryKey() {CmsArticle article = mapper.selectByPrimaryKey(2L);Assertions.assertNotNull(article);Assertions.assertNotNull(article.getTitle());Assertions.assertNotNull(article.getContent());}}
5.2.2 Service层单元测试用例
public class ArticleServiceImpl implements ArticleService {private CmsArticleMapper mapper;private IdServiceGateway idServiceGateway;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);}public CmsArticle getById(Long id) {return mapper.selectByPrimaryKey(id);}}
(webEnvironment = SpringBootTest.WebEnvironment.NONE,classes = {ArticleServiceImpl.class})(SpringExtension.class)public class ArticleServiceImplTest {private ArticleService articleService;IdServiceGateway idServiceGateway;private CmsArticleMapper cmsArticleMapper;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());}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层单元测试用例
public class ArticleController {private ArticleService articleService;public void createDraft( CreateDraftCmd cmd) {articleService.createDraft(cmd);}public CmsArticle get(Long id) {CmsArticle article = articleService.getById(id);return article;}}
(SpringExtension.class)(webEnvironment = SpringBootTest.WebEnvironment.MOCK,classes = {ArticleController.class})public class ArticleControllerTest {WebApplicationContext webApplicationContext;MockMvc mockMvc;ArticleService articleService;//初始化mockmvcvoid setUp() {mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();}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();}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 实体的单元测试
@Datapublic 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 {("创建草稿")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());}("修改标题")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());}("修改正文")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());}("发布")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("标题过长");}}public String getValue() {return this.value;}}
public class ArticleTitleTest {("测试业务规则,ArticleTitle为空抛异常")public void whenGivenNull() {Assertions.assertThrows(NullPointerException.class, () -> {new ArticleTitle(null);});}("测试业务规则,ArticleTitle值长度大于64抛异常")public void whenGivenLengthGreaterThan64() {Assertions.assertThrows(IllegalArgumentException.class, () -> {new ArticleTitle("11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111");});}("测试业务规则,ArticleTitle小于等于64正常创建")public void whenGivenLengthEquals64() {ArticleTitle articleTitle = new ArticleTitle("1111111111111111111111111111111111111111111111111111111111111111");Assertions.assertEquals(64, articleTitle.getValue().length());}}
5.3.3 Factory的单元测试
public class ArticleDomainFactoryImpl implements ArticleFactory {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;}}
(webEnvironment = SpringBootTest.WebEnvironment.NONE,classes = {ArticleDomainFactoryImpl.class})(SpringExtension.class)public class ArticleDomainFactoryImplTest {private ArticleFactory articleFactory;("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());}}

