单元测试

一、单元测试和TDD

  • 编程时需理清思路,将编程需求等想好,再开始编。此部分可用伪代码实现。
  • 用编程语言将伪代码翻译一下,就是产品代码了。伪代码是产品代码最好的注释。
  • 写了产品代码,还要写测试代码来证明自己的代码没有问题。Java编程时对类实现的测试叫单元测试。
关于单元测试的练习

需求:在一个MyUtil类中解决一个百分制成绩转成“优、良、中、及格、不及格”五级制成绩的功能。

根据需求撰写伪代码

百分制转五分制:
   如果成绩小于60,转成“不及格”
   如果成绩在60与70之间,转成“及格”
   如果成绩在70与80之间,转成“中等”
   如果成绩在80与90之间,转成“良好”
   如果成绩在90与100之间,转成“优秀”
   其他,转成“错误”

用Java程序语言翻译伪代码为产品代码

package experiment2;
public class MyUtil {
    public static String percentage2fivegrade(int grade){
        //如果成绩小于0,转成“错误”
        if (grade<0) return "错误";
        //如果成绩小于60,转成“不及格”
        if (grade<60) return "不及格";
        //如果成绩在60到70之间,转成“及格”
        else if (grade<70) return "及格";
        //如果成绩在70到80之间,转成“中等”
        else if (grade<80) return "中等";
        //如果成绩在80到90之间,转成“良好”
        else if (grade<90) return "良好";
        //如果成绩在90到100之间,转成“优秀”
        else if (grade<=100) return "优秀";
        //其他,转成“错误”
        else return "错误";
    }
}

接下来就是撰写测试代码,判断自己的产品代码有没有出错了: 选中MyUtil,在左边出现的小灯泡下拉菜单中选择Create Test:

java 伪码是什么 java程序转为伪代码_java

接着在创建出的MyUtilTest撰写测试代码:

import junit.framework.TestCase;
import org.junit.Test;
public class MyUtilTest extends TestCase {
    @Test
    public void testNormal() {
        assertEquals("不及格", MyUtil.percentage2fivegrade(55));
        assertEquals("及格", MyUtil.percentage2fivegrade(65));
        assertEquals("中等", MyUtil.percentage2fivegrade(75));
        assertEquals("良好", MyUtil.percentage2fivegrade(85));
        assertEquals("优秀", MyUtil.percentage2fivegrade(95));
    }

    @Test
    public void testException(){
        assertEquals("错误",MyUtil.percentage2fivegrade(105));
        assertEquals("错误",MyUtil.percentage2fivegrade(-55));
    }

    @Test
    public void testBoundary(){
        assertEquals("不及格",MyUtil.percentage2fivegrade(0));
        assertEquals("及格",MyUtil.percentage2fivegrade(60));
        assertEquals("中等",MyUtil.percentage2fivegrade(70));
        assertEquals("良好",MyUtil.percentage2fivegrade(80));
        assertEquals("优秀",MyUtil.percentage2fivegrade(90));
        assertEquals("优秀",MyUtil.percentage2fivegrade(100));
    }
}

以上测试代码中的testNormal()方法用于测试一般情况,而testException()和testBoundary()则是对意外情况和边界进行测试。如果三种测试中的一种没有通过,IDEA会提示是哪一块出错,以及出了什么错误:

java 伪码是什么 java程序转为伪代码_设计模式_02

我们可以从图中看出是边缘测试没有通过,原本应该显示“优秀”的显示了“错误”。于是回去看MyUtil类:

java 伪码是什么 java程序转为伪代码_java_03

发现是设置“优秀”的范围时没有包含100分(我刚刚改的),所以100分便被归到了“错误”中。修改以后再次运行MyUtilTest,可看到三个测试都通过:

java 伪码是什么 java程序转为伪代码_java_04

由此可看出,除了对普通数据的测试之外,边界情况和非法情况的测试也十分重要。

以TDD的方式研究学习StringBuffer

在老师的博客《积极主动敲代码,使用JUnit学习Java》中,给出了一个用于学习StringBuffer几个方法的程序:

