zoukankan      html  css  js  c++  java
  • 六、测试驱动开发 TDD

    在编写业务代码前,先考虑如何编写测试,再编写业务代码,这种开发方式称作:TDD test-driven development。

    使用TDD的主要优点

    就通常的单元测试而言,最为明显优点就增强了我们对代码按照设计运行的信心。
    而TDD,由于是在编写业务代码提前设计,可以说,这些单元测试就反映了业务需求(当然依赖单元测试的质量),为重构提供了保障。

    简单的开始

    下面的例子是一个完整的TDD的流程,目的是实现Profile类。一遍遵循如下的流程:
    1. 编写一个会失败的测试
    2. 通过编写新的代码使得测试通过
    3. 清理掉无效代码并重复上面的步骤

    首先编写最简单测试用例与业务代码

    package iloveyouboss;
     
    import static org.junit.Assert.assertFalse;
    import org.junit.Test;
     
    public class ProfileTest {
        @Test
        public void matchesNothingWhenProfileEmpty() {
            Profile profile = new Profile();
        }
    }
    这是肯定会出现编译错误,原因是没有Profile类,此时我们建立一个最简单的Profile,仅满足测试用例不报错。Profile类如下:
    package iloveyouboss;
     
    public class Profile {
     
    }

    现在改进一下测试用例

    补充测试用例的内容,让用例先失败,再成功。
    package iloveyouboss;
     
    import static org.junit.Assert.assertFalse;
    import org.junit.Test;
     
    public class ProfileTest {
        @Test
        public void matchesNothingWhenProfileEmpty() {
            Profile profile = new Profile();
     
            Question question = new BooleanQuestion(1, "Relocation package?");
            Criterion criterion = new Criterion(new Answer(question, Bool.TRUE), Weight.DontCare);
     
            boolean result = profile.matches(criterion);
     
            assertFalse(result);
        }
    }
    假定其他依赖的类均存在,此时profile.matches()方法会报错,原因是没有matches方法。这时修改Profile类:
    package iloveyouboss;
     
    public class Profile {
        private Answer answer;
     
        public boolean matches(Criterion criterion) {
            return answer != null;
        }
     
        public void add(Answer answer)
        {
            this.answer = answer;
        }
    }

    清理测试

    在经过上面的循环之后,我们发下这两个测试用例均初始化了Profile,因此可以将其提权到@Before函数中。
    public class ProfileTest {
        private Profile profile;
     
        @Before
        public void createProfile()
        {
            profile = new Profile();
        }
     
        ....
    }
    TDD美好的地方在于,在特性之前编写测试,也就意味着可以始终对重构和清理代码充满信心。
    我们还可以不断的重构测试,如提取公共的变量到@Before中,对变量进行重命名以提高可读性,使得单元测试能够明确的表述功能,并自然的成为文档的一部分。下面是经过重构后的测试代码,可以看到profile, questionIsThrereRelocation, answerThereIsRelocation被提取和改名了。
    package iloveyouboss;
     
    import static org.junit.Assert.assertFalse;
    import static org.junit.Assert.assertTrue;
    import org.junit.Before;
    import org.junit.Test;
     
    public class ProfileTest {
        private Profile profile;
        private BooleanQuestion questionIsThereRelocation;
        private Answer answerThereIsRelocation;
     
        @Before
        public void createProfile() {
            profile = new Profile();
        }
     
        @Before
        public void createQuestion() {
            questionIsThereRelocation = new BooleanQuestion(1, "Relocation package?");
            answerThereIsRelocation = new Answer(questionIsThereRelocation, Bool.TRUE);
        }
     
        @Test
        public void matchesNothingWhenProfileEmpty() {
            Criterion criterion = new Criterion(answerThereIsRelocation, Weight.DontCare);
     
            boolean result = profile.matches(criterion);
     
            assertFalse(result);
        }
     
        @Test
        public void matchesWhenProfileContainsMatchingAnswer() {
            profile.add(answerThereIsRelocation);
            Criterion criterion = new Criterion(answerThereIsRelocation, Weight.Important);
     
            boolean result = profile.matches(criterion);
     
            assertTrue(result);
        }
    }

    再一次改进

    这次增加一个测试用例,用来覆盖答案不能覆盖问题的场景,首先增加一个用例:
        @Test
        public void doesNotMatchWhenNoMatchingAnswer() {
            profile.add(answerThereIsNotRelocation);
            Criterion criterion = new Criterion(answerThereIsRelocation, Weight.Important);
     
            boolean result = profile.matches(criterion);
     
            assertFalse(result);
        }
    这里用例是执行不通过的,然后通过修改profile类使得该测试通过:
    package iloveyouboss;
     
    public class Profile {
        private Answer answer;
     
        public boolean matches(Criterion criterion) {
            return answer != null && answer.match(criterion.getAnswer());
        }
     
        public void add(Answer answer) {
            this.answer = answer;
        }
    }
     
    作为开发者,工作就是考虑代码中所有的可能性和场景。如果要取得TDD的成功,需要将这些场景拆分为测试,并将他们按照一定的顺序排列,以减少使得每个测试通过的代码增量。

    将测试作为文档一部分

    对于TDD,测试用例应该作为文档的一部分,其他开发者可以通过阅读测试用例来了解内部逻辑,这要求测试用例的名称足够清晰。如:
    matchesWhenProfileContainsMatchingAnswer 
    doesNotMatchWhenNoMatchingAnswer 
    matchesWhenContainsMultipleAnswers 
    doesNotMatchWhenNoneOfMultipleCriteriaMatch 
    matchesWhenAnyOfMultipleCriteriaMatch 
    doesNotMatchWhenAnyMustMeetCriteriaNotMet 
    matchesWhenCriterionIsDontCare 
    scoreIsZeroWhenThereAreNoMatches
     
    

      

    另外,为了清晰,也可以将不同关注点的测试用例放到不同的测试类中。

    TDD的节奏

    TDD遵循一个普遍的循环:
    1. 编写测试
    2. 编写逻辑使得测试通过
    3. 重构代码保持清晰
    4. 重复上述过程
    最好将每次循环控制得足够小,例如10分钟以内,如果10分钟不能完成则说明测试的粒度有些大,需要重新写测试,这样不断循环,逐渐逼近和完善功能。
    最终交付的,将不只是代码,同时提供了作为文档使用的测试用例以及重构的保证。
  • 相关阅读:
    [转发]深入理解git,从研究git目录开始
    iOS系统网络抓包方法
    charles抓包工具
    iOS多线程中performSelector: 和dispatch_time的不同
    IOS Core Animation Advanced Techniques的学习笔记(五)
    IOS Core Animation Advanced Techniques的学习笔记(四)
    IOS Core Animation Advanced Techniques的学习笔记(三)
    IOS Core Animation Advanced Techniques的学习笔记(二)
    IOS Core Animation Advanced Techniques的学习笔记(一)
    VirtualBox复制CentOS后提示Device eth0 does not seem to be present的解决方法
  • 原文地址:https://www.cnblogs.com/jiyuqi/p/13841642.html
Copyright © 2011-2022 走看看