Lite-junit设计归纳

代码见lite-junit源代码

简介

Lite-junit是模仿junit3.8写的一个测试框架,里面用到了以下设计模式,是很好的编程练习:

  1. 命令模式: 每个TestCase都有个run方法,run内部包含了业务代码,但是对TestCase来讲不用管, 只要运行run即可
  2. 观察者模式: TestResult是主题, Listener是观察者, 解耦了测试结果和结果显示逻辑
  3. 参数收集: TestResult收集了测试结果, 和测试用例的运行进行了解耦
  4. 组合模式: TestSuite组合了TestCase, 因此可以对类、包、模块进行测试
  5. 装饰器模式: 在Test外面包裹了装饰器, 从而实现重复运行测试用例或者对包整体进行setUp, tearDown
  6. 模板方法: setUp, runTest(), tearDown()
  7. 协议: 规定了public static Test suite(), 测试用例以test开头, 从而方便反射的运用

乍一看,是不是很晕菜,没事,我一点一点讲给大家听。
Lite-junit中的类主要有这几种:

TestCase

测试的最小单元,可以认为每个测试用例都是一个TestCase对象,这里的测试用例是以test开头的方法。在进行测试的时候,测试框架会为每个以test开头的方法创建一个TestCase,这样各个测试方法互相在内存上就是隔离的。任何测试类都需要继承TestCase
我们可以大致看一下TestCase的定义, 这里面用到了命令模式,不用管测试方法的业务代码,都是直接调用run方法即可以运行测试用例。模板方法也是一眼就能看出来的,在doRun里面,运行顺序是setUp->runTest->tearDown,那么任何继承自TestCase的测试类都会按这个顺序来执行,setUptearDown的名字是定好的,不能随意修改:

public abstract class TestCase extends Assert implements Test {
    public void run(TestResult tr) {
        tr.run(this);
    }
    public void doRun() throws Throwable {
        setUp();
        try{
            runTest();
        }
        finally{
            tearDown();
        }
    }
}

而测试类一般写成这样:

public class CalculatorTest extends TestCase {
    public void setUp(){
        cal = new Calculator();
    }
    public void tearDown(){
    }
    public void testAdd()
    {
        Calculator cal = new Calculator();
        int result = cal.add(1, 2);
        //断言assert
        Assert.assertEquals(3, result);
    }
    public void testMultiply()
    {
        Calculator cal = new Calculator();
        int result = cal.multiply(4, 2);
        Assert.assertEquals(8,result);
    }
}

TestSuite

测试用例一定是好几个放在一起执行的,因此需要有个容器来存放测试类,这个容器就是TestSuite,它是组合模式应用的范例,它内部有个 Test的列表,装了一堆测试用例,可以是TestCase,也可以是TestSuite,当调用run方法的时候,就能递归得执行到最底层的测试用例了:

public class TestSuite implements Test {
    private List<Test> tests = new ArrayList<>(10);

    @Override
    public void run(TestResult tr) {
        for (Test test: tests) {
            if (tr.shouldStop()) break;
            test.run(tr);
        }
    }

    public void addTest(Test test) {
        tests.add(test);
    }
}

TestResult

测试用例的执行过程中,往往需要将测试结果保存到一个地方,并且希望测试过程和测试结果能够相对隔离,TestResult做的就是这件事。仔细看 TestCaserun(TestResult tr)方法,发现传入一个TestResult,而后会运行tr.run(this),在这里面TestResult又回调了TestCasedoRun方法:

public class TestResult {
    protected List<TestFailure> failures;  // 存储assert失败
    protected List<TestFailure> errors;  // 存储业务代码异常,比如空指针
    private List<TestListener> listeners;  // 观察者列表
    private int testCount;  // 执行用例个数
    public void run(TestCase testCase) {
        startTest(testCase);
        try {
            testCase.doRun();  // 回调TestCase的doRun()方法
        } catch (AssertionFailedError e) {
            addFailure(testCase, e);
        } catch (Throwable e) {
            addError(testCase, e);
        }
        endTest(testCase);
    }    

这种把错误收集到一个专门类的方法,叫收集参数模式

Listener

测试用例在执行时,可能会展示为图表等内容,最好的方式就是观察者模式了,仔细看TestResultrun方法里面有几个方法addError, addFailure, startTest, endTest,它们就是观察者的方法, 其实现也定义在TestResult里面,一眼就看出这是最基本的观察者模式:

