简介
Lite-junit是模仿junit3.8写的一个测试框架,里面用到了以下设计模式,是很好的编程练习:
- 命令模式: 每个TestCase都有个run方法,run内部包含了业务代码,但是对TestCase来讲不用管, 只要运行run即可
- 观察者模式: TestResult是主题, Listener是观察者, 解耦了测试结果和结果显示逻辑
- 参数收集: TestResult收集了测试结果, 和测试用例的运行进行了解耦
- 组合模式: TestSuite组合了TestCase, 因此可以对类、包、模块进行测试
- 装饰器模式: 在Test外面包裹了装饰器, 从而实现重复运行测试用例或者对包整体进行setUp, tearDown
- 模板方法: setUp, runTest(), tearDown()
- 协议: 规定了public static Test suite(), 测试用例以test开头, 从而方便反射的运用
乍一看,是不是很晕菜,没事,我一点一点讲给大家听。
Lite-junit中的类主要有这几种:
TestCase
测试的最小单元,可以认为每个测试用例都是一个TestCase
对象,这里的测试用例是以test开头的方法。在进行测试的时候,测试框架会为每个以test开头的方法创建一个TestCase,这样各个测试方法互相在内存上就是隔离的。任何测试类都需要继承TestCase
。
我们可以大致看一下TestCase
的定义, 这里面用到了命令模式,不用管测试方法的业务代码,都是直接调用run
方法即可以运行测试用例。模板方法也是一眼就能看出来的,在doRun
里面,运行顺序是setUp
->runTest
->tearDown
,那么任何继承自TestCase
的测试类都会按这个顺序来执行,setUp
和tearDown
的名字是定好的,不能随意修改:
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做的就是这件事。仔细看 TestCase
的 run(TestResult tr)
方法,发现传入一个TestResult
,而后会运行tr.run(this)
,在这里面TestResult又回调了TestCase
的doRun
方法:
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
测试用例在执行时,可能会展示为图表等内容,最好的方式就是观察者模式了,仔细看TestResult
的run
方法里面有几个方法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里面,然后一股脑儿进行测试,最后由TestResult
和TestListener
来把结果统计出来。
测试用例的准备
如上图所示的java文件,我们要测试三个类
Calculator
, Byebye
, HelloWorld
,因此我们写了三个测试类(都继承了TestCase
): CalculatorTest
, ByebyeTest
, HelloWorldTest
,我又约定了需要实现suite
方法的函数(暂时不用理会RepeatedTest
和SetUpTest
,后面会讲到):
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!");
}
};
}
}
AAllTest
、BAllTest
、AllTest
就把这几层文件的测试用例都组织起来了,最后在前文中的TestRunner
中传入AllTest
类,调用它的suite
方法就可以对所有类进行测试了。
TestDecorate
有时候,想重复执行某个用例,或者相对某个测试集合有个整体的setUp,那么可以用装饰器来做到这一点。
类图如下所示:
实现很简单,就不展示代码了。
用例图和类图
下图是类图:
下图是用例图,???是TestRunner
总结
感觉写文字很难,别人看我写的文字估计是一头雾水,而且写到后面自己都不太高兴写了,用例图和类图写的很粗浅,以后慢慢增强写作能力,希望大家能逐渐看懂我的文字。