public class StringBufferDemo{        
   public static void main(String [] args){    
       StringBuffer buffer = new StringBuffer();    
       buffer.append('S');     
       buffer.append("tringBuffer");     
       System.out.println(buffer.charAt(1));     
       System.out.println(buffer.capacity();     
       System.out.println(buffer.indexOf("tring"));    
       System.out.println("buffer = " + buffer.toString());    
  }    
}

了解这几个方法以后,选择了charAt(),length(),capacity()这三个方法,开始撰写产品代码:

public class StringBufferDemo {
    StringBuffer buffer=new StringBuffer();
    public Character charAt(int i){
        return buffer.charAt(i);
    }
    public int length(){
        return buffer.length();
    }
    public int capacity(){
        return buffer.capacity();
    }
}

接着是撰写测试代码,来测试这三个方法是否正确:

import junit.framework.TestCase;
import org.junit.Test;

public class StringBufferDemoTest extends TestCase {
    StringBuffer a=new StringBuffer("yesterday");
    StringBuffer b=new StringBuffer("tomorrowisnotnow");
    StringBuffer c=new StringBuffer("liveherenowtodayisneccessary");
    
    @Test
    public void testcharAt(){
        assertEquals('y',a.charAt(0));
        assertEquals('r',b.charAt(5));
        assertEquals('n',c.charAt(8));
    }
    
    @Test
    public void testlength(){
        assertEquals(9,a.length());
        assertEquals(16,b.length());
        assertEquals(28,c.length());
    }
    
    @Test
    public void testcapacity(){
        assertEquals(25,a.capacity());
        assertEquals(32,b.capacity());
        assertEquals(44,c.capacity());
    }
}

在测试代码中,我设定了三个长度逐渐增加的字符串。运行后,显示测试成功。

以TDD的方式开发一个复数类Complex

要求:定义属性并生成getter,setter;定义构造函数;定义公有方法(加减乘除)。

测试代码:

import junit.framework.TestCase;
import org.junit.Test;

public class ComplexTest extends TestCase {
    Complex a=new Complex(1,2);
    Complex b=new Complex(-2,-1);
    Complex c=new Complex(4,-2);
    Complex d=new Complex(4,-2);
    @Test
    public void testequals(){
        assertEquals(false,a.equals(b));
        assertEquals(false,b.equals(c));
        assertEquals(true,c.equals(d));
    }

    @Test
    public void testAdd(){
        assertEquals(new Complex(-1,1),a.ComplexAdd(b));
        assertEquals(new Complex(5,0),a.ComplexAdd(c));
    }

    @Test
    public void testSub(){
        assertEquals(new Complex(3,3),a.ComplexSub(b));
        assertEquals(new Complex(-3,4),a.ComplexSub(c));
    }

    @Test
    public void testMulti(){
        assertEquals(new Complex(0,-5),a.ComplexMulti(b));
        assertEquals(new Complex(8,6),a.ComplexMulti(c));
    }

    @Test
    public void testDiv(){
        assertEquals(new Complex(0,0.5),a.ComplexDiv(c));
        assertEquals(new Complex(-0.3,-0.4),b.ComplexDiv(c));
    }
}

产品代码:

public class Complex {
    // 定义属性并生成getter,setter
    private double r;
    private double i;
    // 定义构造函数
    public Complex(double r,double i){
        this.r=r;
        this.i=i;
    }
    public static double getRealPart(double r){
        return r;
    }
    public static double getImagePart(double i){
        return i;
    }

    //Override Object
    public boolean equals(Object obj){

        Complex complex=(Complex) obj;
        if (complex.r!=r) {
            return false;
        }
        if(complex.i!=i){
            return false;
        }
        return true;
    }
    public String toString(){
        String str=new String();
        if (i==0) str=r+"";
        else if(i<0) str=r + ""+i+"i";
        else str=r+""+"+"+i+"i";
        return str;
    }

