未接触 JUnit 之前,曾经对茫茫的代码不知所措……哪怕是自己写的……多写注释?重构代码?甚至为一个方法去写一篇技术文章来解释?——这些都是试过,感觉不是“控制代码”的可行之道,甚至说”徒劳“的。关于单元测试(Unit test),之前亦略有所闻,感觉用处不大,因为对一个方法检测返回的结果是否正确,——有点无聊——心想,我写的方法当然能返回预期的结果,这还有说?不至于那么低级的错误也犯得着吧!?于是对所谓测试的东东感觉简直就是在增加工作量——我把代码写漂亮点就行了。
事实结果证明,无论你怎么把代码写得漂亮,终究还是你个人主观的愿望在作崇——因为你不能既当运动员,又当裁判员。
还是来自台湾的朋友说得好,我把问题一五一十地说下:
試想一個情境,當有一個同事離職了,老闆交辦要開始維護他所留下來的程式,時程上不太允許重寫程式,老闆也不會答應。那要怎麼開始的呢?先看文件?然後再閱讀程式碼,一邊碎念,一邊感嘆自己的悲情?假以時日後,才漸漸懂得如何運用前同事的程式?在這過程中,需求還是不斷進來,必須對原來已經穩定的程式加以修修改改,一不小心又藏了一隻 bug 。然後 bug 爆發,老闆就會質疑原本好好的程式怎麼會出包,最後開會被釘到牆上。很熟悉吧,也很無奈,難道這是程式員的宿命?
遇到这种情况,该怎么办?作者且说,
所以一般的流程是先讀程式碼,了解程式碼後,才去運用。其間也沒有其他工具輔助,頂多畫畫流程圖。然後又換人接手維護,再一次進行前述的苦情循環。現有的開發環境和工具對這情形是沒有多大幫助的。但是一直等我遇到了單元測試,這情形就改觀了許多。
但是,引入单元测试之后,就是:
當我拿到像一鍋粥的程式碼,我不會馬上跳進去和它們攪和一起。我會先快速掃一下,依據經驗找出程式壞味道(bad smell),針對這壞味道所在的目標程式,先進行單元測試撰寫。我會針對我對目標程式介面的認知,撰寫我自認為正確邏輯的測試。然後測試它,如果測試 pass ,就是我的認知符合我對目標程式的期待,並且把這樣的認知,透過單元測試寫了下來,確認了這樣的"規格";如果測試 fail ,就是我的認知和程式行為不一致,要不是程式有問題,就是我的認知是錯的。此時我才會跳進目標程式碼,作細節探究。假設是我的認知錯誤,則修正測試程式;反之,就是目標程式的問題,就小心的修改目標程式,直到測試 pass 。
這樣的過程,不僅只有探索,而且記錄了結果。這還可以作為日後的回歸測試的依據。
這樣程式設計的思維方式從原本的讀碼 -> 了解 -> 使用,轉變成讀碼 -> 使用 -> 了解,僅是後兩步驟順序互換,就會帶來非常不一樣的效果。寫程式時的思維,會由原本的先了解程式細節切入如何使用,轉變成觀察使用的程式介面,探索如何使用。這有一點退一步觀賞的意味,不管是正在欣賞的是藝術作品,還是密密麻麻的程式碼,道理是相似的。這樣就不容易當局者迷,墮入程式碼的五里霧中,分辨不清方向。它也促使你重新思考這樣的設計是否合理,繼而考慮是否要重新設計。
所以啊,还是虚心地扫下盲,来:
JUnit ——是一个开源的 Java 测试框架,主要用于编写白盒测试,回归测试。无论白盒测试还是回归测试,都是运行可重复的测试。所谓”回归测试“——就是,软件或环境的修复或更正后的“再测试”,自动测试工具对这类测试尤其有用;而所谓”单元测试“——就是,最小粒度的测试,以测试某个功能或代码块。一般由程序员来做,因为它需要知道内部程序设计和编码的细节。对于持续发展的产品,单元测试在后期的维护,回归有重要等方面有重要作用。
好吧,不多说废话,否则 94 《论”单元测试“的重要性》云云……本文提到的 jar 包,分别如下:
- 当前 Eclispe 4.4 已经集成的 JUnit 4,使用非常方便
- 不过,Mockito 就需要自己下载。官网:http://mockito.org 文档
新建 JUnit 测试
首先新建一个 测试类 Class,导入依赖包 import static org.junit.Assert.*;,也可以通过 IDE 界面来操作: File → New → JUnit → JUnit Test case。编写以下方法。
@Test public void testMultiply() { MyClass tester = new MyClass(); assertEquals( "10 x 5 must be 50 ", 50, tester.multiply( 10, 5 )); }
在测试类中,并不是每一个方法都是用于测试的,你必须使用“注解”来明确表明哪些是测试方法,例如 @Test 是必须的注解,表明这是一个测试用例。我们可以看到,在某些方法的前有 @Before、@Test、@Ignore等字样,这些就是 JUnit 的注解,以一个“@”作为开头。掌握这些标注的含义非常重要。
接着,你要测试哪个类,那么你首先就要创建一个该类的对象,如:
MyClass tester = new MyClass();
这里的 assertEquals 是 Assert 静态方法。Assert 包含了一组静态的测试方法,用于期望值和实际值比对是否正确,即测试失败,Assert 类就会抛出一个 AssertionFailedError 异常,JUnit 将这种错误归入 Failes 并加以记录,同时标志为未通过测试。如果该类方法中指定一个 String 类型的传参则该参数将被做为 AssertionFailedError 异常的标识信息,告诉测试人员改异常的详细信息。
然后运行这个测试用例,右键点击需要测试的类并且选择 Run → Run As → JUnit Test。如下图所示。
运行返回的结果如下,
进度条是红颜色表示发现错误,具体的测试结果在进度条上面有表示“共进行了4个测试,其中1个测试被忽略,一个测试失败”。
注解和断言的作用
下面补充一些 JUnit 的注解。
- @Test (expected = Exception.class) 表示预期会抛出Exception.class 的异常
- @Ignore 含义是“某些方法尚未完成,暂不参与此次测试”。这样的话测试结果就会提示你有几个测试被忽略,而不是失败。一旦你完成了相应函数,只需要把@Ignore注解删去,就可以进行正常的测试。
- @Test(timeout=100) 表示预期方法执行不会超过 100 毫秒,控制死循环
- @Before 表示该方法在每一个测试方法之前运行,可以使用该方法进行初始化之类的操作
- @After 表示该方法在每一个测试方法之后运行,可以使用该方法进行释放资源,回收内存之类的操
- @BeforeClass 表示该方法只执行一次,并且在所有方法之前执行。一般可以使用该方法进行数据库连接操作,注意该注解运用在静态方法。
- @AfterClass 表示该方法只执行一次,并且在所有方法之后执行。一般可以使用该方法进行数据库连接关闭操作,注意该注解运用在静态方法。
下面简单介绍一下上边用到的静态类 junit.framework.Assert。该类主要包含七个方法:
- assertEquals() 方法,用来查看对象中存的值是否是期待的值,与字符串比较中使用的 equals() 方法类似;
- assertFalse() 和 assertTrue() 方法,用来查看变量是是否为 false 或 true,如果 assertFalse() 查看的变量的值是 false 则测试成功,如果是 true 则失败,assertTrue() 与之相反。
- assertSame() 和 assertNotSame() 方法,用来比较两个对象的引用是否相等和不相等,类似于通过“==”和“!=”比较两个对象;
- assertNull() 和 assertNotNull() 方法,用来查看对象是否为空和不为空。
更多用法请参阅 JUnit 文档。
TestSuite
如果你须有多个测试单元,可以合并成一个测试套件进行测试,况且在一个项目中,只写一个测试类是不可能的,我们会写出很多很多个测试类。可是这些测试类必须一个一个的执行,也是比较麻烦的事情。鉴于此, JUnit 为我们提供了打包测试的功能,将所有需要运行的测试类集中起来,一次性的运行完毕,大大的方便了我们的测试工作。并且可以按照指定的顺序执行所有的测试类。下面的代码示例创建了一个测试套件来执行两个测试单元。如果你要添加其他的测试单元可以使用语句 @Suite.SuiteClasses 进行注解。
import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; @RunWith( Suite.class ) @SuiteClasses( { JUnit1Test.class, StringUtilTest.class } ) public class JSuit { }
TestSuite 测试包类——多个测试的组合 TestSuite 类负责组装多个 Test Cases。待测得类中可能包括了对被测类的多个测试,而 TestSuit 负责收集这些测试,使我们可以在一个测试中,完成全部的对被测类的多个测试。 TestSuite 类实现了 Test 接口,且可以包含其它的 TestSuites。它可以处理加入Test 时的所有抛出的异常。
TestResult 结果类集合了任意测试累加结果,通过 TestResult 实例传递个每个测试的 Run() 方法。TestResult 在执行 TestCase 是如果失败会异常抛出
TestListener 接口是个事件监听规约,可供 TestRunner 类使用。它通知 listener 的对象相关事件,方法包括测试开始 startTest(Test test),测试结束 endTest(Test test),错误,增加异常 addError(Test test, Throwable t) 和增加失败 addFailure(Test test, AssertionFailedError t) 。TestFailure 失败类是个“失败”状况的收集类,解释每次测试执行过程中出现的异常情况,其 toString() 方法返回“失败”状况的简要描述。
小结
再唠叨几句:
- JUnit 可以指定 Runner 运行器
- JUnit 可以参数化测试,详见这里
- 事实上在Junit 中使用try-catch 来捕获异常是没有必要的,Junit 会自动捕获异常。那些没有被捕获的异常就被当成错误处理
- 不要认为压力大,就不写测试代码。相反编写测试代码会使你的压力逐渐减轻,因为通过编写测试代码,你对类的行为有了确切的认识。你会更快地编写出有效率地工作代码。
参见资源: