以下內容引自: http://blog.csdn.net/wanglha/article/details/42004695
TestNG深入理解
TestNG annotaion:
- @DataProvider
- @ExpectedExceptions
- @Factory
- @Test
- @Parameters
<suite name="ParametersTest"> <test name="Regression1"> <classes> <class name="com.example.ParameterSample" /> <class name="com.example.ParameterTest"> <mtehods> <include name="database.*" /> <exclude name="inProgress" /> </methods> </class> </classes> </test> <test name="Parameters"> <packages> <package name="test.parameters.Parameter*" /> </packages> </test> </suite>
一個suite(套件) 由一個或多個測試組成。
一個test(測試) 由一個或多個類組成
一個class(類) 由一個或多個方法組成。
@BeforeSuite/@AfterSuite 在某個測試套件開始之前/在某個套件所有測試方法執行之后
@BeforeTest/@AfterTest 在某個測試開始之前/在某個測試所有測試方法執行之后
@BeforeClass/@AfterClass 在某個測試類開始之前/在某個類的所有測試方法執行之后
@BeforeMethod/@AfterMethod 在某個測試方法之前/在某個測試方法執行之后
@BeforeGroup/@AfterGroup 在某個組的所有測試方法之前/在某個組的所有測試方法執行之后
1、分組
@Test(groups = {"fast", "unit", "database"}) public void rowShouldBeInserted() {} java org.testng.TestNG -groups fast com.example.MyTest
測試的一個目標就是確保代碼按照預期的方式工作。這種測試要么在用戶層面上進行,要么在編程層面上進行。這兩種類型的測試分別是通過功能測試和單元測試來實現的。
針對失敗而測試
Java提供了兩種不同類型的異常:從java.lang.RuntimeException派生的運行時刻異常和從java.lang.Exception派生的被檢查的異常。
拋出被檢查的異常的經驗法則:調用者可以為這個異常做什么嗎?如果答案是肯定的,那么可能應該是用被檢查的異常,否則,最好是選擇運行時刻異常。
@Test(expectedExceptions = {ReservationException.class, FlightCanceledException.class}) public void shouldThrowIfPlaneIsFull() { Plane plane = createPlane(); plane.bookAllSeats(); plane.bookPlane(createValidItinerary(), null); }
屬性expectedExceptions是一組類,包含了這個測試方法中預期會拋出的異常列表。如果沒有拋出異常,或拋出的異常不再該屬性的類表中,那么TestNG就會認為這個測試方法失敗了。
單一職責:
public class BookingTest { private Plane plane; @BeforeMethod public void init() { plane = createPlane(); } @Test(expectedException = PlaneFullException.class) public void shouldThrowIfPlaneIsFull() { plane.bookAllseats(); plane.bookPlane(createValidItinerary(), null); } @Test(expectedException = FlightCanceledException.class) public void shouldThrowIfFlightIsCanceled() { cancelFlight(/* ... */); plane.bookPlane(createValidItinerary(), null); } }
testng-failed.xml
當您執行包涵失敗的測試套件時,TestNG會在輸出目錄(默認是test-output/)下自動生成一個名為testng-failded.xml的問他件。這個XML文件包含了原來的testng.xml中失敗的方法所構成的子集。
java org.testng.TestNG test.xml java org.testng.TestNG test-output/testng-failed.xml
2、工廠
TestNG讓您可以選擇自己將測試類實例化。這是通過@Factory annotation來實現的,他必須放在返回一個對象數組方法的頂部。所有這些對象都必須是包含TestNG annotation的類的實例。如果有@Factory annotation,那么這個循環會繼續下去,知道TestNG拿到的都是沒有@Factory annotation實例,或者@Factory方法都已被調用過的實例。
public class ExpectedAttributes { private Image image; private int width; private height; private String path; @Test public void testWidth() {} @Test public void testHeight() {} public PictureTest(String path, int width, int height, int depth) throws IOException { File f = new File(path); this.path = path; this.image = ImageIO.read(f); this.width = width; this.height = height; } private static String[] findImageFileNames() {} @Factory public static Object[] create() throws IOException { List result = new ArrayList(); String[] paths = findImageFileNames(); for (String path : paths) { ExpectedAttributes ea = findAttributes(path); result.add(new PictureTest(path, ea.width, ea.height, ea.depth)); } return result.toArray(); } public class ExpectedAttributes { public int width; public int height; public int depth; } private static ExpectedAttributes findExpectedAttributes(String path) { // ...... } }
可以安全的在同一個類包含@Factory和@Test annotation,因為TestNG確保@Factory方法只被調用一次。
org.testng.ITest接口
public interface ITest { public String getTestName(); }
當TestNG遇到實現了這個接口的測試類時,他會在生成各種報告中包含getTestName()方法返回的信息。
public class PictureTest implements ITest { public String getTestName() { return "[Picture: " + name + "]"; } }
數據驅動測試
測試需要針對許多具有類似結構的數據來執行。
實際的測試邏輯是一樣的,僅僅發生改變的是數據。
數據可以被一組不同的人修改。
參數測試方法
測試邏輯可以非常簡單或不易改變,而提供給他的數據肯定會隨着時間增長。
TestNG可以通過兩種方式向測試方法傳遞參數:
- 利用testng.xml
- 利用DataProviders
1、利用testng.xml傳遞參數
<suite name="Parameters"> <parameter name="xml-file" value="accounts.xml" /> <parameter name="hostname" value="arkonis.example.com" /> <test name="ParameterTest"> <parameter name="hostname" value="terra.example.com" /> ... </test> ... </suite>
在測試方法中指定參數
@Test(parameters = {"xml-file"}) public void validateFile(String xmlFile) { // xmlFile has the value "accounts.xml" }
如果犯下以下錯誤之一,TestNG將拋出一個異常:
- 在testng.xml中指定了一個參數,但不能轉換為對應方法參數的類型。
- 聲明了一個@Parameters annotation,但引用的參數名稱在testng.xml中沒有聲明。
2.利用@DataProvider傳遞參數
如果需要向測試方法傳遞的參數不是基本的Java類型,或者如果需要的值智能在運行時刻創建,那么我們應該考慮使用@DataProvider annotation。
數據提供者是用@Dataprovider標注的方法。這個annotation只有一個字符串屬性:他的名稱,如果沒有提供名稱,數據提供者的名稱就默認采用方法的名稱。
數據提供者同時實現兩個目的:
向測試方法傳遞任意數目的參數
根據需要,允許利用不同的參數集合對他的測試方法進行多次調用。
@Test(dataProvider = "range-provider") public void testIsBetWeen(int n, int lower, int upper, boolean expected) { println("Received " + n + " " + lower + "-" + upper + " expected: " + expected); assert.assertEquals(expected, isBetween(n, lower, upper)); } @DataProvider(name = "range-provider") public Object[][] rangeData() { int lower = 5; int upper = 10; return new Object[][] { { lower-1, lower, upper, false}, { lower, lower, upper, true}, { lower+1, lower, upper, true}, { upper, lower, upper, true}, { upper+1, lower, upper, false}, }; }
由於數據提供者是測試類中的一個方法,他可以屬於一個超類,然后被一些測試方法復用。我們也可以有幾個數據提供者,只要他們定義在測試類或者他的一個子類上。當我們像在合適的地方記錄數據源,並在幾個測試方法中復用他時,這種方法是很方邊的。
針對數據提供者的參數
數據提供者本身可以接受兩個類型的參數:Method和ITestContext
@DataProvider public void craete() { ... } @DataProvider public void create(Method method) { ... } @DataProvider public void create(ITestContext context) { ... } @DataProvider public void create(Method method, ITestContext context) { ... }
Method參數
如果數據提供者的第一個參數是java.lang.reflect.Method,TestNG傳遞這個將調用的測試方法。如果您希望數據提供者根據不同的測試方法返回不同的數據,那么這種做法就非常有用。
@DataProvider public Object[][] provideNumbers(Method method) { String methodName = method.getName(); if (methodName.equals("tow")) { return new Object[][] { new Object[] {2} }; } if (methodName.equals("three")) { return new Object[][] { new Object[] {3} }; } } @Test(dataProvider = "provideNumbers") public void two(int param) { System.out.println("Two received: " + param); } @Test(dataProvider = "provideNumbers") public void three(int param) { System.out.println("Three received: " + param); }
使用同一個數據提供者的地方:
數據提供者代碼相當復雜,應該保存在一個地方,這樣維護起來更方便。
我們要傳入數據的那些測試方法具有許多參數,其中只有少數參數是不一樣的。
我們引入了某個方法的特殊情況。
ITestContext參數
如果一個數據提供者在方法簽名中聲名了一個ITestContext類型的參數,TestNG就會將當前的測試上下文設置給它,這使得數據提供者能夠知道當前測試執行的運行時刻參數。
@DataProvider public Object[][] randomIntegers(ITestContext context) { String[] groups = context.getIncludeGroups(); int size = 2; for (String group : groups) { if (group.equals("functional-test")) { size = 10; break; } } Object[][] result = new Object[size][]; Random r = new Random(); for (int i = 0; i < size; i++) { result[i] = new Object[] { new Integer(r.nextInt()) }; } return result; } @Test(dataProvider = "randomIntegers", groups = {"unit-test", "functional-test"}) public void random(Integer n) { // ...... }
ITestContext對象中的數據是運行時刻的信息,不是靜態的信息:這個測試方法即屬於unit-test組,也屬於functional-test組,但在運行時刻,我們決定只執行functional-test組,這個值由ITestContext#getIncludeGroups方法返回。
延遲數據提供者
為了實現這種方法,TestNG允許我們從數據提供者返回一個Iterator,而不是一個二維對象數組。
這種方法與數組不同之處在於,當TestNG需要從數據提供者取得下一組參數時,他會調用Iterator的next方法,這樣就有機會在最后一刻實例化相應的對象,即剛好在需要這些參數的測試方法被調用之前。
@DataProvider(name = "generate-accounts-lazy") public Iterator generateAccountsLazy { return new AccountIterator(); } @Test(dataProvider = "generate-accounts-lazy") public void testAccount(Account a) { System.out.println("Testing account " + a); } class AccountIterator implements Iterator { private static final int MAX = 4; private int index = 0; public boolean hasNext() { return index < MAX; } public Object next() { return new Object[] { new Account(index++); } } public void remove() { throw new UnsupportedOperationException(); } }
如果傳遞的參數是簡單類型的常數,利用testng.xml的方法是很好的。檔我們需要更多靈活性,並知道參數的數目和值將隨時間增加時,我們可能應該選擇@DataProvider。
提供數據
數據的位置可能是:硬編碼在Java源碼中、再文本文件中、在屬性文件中、在Excel表格中、在數據庫中、在網絡中…。
數據提供者還是工廠
數據提供者向測試方法傳遞參數,而工廠像構造方法傳遞參數。
不如不能確定使用哪種方法,那么就看看測試方法所使用的參數。是否有幾個測試方法需要接收同樣的參數?如果是這樣,您可能最好是將這些參數保存在一個字段中,然后在幾個方法中復用這個字段,這就意味着最好是選擇工廠。反之,如果所有的測試方法都需要傳入不同的參數,那么數據提供者可能是最好的選擇。
異步測試
異步代碼通常出現在下列領域:
- 基於消息的框架,其中發送者和接收者是解耦合的。(JMS)
- 由java.util.concurrent提供的異步機制(FutureTask)
- 由SWT或Swing這樣的工具集開發的圖形用戶界面,其中代碼與主要的圖形部分運行在不同的線程中。
測試異步代碼比測試同步代碼的問題更多:
- 無法確定異步調用何時質性。
- 無法確定異步調用是否會完成。
當調用異步時有三種可能的結果:
- 調用完成並成功。
- 調用完成並失敗。
- 調用沒有完成。基本上,異步編程遵循着一種非常簡單的模式:在發出一個請求時指定一個對象或一個函數,當收到響應時系統會調用回調。
測試異步代碼也同樣遵循下面的模式:
發出異步調用,他會立即返回。如果可能,制定一個回調對象。
如果有回調方法:
等待結果,在接到結果是設置布爾變量,反應結果是否是您的預期。
在測試方法中,監視那個布爾變量,等到他被設置或過了一段時間。
如果沒有回調方法:
在測試方法中,定期檢查預期的值。如果過了一段時間還沒有檢查到預期值,就失敗並退出。
不指定回調方法
private volatile boolean success = false; @BeforeClass public void sendMessage() { // send the message; // Successful completion should eventually set success to true; } @Test(timeOut = 10000) public void waitForAnswer() { while (!success) { Thead.sleep(1000); } }
在這個測試中,消息是作為測試初始化的一部分,利用@BeforeClass發出的,這保證了這段代碼在測試方法調用之前執行並且只執行一次。在初始化后TestNG將調用waitForAswer測試方法,他將進行不完全忙等。
有回調方法:
@Test(groups = “send”) public void sendMessage() { // send the message } @Test(timeOut = 10000, dependsOnGroups = {“send”}) public void waitForAnswer() { while (!success) { Thread.sleep(1000); } }
現在sendMessage()是一個@Test方法,他將包含在最終的報告中,如果發送消息失敗,TestNG將跳過waitForAnswer測試方法,並把他表示為SKIP。
@Test(timeOut = 10000, invocationCount=100, successPercentage = 98) public void waitForAnswer ……
TestNG調用該方法100次,如果98%的調用成功,就認為總體測試通過。
測試多線程代碼
並發測試
private Singleton singleton; @Test(invocationCount = 100, threadPoolSize = 10) public void testSingleton() { Thread.yield(); Singleton p = Singleton.getInstance(); } public static Singleton getInstance() { if (instance == null) { Thread.yield(); Assert.assertNull(instance); instance = new Singleton(); } return instance; }
@invocationCount相當簡單,在不考慮並發時也可以使用:他決定了TestNG調用一個測試方法的次數。
@threadPoolSize要求TestNG分配一定數量的線程,並使用這些線程來調用這個測試方法,當一個測試完成之后,執行他的線程將歸還給線程池,然后可以用於下一次調用。
並發執行
<suite name=”TestNG JDK 1.5” verbose=“1” parallel=“methods” thread-count = “2”>......</suite>
thread-count屬性指定了線程數目,TestNG將使用這些線程來執行這個測試套件中的所有測試方法,parallel屬性告訴TestNG您在執行這些測試時希望采用的並行模式。
parallel=”methods” 在這種模式下,每個測試方法將在他自己的一個線程中執行。
parallel=”test” 在這種模式下,在某個標簽內的所有測試方法將在他們自己的一個線程中執行。
在tests模式中,TestNG保證每個將在他自己的線程中執行。如果希望測試不安全的代碼,這一點是非常重要的。在method模式中,所有限制都被取消,無法預測哪些方法將在同一個線程中執行,哪些方法將在不同的測試中執行。
為可模擬性設計
為了能夠成功地使用模擬模擬對象或樁,重要得失要確保代碼的設計能讓使用模擬對象或樁變得簡單而直接。
這種設計最重要的方面就是正確的確定組件之間的交互,從而確定組件的交互接口。
如果我們有2個組件A和B,A需要用到B,那么應該通過B的接口來完成,而不是通過B的具體實現。
Singleton查找
public void doWork1() { C c = C.getInstance(); c.doSomething(); }
對於某個對象智能由一個實例,這在項目生命周期的后期產生阻礙效果。
JNDI定位服務
public void doWork2() { C c = (C) new InitialContext().lockup("C"); c.Something(); }
不能夠向A提供一個受控制的B的實例。只有一個全局實例,A只能取得這個實例。
依賴注入
private C c; public void setC(C c) { this.c = c; }
從外部通知A應該使用哪個B的實例。這讓我們能夠根據實際情況靈活地決定向A提供B的哪個實例。
EasyMock
import static org.easymock.EasyMock.*; public class EasyMockUserManagerTest { @Test public void createUser() { UserManager manager = new UserManagerImpl(); UserDao dao = createMock(UserDao.class); Mailer mailer = createMock(Mailer.class); manager.setDao(dao); manager.setMailer(mailer); expect(dao.saveUser("tester")).andReturn(true); expect(mailer.sendMail(eq("tester"), (String) notNull(), (String) notNull())).addReturn(true); replay(dao, mailer); manager.createUser("tester"); verify(mailer, dao); } }
1創建模擬對象
這是通過createMock方法完成的,傳入希望模擬的類作為參數。
2紀錄預期行為
只要在模擬對象上調用我們預期會被調用的方法,就能紀錄預期的行為。當用到某些具體的參數時,只要將這些參數傳入就可以了。
3調用主要被測對象
在主要的被測對象上調用一個方法或一組方法,預期這次調用將倒置被測對象調用模擬對象的那些預期的方法。
4驗證預期行為
最后調用verify,檢查所有的模擬對象。
JMock
jMock是一個模擬庫,她讓我們通過編成的方式來之行約束條件。
選擇正確的策略
缺少接口
有時候,我們面對的是龐大臃腫的遺留系統,沒有向期望的那樣有很好的設計。
大多數模擬庫現在都允許替換類,而不僅是接口。這些庫會在運行時刻生成一個新類,通過字節碼操作來實現指定的契約。
復雜的類
如果我們得到了一些類,他們擁有20多個方法,與許多其他組件交互,而且隨着是間的推移變得越來越復雜。
這種情況下,使用動態的模擬對象庫效果會比較好,因為他們能夠定義單個方法的行為,而不是考慮所有的方法。
契約紀錄
使用模擬對象讓我們記錄更多的契約信息,而不止是方法簽名。我們可以隨時驗證器樂的。
測試目標
根據經驗法則,如果希望測試組件之間交互,模擬對象可能優於樁對象。模擬庫能夠以一種准確的方式來指定交互。而樁該作為被測試組件使用的那些次要的組件。在這種情況下,測試的目標是測試被測試組件本身,而不是他與其他組件之間的交互。
模擬易犯的錯誤
依賴模擬對象會導至許多問題,所以重要的是要知道使用模擬對象不利的一面:
- 模擬外部API行
- 虛假的安全感
- 維護開銷
- 繼承與復雜性
依賴的測試
層疊失敗:一個測試的失敗導致一組測試的失敗。
依賴的代碼只要測時方法依賴於其他測試方法,就很難以隔離的方式執行這些測試方法。
彼此依賴的測試方法通常會出現這樣的情況,因為他們共享了一些狀態,而在測試之間共享狀態是不好的。
利用TestNG進行依賴的測試
TestNG通過@Test annotation的兩個屬性(dependsOnGroups和dependsOnMethods)賴支持依賴的測試。
@Test public void launchServer() {} @Test(dependsOnMethods = "launchServer") public void deploy() {} @Test(dependsOnMethods = "deploy") public void test1() {} @Test(dependsOnMethods = "deploy") public void test2() {}
dependsOnMethods的問題:
通過字符串來執行方法名稱,如果將來對他進行重構,代碼就有可能失效。方法名稱違反了”不要重復自己”的原則,方法名稱即在Java方法中用到,也在字符串中使用,另外,等我們不斷添加新的測試方法時,這個測試用例伸縮性也不好。
@Test(groups = "init") public void launchServer() {} @Test(dependsOnGroups = "init", groups = "deploy-apps") public void deploy() {} @Test(dependsOnGroups = "init", groups = "deploy-apps") public void deployAuthenticationServer() {} @Test(dependsOnGroups = "deploy-apps") public void test1() {} @Test(dependsOnGroups = "deploy-apps") public void test2() {}
利用組來指定依賴關系可以解決我們遇到的所有問題:
- 不在遇到重構問題,可以任意秀該方法的名稱。
- 不再違反DRY原則。
- 當新方法須要加入到依賴關系中時,只要將他放到適當的組中,並確保他依賴於正確的組。
依賴的測試和線程
當打算並行執行測試時,要記住,線程池中的一個或多個線程將用於依次執行每個方法。所以,如果打算在不同的線程中執行一些測試,過渡的使用依賴的測試將影像執行性能。
配置方法的失敗
依賴測試方法和配置方法之間唯一的不同就是,測試方法隱式的依賴於配置方法。
雖然dependsOnMethods可以處理簡單的測試或之由一個測試方法依賴於另一個測試方法的情況,但是在大多數情況下,您都應該使用dependsOnGroups,這種方式的伸縮性好,面對將來的重構也更為健壯。
既然我們提供了准去的依賴信息,那么TestNG就能夠按照於騎的順序來執行測試。
測試隔離並沒有因此而受到影響。
如果出現層疊式的錯誤,依賴測試可以加快測試執行速讀。
繼承和annotation范圍
public class CreditCardTest { @Test(groups = "web.credit-card") public void test1() {} @Test(groups = "web.credit-card") public void test2() {} }
他違反了”不要重復自己”的原則
他為將來添加測試方法的開發者帶來了負擔。
@Target({METHOD, TYPE, CONSTRUCTOR}) public @interface Test{} @Test(groups = "web.credit-card") public class CreditCardTest { public void test1() {} public void test2() {} }
annotation繼承
@Test(groups = "web.credit-card") class BaseWebTest {} public class WebTest extends BaseWebTest { public test1() {} public test2() {} }
所有擴展自BaseWebTest的類都會看到,他們所有的工有方法都自動成為web.credit-card組的成員。
WebTest變成了一個普通的傳統Java對象(POJO),不帶任何annotation。
集成易犯的錯誤
由於TestNG的測試方法必須是公有的,在基類中聲明的方法會自動在子類中可見,所以他們永遠也不需要作為測試類顯式的列出(不要將測試基類列在testng.xml文件中)
測試分組分組解決了上面提到的局限性,實際上,他們進一步提升了TesgNG的一個設計目標:在靜態模型(測試代碼)和動態模型(執行哪些測試)之間實現清晰的分離。
語法@Test annotation和配置annotation(@BeforeClass, @AfterClass, @BeforeMethod…)都可以屬於分組
@Test(groups = {"group1"}) @Test(groups = {"group1", "group2"}) @Test(groups = "group1") @Test(groups = "group2") public class B { @Test public test1() {} @Test(groups = "group3") public test2() {} }
test1屬於group2組,test2同時屬於group2組和group3組
分組與運行時刻
<suite name="Simple suite"> <test name="GroupTest"> <groups> <run> <include name="group1" /> </run> </groups> <classes> <class name="com.example.A" /> </classes> </test> </suite>
這個testng.xml告訴TestNG執行com.example.A類中所有屬於group1組的測試方法。
<include name="database" /> <exclude name="gui" />
如果某個方法即屬於包含的組,又屬於排除的組,那么排除的組優先。
如果既沒有include,也沒有exclude,那么TestNG將忽略組,執行所有的測試方法。
另一個功能就是可以在testng.xml中利用正則表達式來指定組。
<groups> <define name="all-web"> <include name="jsp" /> <include name="servlet" /> </define> <run> <include name="all-web"> </run> </groups>
在設計組的層次關系時,能夠在testng.xml中定義新組帶來靈活性:
可以在代碼中使用粒度非常小的分組,然后在運行時刻將這些小分組合並成大分組。
執行分組
利用命令行執行
java org.testng.TestNG -groups jsp -groups servlet -excludegroups broken com.example.MytestClass
利用ant
<testng groups="jsp, servlet" excludegroups="broken"> <classfileset> <include name="com/example/MyTestClass.class" /> </classfileset> </testng>
利用Maven
<dependencies> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>5.10</version> <classifier>jdk15</classifier> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.5</version> <configuration> <suiteXmlFiles> <suiteXmlFile>testng.xml</suiteXmlFile> <suiteXmlFiles> </configuration> </plugin> </plugins> </build>
利用Java API
TestNG tng = new TestNG(); tng.setGroups("jsp, servlet"); tng.setExcludeGroups("broken")
排除失敗的測試
創建一個特書的組如broken
@Test(groups = { "web", "broken"})
然后在運行時刻排除這個組。
<exclude name="broken" />
組分類
測試類型:單元測試、繼承測試
測試規模:小規模、大規模
功能描述:數據庫、界面
測試速度:慢測試、快測試
過程描述:冒煙測試、發布測試
讓開發者能夠指定方法的分組,主要的好處在於開發者因此能夠很容易找出他們需要執行哪些測試。(如剛剛修改了數據庫代碼,可能只需要執行fast和database組測試)
組命名
@Test(groups = {"os.linux.debian"}) @Test(groups = {"database.table.ACCOUNTS"}) @Test(groups = {"database.ejb3.connection"})
TestNG能夠利用正則表達式來之定要執行的組,如果與這項功能配合使用,這種命名方式就很有用了。
<groups> <run> <include name="database.*" /> </run> </groups>
代碼覆蓋率
類的覆蓋率:類覆蓋描熟了項目中多少類已被測試套件訪問。
方法覆蓋率:方法覆蓋率是被訪問的方法的百分比。
語句覆蓋率:語句覆蓋率追蹤單條源代碼語句的調用。
語句塊覆蓋率:語句快覆蓋率將語句塊作為基本的覆蓋律單元。
分支覆蓋率:分支覆蓋率也被稱為判斷覆蓋率。指標計算哪些代碼分支被執行。
覆蓋律工具
Clover、EMMA和Cobertura
成功使用覆蓋率的建議
覆蓋率報告的信息何音的解讀不同
覆蓋率很難
百分比沒有意義
為覆蓋率而設計是錯誤得
有一點好過沒有
覆蓋律工具不會測試不存在的代碼
覆蓋率的歷史講述了自己的故事
企業級測試
單元測試:單元測試對系統中的一個單元進行獨立的測試。
功能測試:功能測試關注一項功能。通常涉及不同組件之間的交互。
繼承測試:繼承測試是一種端到端的測試,他會執行整個應用棧,包括所有的外部依賴關系或系統。
一個具體的例子
系統中有一個相當典型的組件,他接收一條JMS消息,其中包含一段有效的XML文本。這段XML文本相當長,描述了一筆財務交易。這個組件的工作是讀出這條消息,解析XML,根據消息的內容條填充一些數據庫的表,然后調用一個存儲過程來處理這些表。
測試內容
我們將創建一個成功測試。希望確保,如果收到一條有效的XML消息,我們會正確地處理他,並更新正確的數據表,然后存儲過程的調用也成功。
我們將摹擬不同的場景。希望能夠為測試替工不同的XML文本,這樣就能夠很容易地不斷添加測試粒子數據。
我們提供明確的失敗測試。失敗的行為將被記錄和測試,這樣當組件內部出現失敗時,他的狀態就可以與測,並且很容易記錄下來。
非測試內容
我們不測試JMS provider的功能。假定他是一個完全兼容的實現,已經正確的進行了配置,將成功地提交我們期望的消息。我們不執行捕捉所有錯誤得測試。失敗測試應該針對明確的、可重現的失敗場景。我們不測試API。例如,JDBC去冬的行為不是測試的主題。確保所有的測試都貫注業務功能,要避免對Java語言的語義進行測試。
測試成功場景
對於JMS API的接口
利用模擬對象(或樁)對象,創建TextMessage實現,用一個簡單的POJO來表現,帶有消息內容和其他屬性的設置方法。重構該組件,解除業務功能與JMS API的耦合。
public void onMessage(Message message) { TextMEssage tm = (TextMessage) message; processDocument(tm.getText()); } public void processDocument(String xml) { // code previously in onMessage that updates DB } @Test public void componentUpdateDatabase() throws Exception {}
構件測試數據
@Test(dataProvider = "") public void componentUpdateDatabase() throws Exception {} @DataProvider(name = "component-data-files") public Iterator<Object[]> loadXML() throws Exception {}
我們的測試現在接受一個參數,不再需要自己考慮測試數據的來源,也不需要考慮如何加載測試數據。他要做的只是指定數據提供者。加載XML的實際工作現在代理給了一段獨立的加載程序。
@DataProvider(name = "component-data-files") public Iterator<Object[]> loadXML() throws Exception { File[] f = new File("filepath").listFiles(); final Iterator<File> files = Arrays.asList(f).iterator(); return new Iterator<Object[]>() { public boolean hasNext() { return files.hasNext(); } public Object[] next() { return new Object[] { IOUtils.readFile(files.next()) }; } public void remove() { throw new UnsupportedOperationException(); } }; }
當然,可以從數據提供者返回一個Object[]的數組,但是,這種方法意味着我們必需將所有的文件的數據都一次性加載到內存中,因為數組必須事先填充。
測試准備問題
冪等的測試是指,這個測試執行一次和執行多次的結果是一樣的。如果某個東西是冪等的,那么說明他在多次調用時狀態不會改變。
不僅需要測試是冪等的,而且測試的次序應該無關緊要。所以除了需要是是冪等的之外,測試不應該在狀態或數據方面影像其他測試。
對於一些寫操作,成功執行之后很容易會對再次執行產生影響,下面方法有助於我們對付這個問題:
嵌入式數據
有一些基於Java的數據庫引擎,在設計時專門考慮了嵌入式支持。這些數據庫可以在測試過程中臨時創建並進行初始化。他們開銷很小,通常性能不錯。 不足之處在於,它們與應用程序實際執行的環境差別非常大。通常在數據庫特征上存在巨大的詫異。
在測試准備時初始化數據
測試數據庫總加載一定數量的已知測試數據。其中包含希望操作的所有數據,以及組件所依賴的所有外部引用。 不足之處在於,很難維護一個健壯的數據集,使他對測試有足夠的意義。
事務回滾
另一種方法就是利用Java API來防止數據寫入到持久數據存儲中。總的方法是開是一個事務,執行所有的寫操作,驗證一切正常,然后讓事務回滾。 不足之處在於,如果沒有復雜的嵌套事務准備,就不能測試參與事務的代碼或開始自己的事務的代碼。
選擇正確的策略
private WrappedConnection wrappedConnection; @BeforeMethod public void connection() throws SQLException { connection = DatabaseHelper.getConnection(); connection.setAutoCommit(false); wrappedConnection = new WrappedConnection(connection); wrappedConnection.setSuppressCommit(true); } @AfterMethod public void rollback() throws SQLException { wrappedConnection.rollback(); } public class WrappedConnection implements Connection { private Connection connection; private boolean suppressClose; private boolean suppressCommit; public WrappedConnection(Connection connection) { this.connection = connection; } public void commit() throws SQLException { if (!suppressCommit) connection.commit(); } // ...... }
錯誤處理
@Test(dataProvider = "component-data-files") public void componentupdateDatabase(String xml) throws Exception { Component component = new Component(); try { component.processDocument(xml); } catch (InvalidTradeException e) { return; } // rest of test code }
這種方法在於,他們沒能讓我們區分失敗是否是預期的。相反,我們應該能夠區分預期的成功和預期的失敗。這個測時目前在兩種情況下會通過:要么遇到好的數據時會通過,要么遇到壞數據時會通過。在每種情況下,我們都不能確定會發生些什么。
一個測試不應該在兩種或兩種以上的情況下都通過。如果測試驗證了不同的失敗情況,這沒問題,但如果測試在好數據和壞數據的情況下都通過,那就會導致一些微妙的錯誤,這類錯誤難以被發現。(因此我們定義了另一個目錄和數據提供者來處理失敗的情況)
@Test(dataProvider = "component-invalid-data-files", expectedException = InvalidTradeException.class) public void componentInvalidInput(String xml) throws Exception { Component component = new Component(); component.processDocument(xml); // rest of test code }
逐漸出現的單元測試
單元測試不一定是在其它測試之前編寫的,他們可以是功能測試驅動的。特別是對於大型項目或原有的代碼來說,一開始就編寫有用的單元測試可能很困難,因為在不了解全局的情況下,單元測試可能太瑣碎或不太重要。相反,單元測試可以從有意義的繼承測試中推導出來,因為調試開發功能測試和集成測試的過程揭示他們所需的單元測試。
對於例子來說我們需要將XML驗證與數據庫處理放到各自獨立的方法中。這樣就能對他們進行測試
public void processDocument(String xml) throws InvalidDocumentException { Document doc = XMLHelper.parseDocument(xml); validateDocument(doc); // ...... } public void validateDocument(Document doc) throws InvalidDocumentException { // perform constraint checks that can't be captured by XML }
這測重構的結果是我們得到了一個簡單的單元測試。
不論測試編寫的次序如何,功能測試和單元測試都是互不的。功能測試是更為水平化的測試,涉及許多不同的組件,執行代碼的很多部分。相反,單元測試是更為垂直化的測試,他關注范圍狹窄的主題,比功能測試要徹底得多。
競爭消費者模式
消費者的執行是並發的,所以我們必須在測試中進行某種程度的模擬,生產環境中的真實情況。在我們這樣作了之后,也希望驗證結果。不論哪個消費者先開始,也不論哪個消費者先結束,都沒有關系。我們希望確定對於給定數量的消費者,我們將得道一組已知的結果,可以進行驗證。
private final List<Object[]> data = Collections.synchronizedList(new ArrayList<Object[]>()); @BeforeClass public void populateData() { data.add(new Object[] {"value1", "prop1"}); data.add(new Object[] {"value2", "prop2"}); data.add(new Object[] {"value3", "prop3"}); } @Test(threadPoolSize = 3, invocationCount = 3, dataProvider = "concurrent-processor") public void runConcurrentProcessors(String value, String someProp) { MessageProcessor processor = new MessageProcessor(); processor.process(value, someProp); } @Test(dependsOnMethods = "runConcurrentProcessors") public void verifyConcurrentProcessors() { // load data from db // verify that we have 3 results // verify that each of the 3 result matches our 3 input } @DataProvider(name = "concurrent-processor") public Object[][] getProcessorData() { return new Object[][] {data.remove(data.size() - 1)}; }
我們的測試被分成兩個,一個負責執行消費者,另一個負責驗證結果。原因是runConcurrentProcessors會被調用多次,而我們只需要在所有方法調用完成之后,對結果驗證一次。為了表示這種次序,我們利用了dependsOnMethods這個annotation屬性。
當TestNG發現一個數據提供者時,他將針對數據提供者返回的每一條數據調用一次測試。類似的,當我們指定調用次數時,TestNG會按照指定的次數調用測試。因此,如果我們返回數據提供者中准備好的3條數據,那么每個線程都會執行3次測試。
因此解決方案是使用一個棧結構,每次調用數據提供者時,返回一條數據,並將這條數據從列表中清除。數據提供者將被調用3次,每次都將為數據返回不一樣的數據。
原則:將數據的考慮和功能的考慮分開來是很關鍵的。
在這個例子中,消費者需要的數據應該和實際的測試沒有依賴關系。這種方法意味着,隨着我們對數據的需求不斷變化,變得更為復雜,測試本身卻不需要被修改。
一個具體的例子
我們希望測試一個登錄servlet。這個servlet接受一個請求,檢查用戶名和口令,如果他們有效,就在會話中加入一個標記。表明用戶已登錄。
這個例子展示了重構在測試方面起到的重要輔助作用,說明了即使對於看上去很麻煩、需要一很復雜地方是進行交互的API,頁可以通過抽象去掉依賴關系。這種抽象意味着 在測試過程中,我們可以利用更簡單的對象,這些對象更容易構造,因此也更容易測試。
增加可測試性的一個副作用就是改進了設計。為提高可測試性而進行重構,可以幫助我們以一種實際的、代碼中的方式來確定職責和考慮,而這種效果通過畫設計圖是很難達到的。
Java EE測試
容器內測試與容器外測試的對比
容器內測試
優點:
完全符合運行時環境
缺點:
啟動消耗大 難以部署新的測試 難以自動化 誇平台測試的復雜性增加
容器外測試
優點:
提供了相對較快的啟動 可以完全控制環境 可以自動化 容易測試
缺點:
沒有符合運行時環境 測試所用的實現可能與運行時的實現來自不同的提供商。
容器內測試
測試步驟:
創建一個測試環境的實例。
確定測試。
在測試框架中注冊測試。
注冊一個監聽者來接收搜測試結果。
創建測試環境
TestNG tester = new TestNG(); tester.setDeafultSuiteName("container-tests");
確定測試
假定所有的測試類都在WEB-INF/classes目錄下,我們可以遞歸地讀入這個目錄,找到其中所有的類文件。
public static Class[] scan(ServletContext context) { String root = context.getReadPath(); ClassScanner scanner = new ClassScanner(new File(root)); scanner.setClassLoader(ClassScanner.class.getClassLoader()); final File testDir = new File(root, "com/foo/tests"); scanner.setFilter(new FileFilter() { public boolean accept(File pathname) { return pathname.getPath().startsWith(testDir.getPath()); } }); Class[] classes = scanner.getClasses(); return classes; }
context是一個ServletContext實例,他是通過調用servlet或JSP頁面得到的。
注冊測試
注冊測試類的動作告訴了TestNG他要查看的一組類,TestNG將在這組類中查找需要執行哪些測試。他將檢查每個指定的類,確定他是否包涵測試方法或配置方法。當所有類都檢查過后,TestNG內部會生成一個依賴關系圖,以決定照到的這些測試的執行次序。
tester.setTestClasses(classes);
注冊結果監聽者
TestNG自代了3個默認的報告類:
SuiteHTMLRepoter 默認報告類,他在一個目錄下輸出交叉引用HTML文件,讓您能看到某個具體測試的結果。
FailedReporter: 這個報高生成一個TestNG執行配置,該配置包含了所有前一次運行時失敗的測試。他也是默認運行的。
EmailableReporter: 這個報告類生成一個報告文件,可以很容易地通過電子郵件發送,顯示測試的結果。
默認情況下,EmailableReporter在磁盤上生成一個文件。
public class SinglePageReporter extends EmailableReporter { private Writer writer; public SinglePageReporter() { this.writer = writer; } protected PrintWriter createWriter(String out) { return new PrintWriter(writer); } }
調用TestNG的JSP頁面
<%@ page import="org.testng.*, java.io.*" %> <% TestNG tester = new TestNG(); tester.setDefaultSuiteName("container-tests"); String root = application.getRealPath("/WEB-INF/classes"); ClassScanner scanner = new ClassScanner(new File(root)); scanner.setLoader(getClass().getClassLoader()); scanner.setFilter(new FileFilter() { public boolean accept(File pathname) { return pathname.getPath().indexOf(Test) > -1; } }); Class[] classes = scanner.getClasses(); tester.setTestClasses(classes); IReporter reporter = new SinglePageReporter(out); tester.addListener(reporter); tester.run(); %>
Java命名和目錄接(JNDI)
JNDI是一個在全局目錄中查找資源的API。可以把他看成是一個很大的樹型結構,我們在其中按照名稱查找某個節點。
new InitialContext().lockup("someObject");
上面創建一個InitialContext對象,如果在執行容器內執行,會利用供應商提供的API實現,來查找容器內部命名目錄結構。創建了上下文之后,我們在其中查找對象,列出他的內容,或遍歷這棵樹。所有這些都是通過JNDI API來完成的。InitialContext的構造方法由一個重載的版本,接受一個Hashtable對象作為參數,其中包含不同的環境變量值,決定了上下文應該如何創建。
Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, ""); env.put(Context.PROVIDER_URL, "smqp://localhost:4001"); Object topic = new InitialContext(env).lookup("myTopic");
避免JNDI
組件依賴關系要么通過服務定位(通常是JNDI)來實現,要么通過注入來實現。如果您可以選擇,就采用注入的方式,因為這樣測試的開銷最小,並且這種方式帶來了更大的靈活性。
Java消息服務(JMS)
private Session session; private Destination destination; @BeforeClass(groups = "jms") public void setupJMS() throws NamingException, JMSException { Hashtable env = new Hashtable(); // populate environmet for out specific provider InitialContext context = new InitialContext(env); ConnectionFactory factory = (ConnectionFactory) context.lookup("QueueConnectionFactory"); Connection connection = factory.createConnection(); session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); destination = (Detination) context.lookup("TestQueue@router1"); } @Test(groups = "jms") public void sendMessage() throws JMSException { TextMessage msg = session.createTextMessage(); msg.setText("hello!"); msg.setStringProperty("test", "test1"); session.createProducer(destination).send(msg); } @Test(groups = "jms", dependsOnMethods = "sendMessage", timeOut = 1000) public void receiveMessage() throws JMSException { MessageConsumer consumer = session.createConsumer(destination, "test", "test1"); TextMessage msg = (TextMessage) consumer.receive(); assert "hello!".equals(msg.getText()); }
在測試中使用ActiveMQ
@BeforeClass(groups = "jms") public void setupActiveMQ() throws Exception { BrokerService broker = new BrokerService(); broker.setPersistent(false); broker.setUseJmx(false); broker.start(); URI uri = broker.gettVmCnnectionURI(); ConnectionFactory factory = new ActiveMQConnectionFactory(uri); Connection connection = factory.createConnection(); connection.start(); session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); destination = session.createQueue("TestQueue@router1"); }
處理狀態
在JMS的例子中,當我們擁有多個測試時,會引發一個有趣的問題。因為測試是由一對方法組成的,所以讓我們假定同一個類中還有另一對發送/接收測試。
一種方法是將發送和接受者放在一個測試中,並在每個測試方法之前初始化消息代理。請注意兩點都要做到,因為讓消息代里在一對測試方法之前初始化是比較麻煩的。
另一種方法是使用消息分撿器。JMS消息分撿器讓我們能夠過濾接收到的消息,這樣可以只接收與分撿器匹配的消息。
Spring
Spring的測試包功能
TestNG通過自己的一組針對Spring的類來解決這些問題,這些類作為擴展提供。org.testng.spring.test包中包含了所有Spring在其測試包中提供的類,這些類已修改過,可以用在基於TestNG的測試中。
AbstractSpringContextTests
這是所有Spring測試的基類。他的主要職責是提供上下文的管理。這個類包含一個靜態map,其中包含所有注冊的Spring上下文。
AbstractSingleSpringContextTests
這個類擴展了AbstractSpringContextTests,提供了裝入一個ApplicationContext對象的鈎子。他的子類應該實現getConfigLocation(String[] paths)方法。這個方法返回一個字符串數組,指出了Spring配置文件的位置,通常是從classpath加載的。
import org.testng.annotation.Test; import org.testng.spring.test.AbstractSingleSpringContextTests; public class SpringContextTests extends AbstractSingleSpringContextTests { protected String[] getConfigLocations() { return new String[] {"/spring.xml"}; } @Test public void beanDefined() { assert applicationContext.getBean("myBean") != null; } }
Spring的配置方法被聲明在名為Spring-init的TestNG分組中。我們不必依賴於單個的onSetUp或ontearDown方法,可以根據需要聲明任意多個@BeforeMethod/@AfterMethod配置方法,只要指定他們依賴於spring-init,就可以確保他們在Spring執行完准備工作之后得到調用。
AbstractDependencyInjectionSpringContextTests
這個類提供的最有趣的功能就是向測試注入依賴。測試依賴關系可以表現為設值方法或成員字斷。測試也可以指定Spring應該對他們的樹性執行哪種類型的自動編織。
public class SpringInjectionTests extends AbstractDependncyInjectionSpringContextTests { private PersonManager manager; protected String[] getConfigLocation() { return new String[] {"/spring.xml"}; } public void setManager(PersonManager manager) { this.manager = manager; } @Test public void verifyManager() { assert manager != null; } }
這個類有一個事務管理器屬性。因為他派生自支持注入的類,配置文件中必需指定一個PlatforTransactionManager,以便注入。
public class StrpingTranscationTests extends AbstractTransactionalSpringContextTests { private PersonManager manager; public void setManager(PersonManager manager) { this.manager = manager } protected String[] getConfigLocation() { return new String[] {"/spring.xml"}; } @Test public void savePerson() { Person p = new Person(); manager.savePerson(p); assert p.getId() != null; // setComplete(); } }
我們沒有在測試中指定任何事務行為,超類自動處理了事務方面的問題,讓每個測試在他自己的事務中執行,這個十五將在該測試完成之后回滾。
增加的調用setComplete通知超類在測試執行之后應該提交這個事務,而不是回滾。調用這個方法由一個有趣的副作用:這個類中所有后續測試都會提交事務,而不是依賴於默認行為。
答案在於JUnit和TestNG之間的一個微妙區別。Spring測試假定采用了JUnit的語義。每個測試類對每個測試方法都會重新實例化。因此,所有的測試都假定每個測試開始時,測試時里的狀態都會復原,但TestNG不是這樣的。
AbstractTransactionalDataSouceSpringContextTests
這個類添加了一些JDBC相關的便利方法。
AbstractAnnotationAwareTransactionalTests
這個類支持超類提供的所有功能之外,這個類允許我們在測試方法上指定Spring特有的事務annotation,而不是通過編程的方式來指定事務行為。
Guice
第2章中的例子,對於每個接口,我們都有兩個實現。一個是實際的產品實現,他會與一些外部的依賴關系進行交互,如UserDAO對象會與數據庫交互,Mailer對象會與SMTP郵件服務器交互。我們還有樁對象實現。
@Test public void verifyCreateUser() { UserManager manager = new UserManagerImpl(); MailerStub mailer = new MailerStub(); manager.setMailer(mailer); manager.setDao(new UserDAOStub()); manager.createUser("tester"); assert mailer.getMails().size() == 1; }
Guice注入測試
@Inject private UserManager manager; @Inject private MailerStub mailer; @Test public void verifyCreateUser() { manager.createUser("tester"); assert mailer.getMails().size() == 1; }
Spring注入測試
private UserManager manager; private MailerStub mailer; public void verifyCreateUser() { manager.createUser("tester"); assert mailer.getMails().size() == 1; } public void setManager(UserManager manager) { this.manager = manager; } public void setMailer(MailerStub mailer) { this.mailer = mailer; }
對象工廠
雜談
關注和提供異常
一層遇到了一個沒有預料到的錯誤,不知道如何處理。所以這一層就快樂地向上一層拋出一個異常,希望這個可憐的異常最終會遇到知道怎么作的人。
吞掉拋出異常
try { callBackend(); } catch (SQLException ex) { throw new BackendException("Error in backed"); }
這種方法問題在於,實際上我們丟試了真實錯誤的有意義的信息。當我們最后有機會處理這個異常的時候,我們得到的信息僅僅是某一層中出了問題,但我們不知道是在這一曾本身出了問題,還是更低的層出了問題。
記日志並拋出
try { callBackend(); } catch (SQLException ex) { log.error("Error calling backend", ex); throw ex; }
問題在於調用棧經常發生的情況:信息隱藏。假定一個應用程序有三層或四層,每一層都在日志中記錄他處里的異常,查遍8頁的調用棧信息並不是最有效地方式。
嵌套拋出
try { callBackend(); } catch (SQLException ex) { throw new BackendException("Error in backend", ex); }
當這個調用棧顯示的時候,沒有絲毫暗示表明背后的原因是什么。您必須編寫一個幫助方法,從外面的包裝中取出背后實際的異常。
我們建議兩種解決方案
避免需檢察異常。運行時異常很合適這樣的情況。
包裝異常。假如您非常肯定在調用棧打印時,背后的原因會顯示出來,那么包裝的異常也很好。
有狀態測試
有兩種截然不同的狀態分類:不可修改的狀態和可修改的狀態
不可修改的狀態
訪問共享的不可修改的狀態的測試方法,相互之間是獨立的。
因為這些方法都不能修改他們讀到的狀態,所以調用順序可以是任意的,因此他們沒有違反測試訪法應該彼此肚里的原則。
可修改的狀態
public class MyTest extends TestCase { private int count = 0; public void test1() { count++; assertEquals(1, count); } public void test2() { count++; assertEquals(2, count); } }
JUnit會讓這個測試通過,但TestNG不會。
只有當您知道測試方法被調用的順序時,共享可修改的狀態才有意義。
安全的共享數
安全 不可修改的狀態
安全 可修改的狀態與完全指定的依賴關系
不安全 可修改的狀態與不確定的依賴關系
測試驅動開發的缺點
他注重微觀設計超過宏觀測試
他在實踐中難以應用
TDD注重微觀設計超過宏觀設計
測試驅動開發可以得到更健壯的軟件,但他也可能導致不必要的反復和過度重構的趨勢,這可能對飲的軟件、設計和最后期限產生負面影響。
TDD難以應用
這種方式得到的代碼本身並不會優先於傳統方法測試的代碼。
如何創建測試並不重要,重要的是確實要創建測試
測試私有方法
如果他有可能出問題,您就應該測試他。
測試私有方法的比較好的方法是提升方法的可見性,例如,讓他變成保護可見,或者包可見。后者更好一些,因為您可以把測試和被測類放在同一個包中,然后您就可以訪問他的所有字段。如果這些方法都不可取,我們測試私有方法的方式就是利用反射。
public class LoginController { private String userName; private void init() { this.username = getUserNameFromCookie(); } } @Test public void verifyInit() { LoginController lc = new LoginController(); Field f = lc.getClass().getField("username"); Object valueBefore = f.get(lc); Assert.assertNull(valueBefore); Method m = lc.getClass().getDeclaredMethod("init", null); m.setAccessible(true); m.invock(lc); Object valueAfter = f.get(lc); Assert.assertNotNull(vlaueAfter); }
我們利用字符串來描述Java元素。如果您對這些元素進行重命名,這種危險的實踐肯定會失效。
我們利用了該類危險的私有信息。我們不僅假定存在一些私有方法和屬性,而且也假定這個方法將以某種方式修改一個字段。
測試與封裝
如果讓代碼更可測試,不要擔心破壞封裝。可測試星應該勝過封裝。
讓一個私有方法(或字段)成為包可見的、保護可見的或公有的。
去掉方法的final限定符。這讓測試勒能夠擴展這些類,或重寫這些方法,模擬或稍微改變他們的實現,從而讓系統的其它部分更可測試。
記日志的最佳實踐
在出錯時,輸出錯誤或警告是合理的。但是對於警告的情況,重要的是確定這是不是該做的事情。我們添加的每一條無用的日志信息都會干擾有用的信息,所已精心選擇是有意義的。
對於調試需求,記日志是有用的。但是,只要有一個開關能打開或關閉,絕大多數的記日志需求都能夠滿足了,不需要復雜的解決方案。