收集測試結果


繼 續 建立測試案例 的故事,測試人員又來了:「你的執行器提供的測試訊息怎麼都是文字模式?如果哪天要顯示在視窗模式下怎麼辦?還有!可以多些測試訊息嗎?只顯示成功或失敗 其實沒啥幫助耶!」

他的抱怨也不是沒有道理,那麼,所有的錯誤就先用例外(Exception)丟出好了,到時接住(Catch)例外,再看怎麼顯示訊息好了。所以,你改了 一下Assert類 別:
package test.cc.openhome;
public class Assert {
    public static void assertEquals(String message, int expected, int result) {
        if(expected != result) {
            if(message == null) {
                throw new RuntimeException(String.format(
                        "失敗,預期為 %d,但是傳回 %d!", expected, result));
            }
            throw new RuntimeException(String.format(message, expected, result));
        }
    }
    public static void assertEquals(int expected, int result) {
        assertEquals(null, expected, result);
    }
}

你很貼心,這次修改後還可以讓測試人員自訂錯誤訊息。不過接下來比較麻煩,你要收集錯誤訊息,所以你先定義一個TestResult
package test.cc.openhome;
import java.util.*;
public class TestResult {
    private List<String> messages = new ArrayList<String>();
    public void run(TestCase test) {
        try {
            test.run();
        }
        catch(Throwable t) {
            messages.add(t.getMessage());
        }
    }
    public List<String> getMessages() {
        return messages;
    }
}

測試失敗時會丟出例外,例外發生時收集訊息。

接著要改比較多東 西了,因為要讓這個TestResult進入TestCase收集訊息。首先你調整了一下Test介面的定義:
package test.cc.openhome;
public interface Test {
    void runTest(TestResult result);
}

由於介面定義改了,實作Test介面的相關類別都得調整一下,以下紅字部份是有調整的:
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 runTest(TestResult result) {
        result.run(this);
    }
   
    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 {
            // invoke 中發生所有例外,一律會用InvocationTargetException包裹
            runMethod.invoke(this, new Class[0]);
        }
        catch(InvocationTargetException e) {
            // 這邊要取得
InvocationTargetException 中的目標例外,才是真正的錯誤訊息
            throw new RuntimeException(this.getClass() + "." +
              runMethod.getName() + ": " + e.getTargetException().getMessage());

        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
   
    public String getName() {
        return fName;
    }
   
    public void setName(String name) {
        fName = name;
    }
}

這樣TestCase就 調整好了,由於你還會使用TestSuite來 組合測試,而且它也實作了Test介 面,所以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) {
        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) constructor.newInstance();
                    testCase.setName(method.getName());
                    add(testCase);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    @Override
    public void runTest(TestResult result) {
        for (Test test : tests) {
            test.runTest(result);
        }
    }

    public void add(Test test) {
        tests.add(test);
    }
    public void add(Class clz) {
        tests.add(new TestSuite());
    }
    public List<Test> get() {
        return tests;
    }
}

再來是 TestRunner了,原本的TestRunner改一下,建立TestResult,執行測試以收集資訊:
package test.cc.openhome;
public class TestRunner {
    public static TestResult run(Test test) {
        TestResult result = new TestResult();
        test.runTest(result);
        return result;
    }
    public static TestResult run(Class clz) {
        return run(new TestSuite(clz));
    }
}

如果要個文字模式的顯示,那就設計個TextTestRunner好了:
package test.cc.openhome;
public class TextTestRunner {
    public static void run(Test test) {
        TestResult result = TestRunner.run(test);
        for(String message : result.getMessages()) {
            System.out.println(message);
        }
       
    }
    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;
   
    @Override
    protected void setUp() {
        calculator = new Calculator();
    }

    @Override
    protected void tearDown() {
        calculator = null;
    }

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

testPlus() 與testMinus()中的expected我故意寫錯了,所以測試一定會失敗,這時 測試人員會看到的訊息是:
class test.cc.openhome.CalculatorTest.testPlus: 失敗,預期為 2,但是傳回 5!
class test.cc.openhome.CalculatorTest.testMinus: 失敗,預期為 2,但是傳回 1!

事實上,這個故事發生前,你若知道有個 JUnit 測試框架,你什麼都不用作,人家都幫你寫好了,而且更完善,JUnit 基本原理都說完了,所以接下來,可以開始介紹 JUnit 了。。XD