簡介
測試 在軟件開發中是一個很重要的方面,良好的測試可以在很大程度決定一個應用的命運。
軟件測試中,主要有3大種類:
-
單元測試
單元測試主要是用于測試程序模塊,確保代碼運行正確。單元測試是由開發者編寫并進行運行測試。一般使用的測試框架是 JUnit 或者 TestNG。測試用例一般是針對方法 級別的測試。 -
集成測試
集成測試用于檢測系統是否能正常工作。集成測試也是由開發者共同進行測試,與單元測試專注測試個人代碼組件不同的是,集成測試是系統進行跨組件測試。 -
功能性測試
功能性測試是一種質量保證過程以及基于測試軟件組件的規范下的由輸入得到輸出的一種黑盒測試。功能性測試通常由不同的測試團隊進行測試,測試用例的編寫要遵循組件規范,然后根據測試輸入得到的實際輸出與期望值進行對比,判斷功能是否正確運行。
概述
本文只對 單元測試 進行介紹,主要介紹如何在 Android Studio 下進行單元測試,單元測試使用的測試框架為 JUnit
好處
可能目前仍有很大一部分開發者未使用 單元測試 對他們的代碼進行測試,一方面可能是覺得沒有必要,因為即使沒有進行單元測試,程序照樣運行得很好;另一方面,也許有些人也認同單元測試的好處,但是由于需要額外的學習成本,所以很多人也是沒有時間或者說是沒有耐心進行學習······
這里我想說的是,如果大家去看下 github 上目前主流的開源框架,star 數比較多的項目,一般都有很詳盡的測試用例。所以說,單元測試對于我們的項目開發,還是挺有好處的。
至于單元測試的好處,我這里提及幾點:
- 保證代碼運行與我們預想的一樣,代碼正確性可以得到保證
- 程序運行出錯時,有利于我們對錯誤進行查找(因為我們忽略我們測試通過的代碼)
- 有利于提升代碼架構設計(用于測試的用例應力求簡單低耦合,因此編寫代碼的時候,開發者往往會為了對代碼進行測試,將其他耦合的部分進行解耦處理)
······
JUnit 簡介
JUnit is a simple framework to write repeatable tests. It is an instance of the xUnit architecture for unit testing frameworks.
JUnit 是一個支持可編寫重復測試用例的簡單框架。它是 xUnit 單元測試框架架構的一個子集。
名稱 | 解釋 |
---|---|
Assertions | 單元測試實用方法 |
Test Runners | 測試實例應當怎樣被執行(測試運行器) |
Aggregating tests in Suites | 合并多個相關測試用例到一個測試套件中(當運行測試套件時,相關用例就會一起被執行) |
Test Execution Order | 指定測試用例運行順序 |
Exception Testing | 如何指定測試用例期望的異常 |
Matchers and assertThat | 如何使用 Hamcrest 的匹配器 (matchers ) 和更加具備描述性的斷言 (assertions ) |
Ignoring Tests | 失能類或方法的測試用例 |
Timeout for Tests | 指定測試用例的最大運行時間(超過這個時間,自動結束測試用例) |
Parameterized Tests | 測試用例運行多次,每次都使用不同的參數值 |
Assumptions with Assume | 類似斷言,但不會使測試用例失敗 |
Rules | 為測試用例增加Rules (相當于添加功能) |
Theories | 使用隨機生成的數據使測試用例更加科學嚴謹 |
Test Fixtures | 為測試方法或者類指定預備的set up 和clean up 方法 |
Categories | 將測試用例組織起來,方便過濾 |
··· | ··· |
Assertions - 斷言
JUnit 為所有的原始類型和對象,數組(原始類型數組或者對象數組)提供了多個重載的斷言方法(assertion method
)。斷言方法的參數第一個為預期值,第二個為實際運行的值。另一個可選方法的第一個參數是作為失敗輸出的字符串信息。還有一個稍微有些區別的斷言方法:assertThat
。assertThat
的參數有一個可選的失敗信息輸出,實際運行的值和一個 Matcher
對象。請知悉assertThat
的預期值和實際運行值與其他的斷言方法位置是相反的。
ps:實際開發中,建議采用 Hamcrest 提供的斷言方法:assertThat
,因為這個方法一方面寫出的代碼更具可讀性,一方面當斷言失敗時,這個方法會給出具體的錯誤提示信息。
更多的 Assertions 信息,請查看文檔:Assert
Test Runners - 測試運行器
當一個類被注解@RunWith
或者集成一個被@RunWith
注解的類時,JUnit 會把測試用例運行在該類上,而不是內置的運行器上。
ps: JUnit 的默認運行器是 BlockJUnit4ClassRunner
。
如果類注解為@RunWith(JUnit4.class)
,則使用的是默認的測試運行器 BlockJUnit4ClassRunner
。
更多詳細信息,請查看文檔:@RunWith
Aggregating tests in Suites - 測試套件
使用套件(Suite
)作為運行器使得你可以手動建造一個可以容納許多類的測試用例。使用測試套件時,你需要創建一個類,然后為其注解上@RunWith(Suite.class)
和@SuiteClasses(TestClass1.class, ...)
,這樣,當你運行這個類時,測試套件各個類的測試用例就會全部被執行。
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
TestFeatureLogin.class,
TestFeatureLogout.class,
TestFeatureNavigate.class,
TestFeatureUpdate.class
})
public class FeatureTestSuite {
// the class remains empty,
// used only as a holder for the above annotations
}
Test Execution Order
從 JUnit 4.11版本開始,JUnit 默認使用確定的,不可預見性的測試用例執行順序(MethodSorters.DEFAULT
)。要改變測試用例執行順序,只需簡單為測試類添加@FixMethodOrder
注解,并指定一個方法排序規則:
@FixMethodOrder(MethodSorters.JVM)
:由JVM決定方法執行順序,在不同的JVM上,執行順序可能不同。
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
:按方法名進行排序(字典序)進行執行。
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestMethodOrder {
@Test
public void testA() {
System.out.println("first");
}
@Test
public void testB() {
System.out.println("second");
}
@Test
public void testC() {
System.out.println("third");
}
}
Exception Testing
你如何驗證代碼拋出的異常是你所期望的?驗證代碼正常走完是很重要,但是確保代碼在異常情況下表現也與預期一樣也是很重要的,比如:
new ArrayList<Object>().get(0);
這句代碼應該拋出一個 IndexOutOfBoundsException
異常。@Test
注解有一個可選的參數 expected
,它可以攜帶一個Throwable
的子類。如果我們希望驗證ArrayList
能正確拋出一個異常,我們應該這樣寫:
@Test(expected = IndexOutOfBoundsException.class)
public void empty() {
new ArrayList<Object>().get(0);
}
參數expected
的使用應該慎重。只要測試代碼中的任何一句拋出一個IndexOutOfBoundsException
異常,那么上面的測試用例就會通過。對于代碼比較長的測試用例,推薦使用 ExpectedException
規則。
更多詳情,請查看:Exception testing
-
assertThat
的一個通用格式為:
assertThat([value], [matcher statement])
示例:
assertThat(x, is(3));
assertThat(x, is(not(4)));
assertThat(responseString, either(containsString("color")).or(containsString("colour")));
assertThat(myList, hasItem("3"));
assertThat
的第二個參數是一個Matcher
.
詳細的Matcher
介紹,可以查看以下兩個文檔:
-
JUnit Matchers:JUnit 提供的
Matcher
-
Hamcrest CoreMatchers:Hamcrest 提供的
Matcher
Ignoring Tests
由于某些原因,你不希望測試用例運行失敗,你只想忽略它,那你只需暫時失能這個測試用例即可。
在 JUnit 中,你可以通過注釋方法或者刪除@Test
注解來忽略測試用例;但是這樣的話測試運行器就不會對該測試用例進行相關報告。另一個方案是為測試用例在@Test
注解前面或后面添加上@Ignore
注解;那么測試運行器運行后,就會輸出相關測試用例忽略數目,運行所有測試用例的數目和測試用例失敗的數目顯示。
注意下@Ignore
注解可以攜帶一個可選參數(String
類型),如果你想記錄測試用例忽略的原因,可以使用這個參數:
@Ignore("Test is ignored as a demonstration")
@Test
public void testSame() {
assertThat(1, is(1));
}
Timeout for Tests
對于失控或者運行時間太長的測試用例,則自動被認為失敗,有兩種方法可以實現這個動作。
- 為
@Test
增加timeout
參數
你可以為一個測試用例指定一個超時時間(毫秒),在規定時間內,如果測試用例沒有運行結束,那么測試用例運行所在線程就會拋出一個異常,從而引起測試失敗。
@Test(timeout=1000)
public void testWithTimeout() {
...
}
這種實現方式是通過將測試用例方法運行在另一個單獨的線程中。如果測試用例運行時間超過規定的時間,那么測試用例就會失敗,JUnit 就會打斷執行測試用例的線程。如果測試用例內部執行有可以中斷的操作,那么運行測試用例的線程就會退出(如果測試用例內部是一個無限循環,那么運行測試用例的線程將會永遠運行,而其他測試用例仍在其他的線程上執行)。
-
Timeout Rule (應用到測試類的所有測試用例)
Timeout Rule
會將同一個超時時間應用到測試類的所有測試方法中,并且如果測試用例@Test
帶有timeout
參數,則會疊加到一起(實際測試中,并沒有疊加的效果,甚至tiemout
參數并不生效,依舊還是以Timeout Rule
為準)
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
public class HasGlobalTimeout {
public static String log;
private final CountDownLatch latch = new CountDownLatch(1);
@Rule
public Timeout globalTimeout = Timeout.seconds(10); // 10 seconds max per method tested
@Test
public void testSleepForTooLong() throws Exception {
log += "ran1";
TimeUnit.SECONDS.sleep(100); // sleep for 100 seconds
}
@Test
public void testBlockForever() throws Exception {
log += "ran2";
latch.await(); // will block
}
}
Timeout rule
指定的超時時間timeout
會應用到所有的測試用例中,包括任何的@Before
和@After
方法。如果測試方法是一個無限循環(或者是無法響應中斷操作),那么@Afte
注解的方法永遠不會被執行。
Parameterized Tests - 參數化測試
對于單元測試來說,如果想要同一個測試用例中測試多組不同的數據,那么只能手動執行一次后,更改數據,再進行執行,而使用參數化測試的話,則可以將上述的行為進行自動化,我們所需要做的就是提供一個數據集合,然后創建相應的成員變量用來接收數據集合傳遞過來的數據(在測試類構造器中接收),最后運行測試用例時,參數化測試運行器就會依次從數據集合中取出一個數據,并傳給測試用例運行:
//功能類
public class Math {
public static int add(int a, int b) {
return a + b;
}
}
//單元測試類
@RunWith(Parameterized.class) //指定參數化測試運行器
public class MathTest {
private int a; //聲明成員變量用于接收數據
private int b;
public MathTest(int a, int b) { //接受集合數據
this.a = a;
this.b = b;
}
@Parameterized.Parameters //創建參數集合
public static Collection<Object[]> data() {
Collection<Object[]> collection = new ArrayList<>();
collection.add(new Object[]{1, 2});
collection.add(new Object[]{10, 20});
collection.add(new Object[]{30, 40});
return collection;
}
@Test
public void add() throws Exception {
assertThat(Math.add(a, b), is(equalTo(30)));
}
}
Assumptions with Assume - 前置條件
前置條件與斷言類似,只是斷言在不匹配時,測試用例就會失敗,而前置條件在不匹配時只會使測試用例退出。
前置條件的使用場景是:當你的代碼在不同的環境下,可能有不同的結果時,如果你明確后續的測試代碼是基于某一特定的環境下,才進行測試,那么,借助前置條件,就可以實現所需功能。
比如,假設 Windows 平臺的文件路徑分隔符為"\",而 Linux 平臺的為"/”,假設我們的測試用例只想在 Linux 平臺上進行測試,那么:
@Test
public void filenameIncludesUsername() {
assumeThat(File.separatorChar, is('/'));
assertThat(new User("optimus").configFileName(), is("configfiles/optimus.cfg"));
}
如果在 Windows 平臺運行測試用例時,assumeThat(File.separatorChar, is('/'))
就會不匹配,那么測試用例就直接退出(類似異常機制)。
Rules - 規則
Rules
允許為測試用例增加靈活的條件或者是重新定義每個類的測試用例行為。測試類可以重新或者繼承一下任一提供的Rules
,或者自己自定義一個。
Rule | Description |
---|---|
TemporaryFolder | 創建臨時文件夾/文件(測試方法完成后文件被自動刪除) |
ExternalResource | 外部資源Rules 的一個基類 |
ErrorCollector | 收集錯誤信息 |
Verifier | 具備校驗功能的一個基類 |
TestWatcher | 具備測試結果記錄的一個基類 |
TestName | 該Rules 對象可在測試用例內部獲取測試用例方法名 |
Timeout | 為測試類所有測試用例約束最長運行時間 |
ExpectedException | 該類使得測試用例能在方法內判別測試代碼是否拋出預期異常 |
ClassRule | 類級別Rule ,用于靜態變量的注解,在測試類運行時只執行一次 |
Rule | 方法級別的Rule ,用于成員變量的注解,在類的每個測試用例執行時都會被執行 |
RuleChain | 為多個Rules 指定順序 |
TestRule | 自定義Rules 基類 |
這里簡單介紹下自定義Rules
,假設我們要為所有的測試用例輸出前后添加"------------",那么,我們需要先創建一個Rule
:
public class CustomerRule implements TestRule {
@Override
public Statement apply(final Statement base, Description description) {
return new Statement(){
@Override
public void evaluate() throws Throwable {
System.out.println("--------------------------");
base.evaluate();
System.out.println();
System.out.println("--------------------------");
}
};
}
}
然后把自定義的TestRule
運用到測試類里面即可:
@Rule
public CustomerRule customerRule = new CustomerRule();
@Test
public void testCustom() {
assertThat(1, is(1));
}
更多Rules
詳細信息,請查看:Rules
Theories - 測試理論
JUnit 中的 Theories 可以理解成一個測試理論,該理論把測試分為兩部分:一個是提供測試數據(單個數據用@DataPoint
注解,集合數據使用@DataPoints
注解),數據提供者必須為靜態成員/方法;另一個是理論本身,也即測試用例方法。
Theories 的測試用例允許參數傳遞(普通測試用例測試方法不能攜帶參數),參數傳遞規則是首先從數據集合中取出一個作為第一個參數,然后依次取出集合的元素(包含已作為參數1的那個數據)作為第二個參數····
看下下面的測試用例就會比較清楚 Theories 的運作流程:
@RunWith(Theories.class)
public class MathTest {
// @DataPoint
// public static int arg0 = 1;
// @DataPoint
// public static int arg1 = 10;
// @DataPoint
// public static int arg2 = 0;
@DataPoints
public static int[] args = new int[]{1, 10, 0};
@Theory
public void divied(int a, int b) throws Exception {
Assume.assumeTrue(b != 0);
System.out.println(String.format("a=%d,b=%d", a, b));
assertThat(Math.divied(a, b), not(equalTo(2)));
}
}
運行結果如下:
從上面的測試用例可以看出,
MathTest
提供的數據集合為{1,10,0}
,所以:第一次 運行測試用例
divied(int a, int b)
時,從集合中取出一個參數,即1
會傳遞給參數a
,然后又從集合中取出一個參數,也是1
,傳遞給b
,然后執行測試用例;第二次 運行時,參數
a
保持不變,然后從新從集合中取出下一個元素給到b
,所以b=10
,然后執行測試用例;第三次 運行時,參數
a
保持不變,然后從新從集合中取出下一個元素給到b
,所以b=0
,然后執行測試用例時,由于不滿足Assume
前置條件,故測試用例不再往下運行,直接退出,所以看到當b=0
時,沒有打印結果;第四次 運行時,由于
b
在前面第一輪運行時已完整取出了整個集合數據,所以此時就輪到參數a
取出集合的下一個數據,即a=10
,然后就按照前一輪的執行邏輯繼續執行下去。
從上面的分析中可以看出,Theories 與 Parameterized Tests 很類似,兩者都實現了多組數據共同作用于同一個測試用例的功能,不過兩者的參數傳遞機制還是有很大的不同的, Parameterized Tests 可以提供多維數組的形式符合參數個數順序,而 Theories 的參數集合中的每個元素都會同時作用于各個參數;個人感覺還是 Parameterized Tests 更符合通常的測試邏輯。
Test Fixtures - 測試設備
Test Fixtures 是被用作測試用例運行的基準的一系列對象的混合狀態,Test Fixtures 為我們提供了4個注解(均用于方法上):
Annotation | Description |
---|---|
@BeforeClass | 測試類運行時執行 |
@AfterClass | 測試類結束時執行 |
@Before | 每個測試用例執行前先執行 |
@After | 每個測試用例執行后再執行 |
Categories - 分類
Categories 見名知意,就是將一系列測試類/測試方法進行分類,每個類或者接口都可以作為一個Category
,且支持類別繼承。
比如,你指定一個測試用例屬于SuperClass.class
的類別(使用@Category(SuperClass.class)
注解在測試類用例上),然后@IncludeCategory(SuperClass.class)
,那么任何測試用例上注解了@Category(SuperClass.class)
或者@Category({SubClass.class})
的方法都會被執行。
舉個例子:
- 首先我們需要定義一個或多個測試類別(即
Category
)
public class Category {
public static interface Category01 {}
public static interface Category02 {}
public static interface Category01Impl extends Category01{}
}
這里有3種測試Category
,其中,類別Category01Impl
繼承了類別Category01
,所以任何@IncludeCategory(Category01.class)
的測試類,測試時也會執行類別為Category01Impl
的測試用例。
- 定義好了測試類別后,我們就需要將這些類別運用到測試類或者測試用例上
public class Tests {
public static class Test01 {
@Test
@Category(Category01.class) //運用到測試用例上
public void test01() {
System.out.println("This testCase belongs to Category01");
}
@Test
@Category(Category01Impl.class)//運用到測試用例上
public void test01Impl() {
System.out.println("This testCase belongs to Category01Impl");
}
}
@Category(Category02.class)//運用到測試類上,類中所有測試方法都屬于`Category02.class`這個類別
public static class Test02 {
@Test
public void test02() {
System.out.println("This testCase belongs to Category02");
}
}
}
- 最后,再
Categories
類別測試運行器上運行需要的測試用例即可
@RunWith(Categories.class)
@IncludeCategory(Category01.class)
@SuiteClasses({Tests.Test01.class, Tests.Test02.class}) // Note that Category is a kind of Suite
public class CategoryTest {
}
更多詳細信息,請查看:Categories
Android Studio 進行單元測試
假設我們需要對一個 Java Module 進行單元測試,采用 JUnit 框架,則部署步驟如下:
- 在
build.gralde
中依賴 JUnit:
dependencies {
testImplementation 'junit:junit:4.12' //or testCompile
}
- 創建一個類
public class Math {
public static int add(int a, int b) {
return a + b;
}
}
- 對上面的類
Math
的add
方法進行測試
我們可以手動創建一個Math
的測試類,但是借助于 Android Studio,我們可以很方面的使用快捷操作自動生成測試類和測試用例,具體做法為:打開要進行測試的類文件,雙擊類名/方法名進行選中,然后按快捷鍵:<Ctrl-Shift-T>
- 最后,寫上測試代碼,進行測試就可以了。
更多詳細信息,請查看官網:Building Local Unit Tests