簡介
測試 在軟件開發(fā)中是一個很重要的方面羔杨,良好的測試可以在很大程度決定一個應(yīng)用的命運。
軟件測試中,主要有3大種類:
-
單元測試
單元測試主要是用于測試程序模塊轧铁,確保代碼運行正確端铛。單元測試是由開發(fā)者編寫并進(jìn)行運行測試泣矛。一般使用的測試框架是 JUnit 或者 TestNG。測試用例一般是針對方法 級別的測試禾蚕。 -
集成測試
集成測試用于檢測系統(tǒng)是否能正常工作您朽。集成測試也是由開發(fā)者共同進(jìn)行測試,與單元測試專注測試個人代碼組件不同的是,集成測試是系統(tǒng)進(jìn)行跨組件測試哗总。 -
功能性測試
功能性測試是一種質(zhì)量保證過程以及基于測試軟件組件的規(guī)范下的由輸入得到輸出的一種黑盒測試几颜。功能性測試通常由不同的測試團(tuán)隊進(jìn)行測試,測試用例的編寫要遵循組件規(guī)范讯屈,然后根據(jù)測試輸入得到的實際輸出與期望值進(jìn)行對比蛋哭,判斷功能是否正確運行。
概述
本文只對 單元測試 進(jìn)行介紹涮母,主要介紹如何在 Android Studio 下進(jìn)行單元測試谆趾,單元測試使用的測試框架為 JUnit
好處
可能目前仍有很大一部分開發(fā)者未使用 單元測試 對他們的代碼進(jìn)行測試,一方面可能是覺得沒有必要叛本,因為即使沒有進(jìn)行單元測試沪蓬,程序照樣運行得很好;另一方面来候,也許有些人也認(rèn)同單元測試的好處怜跑,但是由于需要額外的學(xué)習(xí)成本,所以很多人也是沒有時間或者說是沒有耐心進(jìn)行學(xué)習(xí)······
這里我想說的是吠勘,如果大家去看下 github 上目前主流的開源框架性芬,star 數(shù)比較多的項目,一般都有很詳盡的測試用例剧防。所以說植锉,單元測試對于我們的項目開發(fā),還是挺有好處的峭拘。
至于單元測試的好處俊庇,我這里提及幾點:
- 保證代碼運行與我們預(yù)想的一樣,代碼正確性可以得到保證
- 程序運行出錯時鸡挠,有利于我們對錯誤進(jìn)行查找(因為我們忽略我們測試通過的代碼)
- 有利于提升代碼架構(gòu)設(shè)計(用于測試的用例應(yīng)力求簡單低耦合辉饱,因此編寫代碼的時候,開發(fā)者往往會為了對代碼進(jìn)行測試拣展,將其他耦合的部分進(jìn)行解耦處理)
······
JUnit 簡介
JUnit is a simple framework to write repeatable tests. It is an instance of the xUnit architecture for unit testing frameworks.
JUnit 是一個支持可編寫重復(fù)測試用例的簡單框架彭沼。它是 xUnit 單元測試框架架構(gòu)的一個子集。
名稱 | 解釋 |
---|---|
Assertions | 單元測試實用方法 |
Test Runners | 測試實例應(yīng)當(dāng)怎樣被執(zhí)行(測試運行器) |
Aggregating tests in Suites | 合并多個相關(guān)測試用例到一個測試套件中(當(dāng)運行測試套件時备埃,相關(guān)用例就會一起被執(zhí)行) |
Test Execution Order | 指定測試用例運行順序 |
Exception Testing | 如何指定測試用例期望的異常 |
Matchers and assertThat | 如何使用 Hamcrest 的匹配器 (matchers ) 和更加具備描述性的斷言 (assertions ) |
Ignoring Tests | 失能類或方法的測試用例 |
Timeout for Tests | 指定測試用例的最大運行時間(超過這個時間姓惑,自動結(jié)束測試用例) |
Parameterized Tests | 測試用例運行多次,每次都使用不同的參數(shù)值 |
Assumptions with Assume | 類似斷言按脚,但不會使測試用例失敗 |
Rules | 為測試用例增加Rules (相當(dāng)于添加功能) |
Theories | 使用隨機(jī)生成的數(shù)據(jù)使測試用例更加科學(xué)嚴(yán)謹(jǐn) |
Test Fixtures | 為測試方法或者類指定預(yù)備的set up 和clean up 方法 |
Categories | 將測試用例組織起來于毙,方便過濾 |
··· | ··· |
Assertions - 斷言
JUnit 為所有的原始類型和對象,數(shù)組(原始類型數(shù)組或者對象數(shù)組)提供了多個重載的斷言方法(assertion method
)辅搬。斷言方法的參數(shù)第一個為預(yù)期值唯沮,第二個為實際運行的值。另一個可選方法的第一個參數(shù)是作為失敗輸出的字符串信息。還有一個稍微有些區(qū)別的斷言方法:assertThat
介蛉。assertThat
的參數(shù)有一個可選的失敗信息輸出夯缺,實際運行的值和一個 Matcher
對象。請知悉assertThat
的預(yù)期值和實際運行值與其他的斷言方法位置是相反的甘耿。
ps:實際開發(fā)中踊兜,建議采用 Hamcrest 提供的斷言方法:assertThat
,因為這個方法一方面寫出的代碼更具可讀性佳恬,一方面當(dāng)斷言失敗時捏境,這個方法會給出具體的錯誤提示信息。
更多的 Assertions 信息毁葱,請查看文檔:Assert
Test Runners - 測試運行器
當(dāng)一個類被注解@RunWith
或者集成一個被@RunWith
注解的類時垫言,JUnit 會把測試用例運行在該類上,而不是內(nèi)置的運行器上倾剿。
ps: JUnit 的默認(rèn)運行器是 BlockJUnit4ClassRunner
筷频。
如果類注解為@RunWith(JUnit4.class)
,則使用的是默認(rèn)的測試運行器 BlockJUnit4ClassRunner
前痘。
更多詳細(xì)信息凛捏,請查看文檔:@RunWith
Aggregating tests in Suites - 測試套件
使用套件(Suite
)作為運行器使得你可以手動建造一個可以容納許多類的測試用例。使用測試套件時芹缔,你需要創(chuàng)建一個類坯癣,然后為其注解上@RunWith(Suite.class)
和@SuiteClasses(TestClass1.class, ...)
,這樣最欠,當(dāng)你運行這個類時示罗,測試套件各個類的測試用例就會全部被執(zhí)行。
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 默認(rèn)使用確定的蚜点,不可預(yù)見性的測試用例執(zhí)行順序(MethodSorters.DEFAULT
)。要改變測試用例執(zhí)行順序拌阴,只需簡單為測試類添加@FixMethodOrder
注解绍绘,并指定一個方法排序規(guī)則:
@FixMethodOrder(MethodSorters.JVM)
:由JVM決定方法執(zhí)行順序,在不同的JVM上皮官,執(zhí)行順序可能不同脯倒。
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
:按方法名進(jìn)行排序(字典序)進(jìn)行執(zhí)行实辑。
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
你如何驗證代碼拋出的異常是你所期望的捺氢?驗證代碼正常走完是很重要,但是確保代碼在異常情況下表現(xiàn)也與預(yù)期一樣也是很重要的剪撬,比如:
new ArrayList<Object>().get(0);
這句代碼應(yīng)該拋出一個 IndexOutOfBoundsException
異常摄乒。@Test
注解有一個可選的參數(shù) expected
,它可以攜帶一個Throwable
的子類。如果我們希望驗證ArrayList
能正確拋出一個異常馍佑,我們應(yīng)該這樣寫:
@Test(expected = IndexOutOfBoundsException.class)
public void empty() {
new ArrayList<Object>().get(0);
}
參數(shù)expected
的使用應(yīng)該慎重斋否。只要測試代碼中的任何一句拋出一個IndexOutOfBoundsException
異常,那么上面的測試用例就會通過拭荤。對于代碼比較長的測試用例茵臭,推薦使用 ExpectedException
規(guī)則。
更多詳情舅世,請查看: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
的第二個參數(shù)是一個Matcher
.
詳細(xì)的Matcher
介紹旦委,可以查看以下兩個文檔:
-
JUnit Matchers:JUnit 提供的
Matcher
-
Hamcrest CoreMatchers:Hamcrest 提供的
Matcher
Ignoring Tests
由于某些原因,你不希望測試用例運行失敗雏亚,你只想忽略它缨硝,那你只需暫時失能這個測試用例即可。
在 JUnit 中罢低,你可以通過注釋方法或者刪除@Test
注解來忽略測試用例查辩;但是這樣的話測試運行器就不會對該測試用例進(jìn)行相關(guān)報告。另一個方案是為測試用例在@Test
注解前面或后面添加上@Ignore
注解网持;那么測試運行器運行后宜岛,就會輸出相關(guān)測試用例忽略數(shù)目,運行所有測試用例的數(shù)目和測試用例失敗的數(shù)目顯示功舀。
注意下@Ignore
注解可以攜帶一個可選參數(shù)(String
類型)谬返,如果你想記錄測試用例忽略的原因,可以使用這個參數(shù):
@Ignore("Test is ignored as a demonstration")
@Test
public void testSame() {
assertThat(1, is(1));
}
Timeout for Tests
對于失控或者運行時間太長的測試用例日杈,則自動被認(rèn)為失敗遣铝,有兩種方法可以實現(xiàn)這個動作。
- 為
@Test
增加timeout
參數(shù)
你可以為一個測試用例指定一個超時時間(毫秒)莉擒,在規(guī)定時間內(nèi)酿炸,如果測試用例沒有運行結(jié)束,那么測試用例運行所在線程就會拋出一個異常涨冀,從而引起測試失敗填硕。
@Test(timeout=1000)
public void testWithTimeout() {
...
}
這種實現(xiàn)方式是通過將測試用例方法運行在另一個單獨的線程中。如果測試用例運行時間超過規(guī)定的時間鹿鳖,那么測試用例就會失敗扁眯,JUnit 就會打斷執(zhí)行測試用例的線程。如果測試用例內(nèi)部執(zhí)行有可以中斷的操作翅帜,那么運行測試用例的線程就會退出(如果測試用例內(nèi)部是一個無限循環(huán)姻檀,那么運行測試用例的線程將會永遠(yuǎn)運行,而其他測試用例仍在其他的線程上執(zhí)行)涝滴。
-
Timeout Rule (應(yīng)用到測試類的所有測試用例)
Timeout Rule
會將同一個超時時間應(yīng)用到測試類的所有測試方法中绣版,并且如果測試用例@Test
帶有timeout
參數(shù)胶台,則會疊加到一起(實際測試中,并沒有疊加的效果杂抽,甚至tiemout
參數(shù)并不生效诈唬,依舊還是以Timeout Rule
為準(zhǔn))
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
會應(yīng)用到所有的測試用例中,包括任何的@Before
和@After
方法缩麸。如果測試方法是一個無限循環(huán)(或者是無法響應(yīng)中斷操作)铸磅,那么@Afte
注解的方法永遠(yuǎn)不會被執(zhí)行。
Parameterized Tests - 參數(shù)化測試
對于單元測試來說杭朱,如果想要同一個測試用例中測試多組不同的數(shù)據(jù)愚屁,那么只能手動執(zhí)行一次后,更改數(shù)據(jù)痕檬,再進(jìn)行執(zhí)行霎槐,而使用參數(shù)化測試的話,則可以將上述的行為進(jìn)行自動化梦谜,我們所需要做的就是提供一個數(shù)據(jù)集合丘跌,然后創(chuàng)建相應(yīng)的成員變量用來接收數(shù)據(jù)集合傳遞過來的數(shù)據(jù)(在測試類構(gòu)造器中接收),最后運行測試用例時唁桩,參數(shù)化測試運行器就會依次從數(shù)據(jù)集合中取出一個數(shù)據(jù)闭树,并傳給測試用例運行:
//功能類
public class Math {
public static int add(int a, int b) {
return a + b;
}
}
//單元測試類
@RunWith(Parameterized.class) //指定參數(shù)化測試運行器
public class MathTest {
private int a; //聲明成員變量用于接收數(shù)據(jù)
private int b;
public MathTest(int a, int b) { //接受集合數(shù)據(jù)
this.a = a;
this.b = b;
}
@Parameterized.Parameters //創(chuàng)建參數(shù)集合
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 - 前置條件
前置條件與斷言類似,只是斷言在不匹配時荒澡,測試用例就會失敗报辱,而前置條件在不匹配時只會使測試用例退出。
前置條件的使用場景是:當(dāng)你的代碼在不同的環(huán)境下单山,可能有不同的結(jié)果時碍现,如果你明確后續(xù)的測試代碼是基于某一特定的環(huán)境下,才進(jìn)行測試米奸,那么昼接,借助前置條件,就可以實現(xiàn)所需功能悴晰。
比如慢睡,假設(shè) Windows 平臺的文件路徑分隔符為"\",而 Linux 平臺的為"/”铡溪,假設(shè)我們的測試用例只想在 Linux 平臺上進(jìn)行測試漂辐,那么:
@Test
public void filenameIncludesUsername() {
assumeThat(File.separatorChar, is('/'));
assertThat(new User("optimus").configFileName(), is("configfiles/optimus.cfg"));
}
如果在 Windows 平臺運行測試用例時,assumeThat(File.separatorChar, is('/'))
就會不匹配棕硫,那么測試用例就直接退出(類似異常機(jī)制)髓涯。
Rules - 規(guī)則
Rules
允許為測試用例增加靈活的條件或者是重新定義每個類的測試用例行為。測試類可以重新或者繼承一下任一提供的Rules
饲帅,或者自己自定義一個复凳。
Rule | Description |
---|---|
TemporaryFolder | 創(chuàng)建臨時文件夾/文件(測試方法完成后文件被自動刪除) |
ExternalResource | 外部資源Rules 的一個基類 |
ErrorCollector | 收集錯誤信息 |
Verifier | 具備校驗功能的一個基類 |
TestWatcher | 具備測試結(jié)果記錄的一個基類 |
TestName | 該Rules 對象可在測試用例內(nèi)部獲取測試用例方法名 |
Timeout | 為測試類所有測試用例約束最長運行時間 |
ExpectedException | 該類使得測試用例能在方法內(nèi)判別測試代碼是否拋出預(yù)期異常 |
ClassRule | 類級別Rule 瘤泪,用于靜態(tài)變量的注解灶泵,在測試類運行時只執(zhí)行一次 |
Rule | 方法級別的Rule 育八,用于成員變量的注解,在類的每個測試用例執(zhí)行時都會被執(zhí)行 |
RuleChain | 為多個Rules 指定順序 |
TestRule | 自定義Rules 基類 |
這里簡單介紹下自定義Rules
赦邻,假設(shè)我們要為所有的測試用例輸出前后添加"------------"髓棋,那么,我們需要先創(chuàng)建一個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
詳細(xì)信息惶洲,請查看:Rules
Theories - 測試?yán)碚?br>
JUnit 中的 Theories 可以理解成一個測試?yán)碚摪瓷摾碚摪褱y試分為兩部分:一個是提供測試數(shù)據(jù)(單個數(shù)據(jù)用@DataPoint
注解,集合數(shù)據(jù)使用@DataPoints
注解)恬吕,數(shù)據(jù)提供者必須為靜態(tài)成員/方法签则;另一個是理論本身,也即測試用例方法铐料。
Theories 的測試用例允許參數(shù)傳遞(普通測試用例測試方法不能攜帶參數(shù))渐裂,參數(shù)傳遞規(guī)則是首先從數(shù)據(jù)集合中取出一個作為第一個參數(shù),然后依次取出集合的元素(包含已作為參數(shù)1的那個數(shù)據(jù))作為第二個參數(shù)····
看下下面的測試用例就會比較清楚 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)));
}
}
運行結(jié)果如下:
從上面的測試用例可以看出钠惩,
MathTest
提供的數(shù)據(jù)集合為{1,10,0}
柒凉,所以:第一次 運行測試用例
divied(int a, int b)
時,從集合中取出一個參數(shù)篓跛,即1
會傳遞給參數(shù)a
膝捞,然后又從集合中取出一個參數(shù),也是1
,傳遞給b
愧沟,然后執(zhí)行測試用例蔬咬;第二次 運行時,參數(shù)
a
保持不變沐寺,然后從新從集合中取出下一個元素給到b
计盒,所以b=10
,然后執(zhí)行測試用例芽丹;第三次 運行時北启,參數(shù)
a
保持不變,然后從新從集合中取出下一個元素給到b
拔第,所以b=0
咕村,然后執(zhí)行測試用例時,由于不滿足Assume
前置條件蚊俺,故測試用例不再往下運行懈涛,直接退出,所以看到當(dāng)b=0
時泳猬,沒有打印結(jié)果批钠;第四次 運行時宇植,由于
b
在前面第一輪運行時已完整取出了整個集合數(shù)據(jù),所以此時就輪到參數(shù)a
取出集合的下一個數(shù)據(jù)埋心,即a=10
指郁,然后就按照前一輪的執(zhí)行邏輯繼續(xù)執(zhí)行下去。
從上面的分析中可以看出拷呆,Theories 與 Parameterized Tests 很類似闲坎,兩者都實現(xiàn)了多組數(shù)據(jù)共同作用于同一個測試用例的功能,不過兩者的參數(shù)傳遞機(jī)制還是有很大的不同的茬斧, Parameterized Tests 可以提供多維數(shù)組的形式符合參數(shù)個數(shù)順序腰懂,而 Theories 的參數(shù)集合中的每個元素都會同時作用于各個參數(shù);個人感覺還是 Parameterized Tests 更符合通常的測試邏輯项秉。
Test Fixtures - 測試設(shè)備
Test Fixtures 是被用作測試用例運行的基準(zhǔn)的一系列對象的混合狀態(tài)绣溜,Test Fixtures 為我們提供了4個注解(均用于方法上):
Annotation | Description |
---|---|
@BeforeClass | 測試類運行時執(zhí)行 |
@AfterClass | 測試類結(jié)束時執(zhí)行 |
@Before | 每個測試用例執(zhí)行前先執(zhí)行 |
@After | 每個測試用例執(zhí)行后再執(zhí)行 |
Categories - 分類
Categories 見名知意,就是將一系列測試類/測試方法進(jìn)行分類娄蔼,每個類或者接口都可以作為一個Category
怖喻,且支持類別繼承。
比如贷屎,你指定一個測試用例屬于SuperClass.class
的類別(使用@Category(SuperClass.class)
注解在測試類用例上)罢防,然后@IncludeCategory(SuperClass.class)
,那么任何測試用例上注解了@Category(SuperClass.class)
或者@Category({SubClass.class})
的方法都會被執(zhí)行唉侄。
舉個例子:
- 首先我們需要定義一個或多個測試類別(即
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)
的測試類恬叹,測試時也會執(zhí)行類別為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 {
}
更多詳細(xì)信息须蜗,請查看:Categories
Android Studio 進(jìn)行單元測試
假設(shè)我們需要對一個 Java Module 進(jìn)行單元測試硅确,采用 JUnit 框架,則部署步驟如下:
- 在
build.gralde
中依賴 JUnit:
dependencies {
testImplementation 'junit:junit:4.12' //or testCompile
}
- 創(chuàng)建一個類
public class Math {
public static int add(int a, int b) {
return a + b;
}
}
- 對上面的類
Math
的add
方法進(jìn)行測試
我們可以手動創(chuàng)建一個Math
的測試類明肮,但是借助于 Android Studio菱农,我們可以很方面的使用快捷操作自動生成測試類和測試用例,具體做法為:打開要進(jìn)行測試的類文件柿估,雙擊類名/方法名進(jìn)行選中循未,然后按快捷鍵:<Ctrl-Shift-T>
- 最后,寫上測試代碼秫舌,進(jìn)行測試就可以了的妖。
更多詳細(xì)信息绣檬,請查看官網(wǎng):Building Local Unit Tests