• 六、测试驱动开发 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分钟不能完成则说明测试的粒度有些大,需要重新写测试,这样不断循环,逐渐逼近和完善功能。
    最终交付的,将不只是代码,同时提供了作为文档使用的测试用例以及重构的保证。
  • 相关阅读:
    iOS中循环引用的解除
    Block的循环引用详解
    Mac OS X下面 Node.js环境的搭建
    swift中闭包和OC的block的对比
    STL priority_queue
    优先使用map(或者unordered_map)的find函数而非algorithm里的find函数
    Insert Interval
    Integer Break
    Unique Binary Search Trees
    腾讯2016实习生笔试
  • 原文地址:https://www.cnblogs.com/jiyuqi/p/13841642.html
Copyright © 2020-2023  润新知