    // 定义公有方法:加减乘除
    Complex ComplexAdd(Complex a){
        return new Complex(r+a.r,i+a.i);
    }
    Complex ComplexSub(Complex a){
        return new Complex(r-a.r,i-a.i);
    }
    Complex ComplexMulti(Complex a){
        return new Complex(r*a.r-i*a.i,r*a.i+i*a.r);
    }
    Complex ComplexDiv(Complex a){
        return new Complex((r*a.r+i*a.i)/(a.r*a.r+a.i*a.i),(i*a.r-r*a.i)/(a.r*a.r+a.i*a.i));
    }
}

测试成功截图:

java 伪码是什么 java程序转为伪代码_设计模式_05

二、面向对象三要素

面向对象的三要素是封装、继承与多态。为借助抽象思维用好三要素:SOLID原则给出了指导。

  • SRP,单一职责原则;
  • OCP,开放-封闭原则;
  • LSP,替换原则;
  • ISP,接口分离原则;
  • DIP,依赖倒置原则。
练习,体会OCP原则和DIP原则的应用

老师给出的设计模式示例代码如下:

// Server Classes 
abstract class Data { 
    abstract public void DisplayValue(); 
}
class Integer extends  Data {    
    int value; 
    Integer() {
         value=100; 
    }  
    public void DisplayValue(){
        System.out.println (value);
    } 
 } 
// Pattern Classes 
abstract class Factory { 
   abstract public Data CreateDataObject(); 
}
class IntFactory extends Factory { 
   public Data CreateDataObject(){
     return new Integer(); 
   } 
} 
//Client classes 
class Document {    
    Data pd; 
    Document(Factory pf){ 
       pd = pf.CreateDataObject(); 
    } 
    public void DisplayData(){
       pd.DisplayValue(); 
   } 
 } 
 //Test class
 public class MyDoc {
    static Document d;
    public static void main(String[] args) {
            d = new Document(new IntFactory()); 
            d.DisplayData(); 
    }   
}

用我的学号(28)模6取余,得到4,须根据以下结果进行代码扩充:让系统支持Float类,并在MyDoc类中添加测试代码表明添加正确。

abstract class Data {
        abstract public void DisplayValue();
    }
    class Integer extends  Data {
        int value;
        Integer() {
            value=100;
        }
        public void DisplayValue(){
            System.out.println (value);
        }
    }
    class Float extends Data{
        float value;
        Float(){
            value=20155328;
        }
        public void DisplayValue(){
            System.out.println(value);
        }
    }
    // Pattern Classes
    abstract class Factory {
        abstract public Data CreateDataObject();
    }
    class IntFactory extends Factory {
        public Data CreateDataObject(){
            return new Integer();
        }
    }
    class FloatFactory extends Factory{
        public Data CreateDataObject(){
            return new Float();
        }
    }

测试代码如下:

//Test class
    public class MyDoc {
        static Document d;
        public static void main(String[] args) {
            d = new Document(new FloatFactory());
            d.DisplayData();
        }
    }
使用StarUML进行建模,画类图

java 伪码是什么 java程序转为伪代码_测试_06

三、实验中遇到的问题及解决方式

  • 问题1:对StringBuffer的capacity()方法理解不太清晰。
  • 解决方法:参考StringBuffer的Capacity详解及实践,知StringBuffer的默认大小是16,即如果长度小于16则默认容量为16,当StringBuffer达到最大容量时,会将最大容量增加到原来容量的两倍再加2.
  • 问题2:在测试Complex类时,出现了这样的red bar:

java 伪码是什么 java程序转为伪代码_java_07

  • 解决方法:由图可知是equals()方法的撰写出现了问题,于是回到Complex中重新撰写如下:
public boolean equals(Object obj){

        Complex complex=(Complex) obj;
        if (complex.r!=r) {
            return false;
        }
        if(complex.i!=i){
            return false;
        }
        return true;
    }

然后再次测试,显示测试成功。

四、实验体会

相比起第一次实验,觉得这次实验让自己受益匪浅。在实验课前花了不少的时间跟着老师的step by step教程慢慢学习,体会到了单元测试的实用性。以前写代码就算会自己带一些测试之类的东西来看看是否正确,但一旦结果出错也需要耗费较长的时间去找出错误,而单元测试就可以通过red bar和错误提示很快找到错误源头,然后解决。希望自己以后可以更好更熟练的掌握TDD编程方法吧。