• 测试驱动开发(TDD)


    测试驱动开发的基本概念##

    • 为什么会出现测试驱动开发
      • 当有一个新的任务时,往往第一个念头就是如何去实现它呢?
      • 拿到任务就开始编码,一边写,变修改和设计
      • 我已经调试了好几遍,应该不会有问题了,好了,先休息一下吧
      • 测试是QA的事,我为什么要做啊,我做了,他们做什么?
      • 奇怪了,怎么代码跟开发文档有这么大的区别啊?
      • 开发部在搞什么啊,怎么代码有这么多的BUG?
      • 这段代码想干嘛?
      • ……
    • 什么是测试驱动
      • 测试驱动是一种开发形式:
        • 首先要编写测试代码
        • 除非存在相关测试,否则不编写任何的产品代码
        • 由测试来决定需要写什么样的代码
        • 要求维护一套详细的测试集
    • 目标
      • 代码整洁可用
        • 只有测试失败时,我们才写代码
        • 消除重复设计,优化设计结构

    测试驱动开发的流程##

    Test-Driven Design(TDD)##

    是一种开发风格,它要求程序员做到:

    • 在写产品代码之前,先写它的单元测试(Unit Tests)
    • 没有单元测试的Class不运行作为产品代码
    • 单元测试用例决定了如何写产品代码
    • 不断地成功运行所有的单元测试用例
    • 不断地完善单元测试用例

    Test-Driven Design是把需求分析,设计,质量控制量化的过程!

    如何做Test-Driven Design and Development###

    在开发一个新功能之前:

    • 首先确定“做什么”(不是要如何做!)
      • 例如,一个网站的增加用户的功能,我们需要一个方法来增加一个用户:
        • public void addUser(User user)
        • 功能包括:增加一个用户(在数据库中插入一条记录);如果已经有相同的用户,应该返回一个用户已存在的消息
    • 然后为这个功能写单元测试例子(Unit Test)
      • 单元测试用例要覆盖这个Method的“做什么”
      • 所以我们至少要有两个测试用例:
        • Test Case1:测试成功增加一个用户
        • Test Case 2:测试增加一个已存在的用户
        • 其它边缘情况测试:Test Case 3:传入的User对象为NULL
    • 写生成(Production)代码
      • 我们清楚地知道这段代码需要做什么,清晰的表明这段代码的Contracts。
      • 只需要能实现在Unit Test中的Contracts和能够通过它的Unit Test
    • 运行Unit Test
      • 如果顺利通过,你已经很好的完成了你的任务
      • 如果没通过,修补代码直到能通过Unit Test为止
      • 如果出现在Unit Test中没预先设定的结果,在Unit Test中增加一个Test Case,修补代码直到通过所有的Test Case为止。

    TDD 和 PSP###

    Personal Software Process的development

    Test-Driven Design and Development

    什么时候写Tests?###

    • 如果你要写一个新的功能,请先写它的测试用例
    • 如果你要在没有经过测试的代码上写新的功能,请先写目前代码的测试用例
    • 如果你要Fix一个Bug,请先为这个Bug写一个测试用例
    • 如果你要Refactor没有测试过的代码,请先写一个测试用例
    • 如果发现一个边缘例外值,请为它写一个测试用例

    xUnit 工具###

    • Java Class的测试Framework: JUnit
    • Java Swing app 的测试Framework: JFCUnit
    • Java Server Side(EJB,Servlet)的测试Framework:Catus
    • HTML Page的测试Framework: HTMLUnit、HTTPUnit
    • C++的测试Framework: CPPUnit
    • .Net APP的测试Framework: .NetUnit

    实例##

    考虑一个质因数分解的例子:将任何一个大于1的自然数分解成质因子相乘,如4=2*2,24=2*2*2*3等。

    第一天###

    1. 建立Maven工程



      创建如下Maven工程:

      在pom.xml文件中加入JUnit信息:
    2. 根据测试驱动要求,写一个TestCase:

    写第一个TestCase:

    public class FactorTest {
           @Test
           public void testFactor() {
               assertEquals(2, Factor.factor(2));
           }
       }
    

    创建一个类Factor和静态方法factor:

    public class Factor {
    	public static int factor(int i){
    		return 2;
    	}
    }
    

    运行第一个TestCase:

    测试通过:

    于是形成质因数分解的第一个版本。

    第二天###

    第一个测试用例可以测试2,那么3呢?
    新增加一个测试用例:

    public class FactorTest {
    	
    	@Test
    	public void testFactor() {
    		assertEquals(2, Factor.factor(2));
    		assertEquals(3, Factor.factor(3));
    	}
    
    }
    

    根据测试用例,修改Factor类的代码以通过测试用例:

    public class Factor {
    	public static int factor(int i){
    		if(i==2) return 2;
    		if(i==3) return 3;
    		return 0;
    	}
    }
    

    运行测试用例:

    测试通过!

    第三天###

    在前2个测试用例的基础上,增加4,又会怎样呢?

    发现测试4,不能通过?怎么办?开始修改Factor类的代码!
    首先希望测试4时,返回的是质因子的集合,修改测试用例:

    assertEquals(Arrays.asList(2,2), Factor.factor(4));
    

    对应的,其它测试用例相应发生更改:

    public class FactorTest {
    	
    	@Test
    	public void testFactor() {
    		assertEquals(Arrays.asList(2), Factor.factor(2));
    		assertEquals(Arrays.asList(3), Factor.factor(3));
    		assertEquals(Arrays.asList(2,2), Factor.factor(4));
    	}
    }
    

    由于返回的是数组集合,因此修改Factor类的factor方法:

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();
    		if(i==2) l.add(2);
    		if(i==3) l.add(3);
    		return l;
    	}
    }
    

    跑测试,发现测试不通过,据此修改factor方法,以使得4的测试能够通过:

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();
    		if(i==2) l.add(2);
    		if(i==3) l.add(3);
    		if(i==4){
    			l.add(2);
    			l.add(2);
    		}
    		return l;
    	}
    }
    

    运行JUnit Test:

    发现测试通过,OK!

    第四天###

    那么5呢?能测试通过吗?

    不行!修改Factor类的factor方法:

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();
    		if(i==2) l.add(2);
    		if(i==3) l.add(3);
    		if(i==4){
    			l.add(2);
    			l.add(2);
    		}
    		if(i==5) l.add(5);
    		return l;
    	}
    }
    

    测试通过:

    如果再继续这么写下去,你会发现你的代码臃肿不堪,不忍直视!
    那么这时候,我们需要对代码进行重构优化

    当你无节操的修改代码以通过测试时,我们一定要在某个点停下来看看,我们是不是可以再优化些什么?
    测试驱动容易让人进入一个局部关注点,不太关注思维的系统整体点。

    停下脚步,回头看看我们的Factor类的factor方法,发现 i=2,i=3,i=5要做的事情似乎一样。
    第一步,可以考虑将代码改成:

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();
    		l.add(i);		
    		if(i==4){
    			l.add(2);
    			l.add(2);
                            return l;
    		}		
    		return l;
    	}
    }
    

    运行测试:

    发现4不能通过!发现4不能直接加。
    怎么办?调整代码顺序!

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();				
    		if(i==4){
    			l.add(2);
    			l.add(2);
    			return l;
    		}
    		l.add(i);
    		return l;
    	}
    }
    

    运行JUnit Test:

    测试通过!与未重构前的代码相比,看上去漂亮多了!

    第五天###

    接下来,我们测试6:

    public class FactorTest {
    	
    	@Test
    	public void testFactor() {
    		assertEquals(Arrays.asList(2), Factor.factor(2));
    		assertEquals(Arrays.asList(3), Factor.factor(3));
    		assertEquals(Arrays.asList(2,2), Factor.factor(4));
    		assertEquals(Arrays.asList(5), Factor.factor(5));
    		assertEquals(Arrays.asList(2,3), Factor.factor(6));
    	}
    }
    

    显然,6不能通过!因此修改Factor类的factor方法:

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();				
    		if(i==4){
    			l.add(2);
    			l.add(2);
    			return l;
    		}
    		if(i==6){
    			l.add(2);
    			l.add(3);
    			return l;
    		}
    		l.add(i);
    		return l;
    	}
    }
    

    测试通过!
    回头再看,i=4i=6的代码,发现有一个拆解的过程,都有一个共同的因子:2
    因此,考虑对代码进行优化:

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();
    		if(i%2==0){
    			l.add(2);
    			i=i/2;
    		}		
    		l.add(i);
    		return l;
    	}
    }
    

    运行测试:

    发现2不能通过,继续修改代码,排除2的情况:

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();
    		if(i%2==0 && i>2){
    			l.add(2);
    			i=i/2;
    		}		
    		l.add(i);
    		return l;
    	}
    }
    

    测试通过

    第六天###

    在此基础上,我们测试8:

    assertEquals(Arrays.asList(2,2,2), Factor.factor(8));
    

    发现测试不能通过!
    发现8包含多个2,在Factor类的factor方法中,似乎应该将if语句改成while循环:

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();
    		while(i%2==0 && i>2){
    			l.add(2);
    			i=i/2;
    		}		
    		l.add(i);
    		return l;
    	}
    }
    

    测试通过!

    第七天###

    测试9:

    assertEquals(Arrays.asList(3,3), Factor.factor(9));
    

    测试不通过!

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();
    		while(i%2==0 && i>2){
    			l.add(2);
    			i=i/2;
    		}
    		while(i%3==0 && i>3){
    			l.add(3);
    			i=i/3;
    		}
    		l.add(i);
    		return l;
    	}
    }
    

    测试通过!
    分别再对9,10,11,12,20进行测试:

    assertEquals(Arrays.asList(3,3), Factor.factor(9));
    assertEquals(Arrays.asList(2,5), Factor.factor(10));
    assertEquals(Arrays.asList(11), Factor.factor(11));
    assertEquals(Arrays.asList(2,2,3), Factor.factor(12));
    assertEquals(Arrays.asList(2,2,5), Factor.factor(20));
    

    测试通过:

    第八天###

    测试25:

    assertEquals(Arrays.asList(5,5), Factor.factor(25));
    

    测试不能通过!原因很简单,代码里面没有对因子5再做处理!
    最直观地:

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();
    		while(i%2==0 && i>2){
    			l.add(2);
    			i=i/2;
    		}
    		while(i%3==0 && i>3){
    			l.add(3);
    			i=i/3;
    		}
    		while(i%5==0 && i>5){
    			l.add(5);
    			i=i/5;
    		}
    		l.add(i);
    		return l;
    	}
    }
    

    测试通过!
    这时候,我们需要停下脚步,优化代码,消除重复。
    在TestCase的保护下,考虑对代码进行修改:

    public class Factor {
    	public static List<Integer> factor(int i){
    		List<Integer> l=new ArrayList<Integer>();
    		int k=2;
    		while(k<i){
    			while(i%k==0 && i>k){
    				l.add(k);
    				i=i/k;
    			}
    			k++;
    		}		
    		l.add(i);
    		return l;
    	}
    }
    

    测试通过!
    考虑一个复杂的TestCase:

    assertEquals(Arrays.asList(2,2,2,3,5,7,7,11), Factor.factor(2*2*2*3*5*7*7*11));
    

    测试通过!

    回头看看这段代码是否可接受?

    小结##

    测试驱动开发:

    • 能够保证编写单元测试
    • 使程序员坚持编写测试
    • 有助于澄清接口和行为的细节
    • 可证明、可再现、自动的验证
    • 增强改变事物的信心
  • 相关阅读:
    福大软工 · 第十二次作业
    Beta 冲刺(7/7)
    Beta 冲刺(6/7)
    Beta 冲刺(5/7)
    Beta 冲刺(4/7)
    Beta 冲刺(3/7)
    Beta 冲刺(2/7)
    福大软工 · 第十次作业
    Beta 冲刺(1/7)
    64位 CentOS NDK 编译 FFMPEG
  • 原文地址:https://www.cnblogs.com/happyzm/p/7468975.html
Copyright © 2020-2023  润新知