JUnit4 已經在業界運用很長時間了。相對於 JUnit3 ,版本 4 做了很大量的改進。主要是針對使用 annotation使得使用者有更多客戶化的定製,為了提供更多更靈活的測試手段。早在版本3的時候JUnit就完美地符合了一個單元測試架構的3個要求:
-
每個單元測試必須獨立於其他的單元測試
- 每個單元測試中產生的錯誤必須被記錄下來
- 使用者能夠輕鬆指定要執行的單元測試
在版本4中間這些優良特性被保留,而且加入了更多的測試靈活性。
JUnit 的代碼不算巨大,但其以高密度的設計模式和靈活性使大家對 JUnit 架構評價很高。作為 JUnit3
的後續版本它仍舊延續了高密度的設計模式的風格,而且代碼更為精巧。利用 Eclipse 的外掛程式CodePro Analytix
來測量
JUnit4 的結果是:抽象類別和介面的比例超過 22% ,平均每個方法的大小約為 5 行。
已經有很多文章描述如何使用 JUnit4 的新特性。此文不打算介紹這些新特性,只是分析和介紹 JUnit4 的設計架構,其版本為 4.8.2 。希望讀者對JUnit至少有初步的理解,這樣才能比較容易理解這篇文章。
JUnit4的輸入輸出:
作為一個程式,首先要知道它的輸入和輸出是什麼。
1. 輸入:JUnit4的輸入是待測的類。這裡需要說明的是這裡的輸入是類的Class對象,而非一個java對象。因為程式的入口是
JUnitCore.
runClasses(Class<?>...)
,也就是說參數是個Class對象,而非這個待測的類的執行個體。
2. 輸出:測試的結果,包括成功和失敗的case的原因,以及測試花費的時間等。
JUni4的總體結構:
總的來說可以分為2大步驟:
1. 首先對輸入的Class利用
org.junit.runners.model.RunnerBuilder
進行分類解析,把它組織成JUnit4內部對象
org.junit.runner.Runner
。
2. 然後調
Runner.run(
RunNotifier
)
來執行testcase,測試過程中的事件由
RunNotifier
來處理。
組織Runner:
JUnit4使用了Builder模式來產生
Runner
。在這個過程裡面,
RunnerBuilder
的子類扮演具體Builder角色。一種
RunnerBuilder
負責一種
Runner
的產生,而每個
Runner
代表一個待測類。
這個需要指出的是JUnit4利用
AllDefaultPossibilitie
sBuilder
類作為Director的角色,它本身就是一個
RunnerBuilder
,它通過對不同的class的特徵來分配對應的
RunnerBui
lder
。
下面是不同特徵的待測class和對應的
RunnerBuilder
,以及其產生的
Runner
的表:
Runner的繼承結構:
最後所有產生的Runner都被封裝到
org.junit.
runners.Suite
,
Suite
還是使用了Composite模式,它本身就是一個
Runner
,並且內部包含了
Runner
集合。也就是說
Suite
裡面可以嵌套
Suite
和其他的
Runner
。
需要特別說明的是
AnnotatedBuilder
,它是處理帶有
@RunWith
的待測類,為其產生
Runner
。這提供了客戶訂製
Runner
的強大功能。有時你需要做一些特別的功能你就可以繼承
Runner
,然後在待測類上標註
@RunWith(MyRunner.
class)
。這個時候
AnnotatedBuilder
會通過反射產生
MyRunne
r的執行個體。你在定義自己的
Runner
的時候就可以對測試的行為“為所欲為”,這個真的很酷。
JUnit4還帶了
Categories
,
Enclosed
和
Parameterized
這些
Runner
,它們都實現了很強大的功能。
說了這麼多不如舉個例子來說明,對於程式員來說有什麼比用代碼說明更清楚的:)
假設我們的待測類有JUnit3的類
Version3Test
, JUnit4的類
Version4Test
,Ignore的類
IgnoreTest
,以及含有靜態
suite()
方法的類
SuiteTest
。我們把
Version3Test
和
Version3Test
放到
Suite1
。 然後為了展示遞迴結構把
Suite1
再放入
Suite2
,順便把
IgnoreTest
和
SuiteTest
也放入
Suit
e2
。
代碼如下:
public class Version3Test extends TestCase{<br /> public void testIn3X(){}<br />}</p><p>public class Version4Test {<br /> @Test public void testIn4X(){}<br />}</p><p>@RunWith(Suite.class)<br />@Suite.SuiteClasses({<br /> Version3Test.class, Version4Test.class<br />})<br />public class Suite1 {}</p><p>@Ignore<br />public class IgnoreTest {<br /> public void testInIgnore(){}<br />}</p><p>public class SuiteTest {<br /> public static Test suite(){ return new TestSuite(); }<br />}</p><p>@RunWith(Suite.class)<br />@Suite.SuiteClasses({//Put all together<br /> Suite1.class, IgnoreTest.class, SuiteTest.class<br />}) public class Suite2 {}
如果是在Eclipse下面運行Suite2
,你就可以看到這樣的結構:
各種Runner的說明:
- JUnit38ClassRunner:它的run方法其實就是把3版本的運行機制搬到4版本。你可以在代碼裡面看到Test.run(TestResult)
這樣的代碼。
- SuiteMethod:它繼承於JUnit38ClassRunner
。這也是個巧妙的設計:SuiteMethod會在構造方法裡面利用反射執行靜態suite()
方法。suite()
方法的傳回值就是Test
對象,這樣就把問題變為版本3的測試問題。
- IgnoredClassRunner:這幾乎不用說了。既然希望忽略的測試,那麼這個Runner的run就是沒有執行測試內容。
- Suite:它是Composite結構的根,它包含了所有子Runner的集合。
- BlockJUnit4ClassRunner:這是全新的設計。對於提供了豐富訂製化的測試來說,原先junit.framework.TestCase.runBare()
方法裡面的Template模式已經完全不夠用了。因此在BlockJUnit4ClassRunner裡面引入了Statement
類的概念,實際上這是個Decorator模式。Statement裡面只有一個方法evaluate()
。一個方法evaluate()
代表一個操作:譬如@Before
是一個Statement
,@BeforeClass
也是一個Statement
,@Tes
t裡面定義的expected
和timeout
都是Statement
。當然被測試的方法本身也是一個Statement
。一個典型的帶有@Before
,@After
以及@Test(expected=Exception.class, timeout=X)
的 測試方法最終組成的Statement裝飾鏈表結構就是:RunAfters -> RunBefores -> FailOnTimeout -> ExpectedException -> InvokeMethod。
自己寫一個Runner:
接下來做點好玩的東西:自己寫一個Runner。在正常情況下,testcase的測試方法的執行順序就是定義測試方法的順序。現在我們自己寫一個按照測試方法名字升序執行的Runner。在
BlockJUnit4ClassRunner
裡面它其實會支援dummy的sort功能,如果我們重新實現這個sort就可以很便捷地自訂排序行為。
雖然這有悖單元測試互相獨立的原則,for fun:)/**<br /> * The Runner provides sort functionality by lexicographic test method name.<br /> *<br /> * @author 盧聲遠<michaellufhl@yahoo.com.cn><br /> */<br />public class SortRunner extends BlockJUnit4ClassRunner {<br /> /**<br /> * Create a Sorter.<br /> * @param klass<br /> * @throws InitializationError<br /> */<br /> public SortRunner(Class<?> klass) throws InitializationError {<br /> super(klass);<br /> Sorter sorter = new Sorter(new Comparator<Description>() {<br /> public int compare(Description o1, Description o2) {<br /> return o1.getMethodName().compareTo(o2.getMethodName());<br /> }});<br /> sort(sorter);<br /> }<br />}</p><p>@RunWith(SortRunner.class)<br />public class Version4Test {</p><p> @Test public void c(){}</p><p> @Test public void a(){}</p><p> @Test public void b(){}<br />}當執行結束後,你會發現執行順序是a->b->c,而不是預設的c->b->a。
關於@Rule: 為了增強訂製行為的高靈活度,JUnit4提供了
@Rule
機制。Rule變數必須實現
org.junit.rules.MethodRule
介面,使用者可以通過自訂@Rule來做些類似Interceptor的操作。而且JUnit4同時支援多個
MethodRule
。MethodRule介面只有一個方法
Statement apply(Statement base, FrameworkMethod method, Object target)
。參數base就是已經產生的Statement,通過apply方法使用者可以修飾已有的Statement base。@Rule機制是如此強大以至於JUnit4已經打算在後續版本把ExpectingException,timeout,Before和After的特性用Rule機制來實現。 下面繼續做點有趣的事情:定義一個可以訂製執行次數的Rule:@Retention(RetentionPolicy.RUNTIME)<br />@interface RunCount {<br /> int count() default 1;<br />}<br />/**<br /> * Define a repeatable Rule.<br /> * The repeat times is defined in annotation RunCount(count=X).<br /> *<br /> * @author 盧聲遠<michaellufhl@yahoo.com.cn><br /> */<br />class RepeatRule implements MethodRule { </p><p> public final Statement apply(final Statement base,<br /> final FrameworkMethod method, Object target) { </p><p> return new Statement() {<br /> @Override<br /> public void evaluate() throws Throwable {<br /> int count =1;//Default<br /> RunCount rCount= method.getAnnotation(RunCount.class) ;<br /> if (rCount != null)<br /> count=rCount.count(); </p><p> while(count-->0){<br /> base.evaluate();<br /> }<br /> }<br /> };<br /> }<br />}<br />public class RepeatTest { </p><p> @Rule<br /> public RepeatRule rule = new RepeatRule(); </p><p> @Test<br /> @RunCount(count=2)//Run twice<br /> public void testIt(){}<br />}
你可以看到testIt()執行了2次,而且參數Statement base其實已經是包含了
@Before
和
@After
等修飾的Statement。
最後:
JUnit4
的結構相比
3
版本變化是巨大的。代碼非常精緻,但同時代碼也相當複雜,充滿
callback
機制和大量的內部類。還有些比較
cool
的特性的代碼在包
org.junit.experimental
下面。譬如
裡面有各種各樣特性的
Runner
,有興趣大家可以看一下。原始碼可以到:
https://github.com/downloads/KentBeck/junit/junit-4.8.2.jar
下載。
參考資料:
- JUnit A Cook's Tour
- JUnit 4 in 60 Seconds
- JUnit in Action
: Vincent Massol, Ted Husted
作者:盧聲遠<michaellufhl@yahoo.com.cn>