JUnit 通過制作滿足預期需求的斷言來測試軟件代碼單元,但是這些斷言只限於基本操作。IBM 軟件工程師 Tony Morris 通過引入 JUnit 斷言擴展(Assertion Extensions for JUnit)填補了這個空白,JUnit 斷言擴展提供了一整套在 JUnit 框架中執行的復雜斷言。下面請隨作者的演示一道,了解如何使用這個來自 alphaWorks 的新包來提高 Java 軟件的可靠性和健壯性。
流行的 JUnit 自動單元測試框架提供了通過制作滿足預期需求的斷言來測試軟件代碼單元的途徑。然而,這些斷言僅局限於基本操作,例如“斷言兩個變量相等”和“斷言引用變量非 null”。基本的 JUnit 斷言是有用的,但是它們無法給出在現實的軟件單元測試場景中所需要的眾多復雜的斷言能力。
JUnit 斷言擴展(Assertion Extensions for JUnit,JUnitX)是 JUnit 框架的一個擴展包,可以從 alphaWorks(請參閱 參考資料) 下載得到。它提供了很多常見的復雜斷言的必要實現。不用為某個斷言編寫復雜的 JUnit 測試用例,可以調用一個 JUnitX 方法,制作來自同一上下文的斷言 —— 而且不需要額外的設置。JUnitX 還聲稱它的功能與文檔描述的一樣,並包括了自己的 JUnit 自測套件。這再次保證了 JUnitX 會根據 JUnitX 文檔來制作斷言;如果某個單元測試失敗,就可以知道是軟件代碼單元失敗了,而不是 JUnitX 的測試實現報告了一個假失敗。
JUnitX 會有用的一個典型場景包括 java.lang.Object 類的 equals(Object) 方法和 hashCode() 方法設置的合約。在開發的類中,通常要求遵守這些合約。用 JUnit 制作遵守合約的斷言,就要求開發復雜的單元測試用例,而這麼做很容易出錯。相比之下,用 JUnitX 進行斷言非常簡單,就像創建一個返回類的實例的工廠實現並從單元測試用例中調用 JUnitX 的 assertEqualsContract 和 assertHashCodeContract 方法一樣簡單。
JUnitX 起步
有效使用 JUnitX 只需要一個最短的學習曲線,如果知道如何直接使用 JUnit 自動測試框架,那麼將會發現使用 JUnitX 擴展包很容易。按照以下步驟即可起步:
如果還沒有設置好運行 JUnit 測試用例的環境,請先設置好。可以參閱在“Automating the build and test process”中詳細介紹的步驟(請參閱 參考資料)。
下載 JUnitX 包(請參閱 參考資料),把 JUnitX 文檔解壓到選定目錄。
讓執行 JUnit 測試的類裝入器能夠找到 lib/JUnitx.jar 文件。
現在可以調用 JUnitx.framework.Assert 類上的方法對功能進行斷言了,調用的方式與在典型 JUnit 測試環境使用 JUnit.framework.Assert 類的方式類似。JUnitX 的在線 API 文檔(請參閱 參考資料)提供了 JUnitx.framework.Assert 類上可以使用的方法調用的詳細描述。
用例場景
假設現在要求實現一個代表人的類。Person 類要求有三個屬性:title(稱呼)、first name(名字)和 surname(姓)。title 屬性是由可能值 MR、MS 和 MRS 組成的有限集合中的一個值,所以准備用 Typesafe Enumeration(類型安全的枚舉)設計模式來實現一個 Title 類。圖 1 中的 UML 圖顯示了這些需求目前的狀態。
圖 1.需求的 UML 圖表
清單 1 顯示了這些需求的源代碼:
清單 1. 需要的類的源代碼
public class Person {
private Title title;
private String firstName;
private String surName;
}
public class Title {
public static final Title MR = new Title();
public static final Title MS = new Title();
public static final Title MRS = new Title();
// private constructor to prevent outside instantiation
private Title() {
}
}
這個源代碼現在需要通過滿足一些更具體的需求來變得更強壯、功能更完整,例如以下一些典型需求:
Person 類根據 equals(Object) 和 hashCode() 方法的合約來重寫這兩個方法,以便該類可以有效地用在 Collection 類型中。
Person 類實現 java.io.Serializable 接口,進行無誤的序列化和反序列化。
Person 類沒有被聲明為 final,因此能夠派生子類。
Title 類被聲明為 final,因為它不公開任何構造函數(這意味著它不能派生子類)。把設計決策的采用寫下來是一個良好習慣,這樣就能在源代碼和生成的 API 文檔中看到 final 修飾符。
Title 類實現 java.io.Serializable 接口,無誤地序列化和反序列化。而且序列化到同一實例(符合 Typesafe Enumeration 設計模式的要求)。
Title 類有一個 private 的默認構造函數(沒有參數),防止來自外部類的實例化。
在 JUnit 測試環境中,可以容易地用 JUnitX 對所有這些需求進行斷言。清單 2 中的源代碼是一套 JUnit 測試用例,用 JUnitX 功能對所有需要滿足的需求進行斷言:
清單 2. 使用 JUnitX 斷言的 JUit 測試用例
import JUnit.framework.TestCase;
import JUnitx.framework.ObjectFactory;
import JUnitx.framework.Assert;
import java.io.Serializable;
import java.lang.reflect.Constructor;
public class TestRequirements extends TestCase {
public void testPersonEqualsAndHashCodeContract() {
// Different surnames should be unequal.
ObjectFactory factory = new ObjectFactory() {
public Object createInstanceX() {
return new Person(Title.MR, "Bob", "Brown");
}
public Object createInstanceY() {
return new Person(Title.MR, "Bob", "Smith");
}
}
// Make sure the object factory meets its contract for testing.
// This contract is specified in the API documentation.
Assert.assertObjectFactoryContract(factory);
// Assert equals(Object) contract.
Assert.assertEqualsContract(factory);
// Assert hashCode() contract.
Assert.assertHashCodeContract(factory);
}
public void testPersonSerialization() {
// Assert that the Person class directly implements Serializable.
Assert.assertDirectInterfaceOf(Person.class, Serializable.class);
// Assert that the Person instance can be serialized and deserialized without errors.
Assert.assertSerializes(new Person(Title.MR, "Joe", "Blog"));
}
public void testPersonNotFinal() {
// Assert that the Person class is not declared final.
Assert.assertNotFinal(Person.class);
}
public void testTitleFinal() {
// Assert that the Title class is declared final.
Assert.assertFinal(Title.class);
}
public void testTitleSerialization() {
// Assert that the Title class directly implements Serializable.
Assert.assertDirectInterfaceOf(Person.class, Serializable.class);
// Assert that the Title instances can be serialized and deserialized without errors.
Assert.assertSerializes(Title.MR);
Assert.assertSerializes(Title.MS);
Assert.assertSerializes(Title.MRS);
// Assert that serialization results in the same instance.
Assert.assertSerializesSame(Title.MR);
Assert.assertSerializesSame(Title.MS);
Assert.assertSerializesSame(Title.MRS);
}
public void testTitleConstructor() {
// Assert that the Title class has a default constructor.
Assert.assertClassHasConstructor(Title.class, null);
try {
// Get the default constructor.
Constructor con = Title.class.getDeclaredConstructor(null);
// Assert that the default constructor is declared private.
Assert.assertPrivate(con);
}
catch(NoSuchMethodException nsme) {
// Should never get here, even when test fails.
throw new IllegalStateException();
}
}
}
清單 1 中的 Person 類和 Title 類不能通過清單 2 中的測試用例,因為它們沒有滿足所有的附加要求。現在可以開發滿足新需求的類,好讓單元測試用例通過,通過意味著需求得到滿足。清單 3 演示了滿足指定需求的實現的一個示例:
清單 3. 修正好的滿足額外需求的類
import java.io.Serializable;
public class Person implements Serializable {
private Title title;
private String firstName;
private String surname;
public Person(Title title, String firstName, String surname) {
this.title = title;
this.firstName = firstName;
this.surname = surname;
}
public boolean equals(Object o) {
// Performance optimization only.
if(this == o) {
return true;
}
if(o == null) {
return false;
}
if(!(o instanceof Person)) {
return false;
}
Person p = (Person)o;
return title == p.title && firstName.equals(p.firstName) && surname.equals(p.surname);
}
public int hashCode() {
final int oddPrime = 461;
int result = 73;
result = result * oddPrime + title.hashCode();
result = result * oddPrime + firstName.hashCode();
result = result * oddPrime + surname.hashCode();
return result;
}
}
import java.io.Serializable;
public final class Title implements Serializable {
public static final Title MR = new Title();
public static final Title MS = new Title();
public static final Title MRS = new Title();
private static int nextIndex = 0;
private final int index = nextIndex++;
private static final Title[] VALUES = new Title[]{MR, MS, MRS};
private Title() {
}
// Ensure that the same instance is returned when deserialized.
Object readResolve() {
return VALUES[index];
}
}
如果運行清單 3 中的 JUnit 測試用例,測試會通過,這樣就可以得出結論:代碼滿足了指定需求。
結束語
如果想不用 JUnitX 在代碼的用例場景上進行斷言,就會有多得多的工作要做。一個測試用例失敗很可能表示一個構造不當的測試用例,而不是軟件代碼單元有誤,這可能要費更多時間來診斷。如果 JUnitX 測試用例失敗了,而軟件代碼單元中的錯誤又不是十分明顯,那麼可以閱讀自測試套件中包含的源代碼,以了解通過測試用例的代碼單元看起來是什麼樣的。