建立測試案例


繼 續 自由組合測試 的故事,測試人員又在抱怨了,每次要寫測試,就得自己實作一個Test介面,著實有點麻 煩,你想了一下,也對!要不這樣好了,測試人員只要在某個類別中寫個公開、不傳回值、沒有參數的testXXX()方法, 你的執行器自動找出當中的方法並執行如何?而且你比他多考慮了一些東西,所以,你先設計了一個TestCase
package test.cc.openhome;
import java.lang.reflect.*;
public class TestCase extends Assert implements Test {
    private String fName;

    public TestCase() {}
    public TestCase(String name) {
        fName = name;
    }
   
    protected void setUp() {}
    protected void tearDown() {}

    @Override
    public void run() {
        setUp();
        runTest();
        tearDown();
    }
   
    public void runTest() {
        Method runMethod= null;
        try {
            runMethod= getClass().getMethod(fName, null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        if (!Modifier.isPublic(runMethod.getModifiers())) {
            throw new RuntimeException("方法 \"" + fName + "\" 必須是 public");
        }
        try {
            runMethod.invoke(this, new Class[0]);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
   
    public String getName() {
        return fName;
    }
   
    public void setName(String name) {
        fName = name;
    }
}

測試人員要撰寫各個測試案例的話,要繼承TestCase實作子類別,每個測試案 例實作一個testXXX()方法。例如:
package test.cc.openhome;
import cc.openhome.Calculator;
public class CalculatorPlusMinusTest extends TestCase {
    public CalculatorPlusMinusTest(String name) {
        super(name);
    }
   
    public void testPlus() {
        Calculator calculator = new Calculator();
        int expected = 5;
        int result = calculator.plus(3, 2);
        assertEquals(expected, result);
    }
   
    public void testMinus() {
        Calculator calculator = new Calculator();
        int expected = 1;
        int result = calculator.minus(3, 2);
        assertEquals(expected, result);
    }
   
    public static Test suite() {
        TestSuite suite = new TestSuite();
        suite.add(new CalculatorPlusMinusTest("testPlus"));
        suite.add(new CalculatorPlusMinusTest("testMinus"));
        return suite;
    }
    public static void main(String[] args) {
        TestRunner.run(suite());
    }
}


有好一些嗎?好像是有,至少不用分別建立Test的實作類別了,然而你還可以讓測試人員更方便一些。你想在執行測試時,用每個testXXX()方法名稱 來建構TestCase的子類別實例
,而後運用反射(Reflection)來執行那些testXXX()方法, 讓測試人員不用自己去作一些建立物件的動作。為此,你要重構TestSuite
package test.cc.openhome;
import java.lang.reflect.*;
import java.util.*;
public class TestSuite implements Test {
    private List<Test> tests = new ArrayList<Test>();

    public TestSuite() {}
    public TestSuite(Class clz) {
        // 找出每個test開頭的方法
        Method[] methods = clz.getDeclaredMethods();
        for (Method method : methods) {
            if (Modifier.isPublic(method.getModifiers())
                    && method.getName().startsWith("test")) {
                Constructor constructor = null;
                try {
                    constructor = clz.getConstructor();
                    // 建立TestCase實例
                    TestCase testCase = (TestCase) constructor.newInstance();
                    // 設定要呼叫的testXXX()方法
                    testCase.setName(method.getName());
                    // 加入測試
                    add(testCase);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    @Override
    public void run() {
        for (Test test : tests) {
            test.run();
        }
    }

    public void add(Test test) {
        tests.add(test);
    }
    public void add(Class clz) {
        tests.add(new TestSuite(clz));
    }
}

你的TestRunner也 作了些小小的修改:
package test.cc.openhome;
public class TestRunner {
    public static void run(Test test) {
        test.run();
    }
    public static void run(Class clz) {
        run(new TestSuite(clz));
    }
}

現在,如果測試人員要寫測試,只需要:
package test.cc.openhome;
import cc.openhome.Calculator;
public class CalculatorTest extends TestCase {
    private Calculator calculator;

    public CalculatorTest() {}  
    public CalculatorTest(String name) {
        super(name);
    }
   
    @Override
    protected void setUp() {
        calculator = new Calculator();
    }
    @Override
    protected void tearDown() {
        calculator = null;
    }

    public void testPlus() {
        int expected = 5;
        int result = calculator.plus(3, 2);
        assertEquals(expected, result);
    }
   
    public void testMinus() {
        int expected = 1;
        int result = calculator.minus(3, 2);
        assertEquals(expected, result);
    }
   
    public static void main(String[] args) {
        TestRunner.run(CalculatorTest.class);
    }
}

省事多了,而且注 意到setUp()與tearDown()的使用,由於TestCase中的run()設計為每次都會呼叫setUp()、testXXX()、 tearDown(),所以setUp()與tearDown()可 用來作為每個testXXX()方法準備測試裝備(Test fixture),這應該算是 Template Method 模式 的實現。

現在測試人員撰寫測試時,彈性也多了許多,可以使用TestSuite自由地組合測試案例中 某些testXXX()方法,或者是直接運行所有的testXXX()方法,每個測試案例也都可以再結合起來。例如:
package test.cc.openhome;
public class CalculatorAll {
    public static Test suite() {
        TestSuite suite = new TestSuite();
        // 隨便你組合
        suite.add(CalculatorPlusMinusTest.suite());
        suite.add(CalculatorTest.class);
        suite.add(new CalculatorTest("testPlus"));
        return suite;
    }
    public static void main(String[] args) {
        TestRunner.run(suite());
    }
}