    private void addError(TestCase testCase, Throwable e) {
        errors.add(new TestFailure(testCase, e));
        for (TestListener listener: listeners) {
            listener.addError(testCase, e);
        }
    }

    private void addFailure(TestCase testCase, AssertionFailedError e) {
        failures.add(new TestFailure(testCase, e));
        for (TestListener listener: listeners) {
            listener.addFailure(testCase, e);
        }
    }

    private void startTest(TestCase testCase) {
        int count= testCase.countTestCases();
        testCount+= count;
        for (TestListener listener: listeners) {
            listener.startTest(testCase);
        }
    }

    private void endTest(TestCase testCase) {
        for (TestListener listener: listeners) {
            listener.endTest(testCase);
        }
    }

我们可以定义一个观察者接口:

public interface TestListener {
    void addError(Test test, Throwable t);
    void addFailure(Test test, Throwable t);
    void startTest(Test test);
    void endTest(Test test);
}

然后自己实现几个观察者,把它们加入到TestResult里面去即可。

TestRunner

Lite-junit的基础类都介绍完了,最后肯定要介绍一下如何触发测试,代码中是通过TestRunner来做到的,该类同时继承了TestListener,会顺便把自己加入到TestResult观察者列表中:

public class TestRunner implements TestListener {
    private static final String SUITE = "suite";
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        TestRunner testRunner = new TestRunner();
        TestResult tr = new TestResult();
        tr.addListener(testRunner);  // 添加listener
        String className = "com.coderising.myood.litejunit.v2.example_test.AllTest";
        Class caseClazz = Class.forName(className);
        Method suiteMethod = caseClazz.getMethod(SUITE, new Class[0]);
        Test suite = (Test) suiteMethod.invoke(null);
        testRunner.tryTest(suite, tr);
    }

    private void tryTest(Test suite, TestResult tr) {
        suite.run(tr);
        System.out.println();
        print(tr);
    }
}

代码很简单,不过有一个很值得观察的点:就是Case是如何组合到一起来执行的呢?代码中的AllTest类有个suite方法,返回的是TestSuite对象,调用它的run方法,也就触发了所有的测试,那么立即可以联想到AllTest里面肯定包含了许许多多的测试用例了。
不错,Lite-junit希望能够把测试用例按照java文件的组织方式把测试用例一个一个加到suite里面,然后一股脑儿进行测试,最后由TestResultTestListener来把结果统计出来。

测试用例的准备

image.png

如上图所示的java文件,我们要测试三个类Calculator, Byebye, HelloWorld,因此我们写了三个测试类(都继承了TestCase): CalculatorTest, ByebyeTest, HelloWorldTest,我又约定了需要实现suite方法的函数(暂时不用理会RepeatedTestSetUpTest,后面会讲到):

public class AAllTest {
    public static Test suite() {
        TestSuite suite = new TestSuite();
        suite.addTestSuite(CalculatorTest.class);
        return new SetUpTest(suite) {

            @Override
            protected void setUp() {
                System.out.println("AAll start!");
            }

            @Override
            protected void tearDown() {
                System.out.println("AAll end!");
            }
        };
    }
}
public class BAllTest {
    public static Test suite() {
        TestSuite suite = new TestSuite();
        suite.addTestSuite(HelloWorldTest.class);
        suite.addTestSuite(ByebyeTest.class);
        return suite;
    }
}
public class AllTest {
    public static Test suite() {
        TestSuite suite = new TestSuite();
        suite.addTest(new RepeatedTest(AAllTest.suite(), 2));  // 5 * 2个用例
        suite.addTest(BAllTest.suite());  //  2个用例
        return new SetUpTest(suite) {
            @Override
            protected void setUp() {
                System.out.printf("All start!\n");
            }

            @Override
            protected void tearDown() {
                System.out.printf("All end!");
            }
        };
    }
}

AAllTestBAllTestAllTest就把这几层文件的测试用例都组织起来了,最后在前文中的TestRunner中传入AllTest类,调用它的suite方法就可以对所有类进行测试了。

TestDecorate

有时候,想重复执行某个用例,或者相对某个测试集合有个整体的setUp,那么可以用装饰器来做到这一点。
类图如下所示:


image.png

实现很简单,就不展示代码了。

用例图和类图

下图是类图:



下图是用例图,???是TestRunner


image.png

总结

感觉写文字很难,别人看我写的文字估计是一头雾水,而且写到后面自己都不太高兴写了,用例图和类图写的很粗浅,以后慢慢增强写作能力,希望大家能逐渐看懂我的文字。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容