什么是單元測試?
Wikipedia 對(duì)單元測試的定義:
在計(jì)算機(jī)編程中,單元測試(Unit Testing)又稱為模塊測試,是針對(duì)程序模塊(軟件設(shè)計(jì)的最小單位)來進(jìn)行正確性檢驗(yàn)的測試工作。
在實(shí)際測試中,一個(gè)單元可以小到一個(gè)方法,也可以大到包含多個(gè)類。從定義上講,單元測試和集成測試是有嚴(yán)格的區(qū)分的,但是在實(shí)際開發(fā)中它們可能并沒有那么嚴(yán)格的界限。如果專門追求單元測試必須測試最小的單元,反而容易造成多余的測試并且不易維護(hù)。換句更嚴(yán)謹(jǐn)一點(diǎn)的說法,我們要考慮測試的場景再去選擇不同粒度的測試。
單元測試和集成測試即可以手工執(zhí)行,也可以是程序自動(dòng)執(zhí)行。但現(xiàn)在一般提到單元測試,都是指自動(dòng)執(zhí)行的測試。所以我們下面提到的單元測試,沒有特別注明,都是泛指自動(dòng)執(zhí)行的單元測試或集成測試。
單元測試入門
下面我們先看兩個(gè)案例,感受一下單元測試到底是什么樣子的。
例子 1:生命游戲單元測試
我們先看一個(gè)很簡單的例子,實(shí)現(xiàn)一個(gè)康威生命游戲。如果不了解康威生命游戲的話,可以看 Wikipedia 的介紹。假設(shè)我們實(shí)現(xiàn)時(shí)定義這樣的接口:
public interface Game {
void init(int[][] shape) ; // 初始化游戲 board
void tick(); // 行進(jìn)到在一個(gè)回合
int[][] get(); // 獲取當(dāng)前游戲 board
}
生命游戲有好幾條規(guī)則,為了測試我們的實(shí)現(xiàn)是否正確,我們可以針對(duì)生命游戲的每個(gè)規(guī)則,寫一個(gè)單元測試。下面測試的是復(fù)活的規(guī)則。
@Test
public void testRelive() {
int[][] shape = {{0, 0, 1}, {0, 0, 0}, {1, 0, 1}};
Game g = new GameImplSample(shape);
g.tick();
// 自己死亡,周圍3個(gè)存活狀態(tài),復(fù)活
assertEquals(1, g.get()[1][1]);
}
例子 2:訂單退款集成測試
我們?cè)诳匆粋€(gè)稍微復(fù)雜一些的例子,測試的是訂單退款的過程。
@Test
public void test300_doRefundItem() {
// 創(chuàng)建訂單、支付,然后退款
Order order = createOrder(OrderSource.XR_DOCTOR);
order = fullPay(order, PayType.WECHAT_JS);
OrderItem item = _doItemRefund(order, 1, false);
// 檢查退款中狀態(tài)
OrderWhole orderWholeRefunding = findOrderWhole(order.getOrderNo());
isTrue(orderWholeRefunding.getRefundStatus().equals(
OrderRefundStatus.PARTIAL_REFUNDING));
isTrue(orderWholeRefunding.getRefunds().get(0).getStatus().equals(
RefundStatus.REFUNDING));
isTrue(orderWholeRefunding.getRefunds().get(0).getItemId().get().equals(
item.getId()));
// 構(gòu)建退款的回調(diào)信息
List payments = findPayments(order.getId());
List refunds = findRefunds(order.getId());
wxRefundNotify(payments.get(0), refunds.get(0), WxRefundStatus.SUCCESS);
// 檢查退款后狀態(tài)
OrderWhole orderWholeFinish = assertRefund(order, FULL_PAID,
PARTIAL_REFUND_OK, RefundStatus.SUCCESS, RefundMode.ITEM, false);
isTrue(orderWholeFinish.getRefundFee() == item.getPaidPrice());
isTrue(orderWholeFinish.getIncomes().stream()
.filter(i -> i.getAmount() < 0).count() == 1);
}
單元測試執(zhí)行
單元測試有很多種執(zhí)行方式:
在 IDE 中執(zhí)行
通過 mvn 或者 gradle 運(yùn)行
在 CI 中執(zhí)行
不論什么方式,單元測試都應(yīng)該很容易就能運(yùn)行,并給出一個(gè)測試結(jié)果。當(dāng)然,單元測試運(yùn)行速度得快,一般是在秒級(jí)的,太慢的話就不能及時(shí)獲得反饋了。
為什么要寫單元測試?
單元測試的好處
確保代碼滿足需求或者設(shè)計(jì)規(guī)格。 使用單元測試來測試代碼,可以通過構(gòu)造數(shù)據(jù)和前置條件,確保測試覆蓋到需要測試的邏輯。而手工測試或 UI 測試則無法做到,并且往往更復(fù)雜。
快速定位并解決問題。 單元測試因?yàn)闇y試范圍比較小,可以比較容易的定位到問題;而手工測試,常常需要耗費(fèi)不少時(shí)間去定位問題。
確保代碼永遠(yuǎn)滿足需求規(guī)格。 一旦需要對(duì)實(shí)現(xiàn)進(jìn)行修改,單元測試可以確保代碼的正確性,極大的降低各種修改和重構(gòu)的風(fēng)險(xiǎn)。特別是避免那些在意想不到之處出現(xiàn)的 BUG。
簡化系統(tǒng)集成。 單元測試確保了系統(tǒng)或模塊本身的正確性,集成時(shí)更不容易出錯(cuò)。
提高代碼質(zhì)量和可維護(hù)性。 不可測試的代碼,其本身的抽象性、模塊性、可維護(hù)性是有些問題的。例如不符合單一職責(zé)、接口隔離等設(shè)計(jì)原則,或者依賴了全局變量。可測試的代碼,往往其質(zhì)量相對(duì)會(huì)高一些。
提供文檔和說明。 單元測試本身就是接口使用方法的很好的案例。
持續(xù)集成和持續(xù)交付
2010 年前后,大部分互聯(lián)網(wǎng)公司的系統(tǒng)部署還是通過手工的方式進(jìn)行的,往往要在半夜上線系統(tǒng)。但是之后持續(xù)集成、持續(xù)交付的理念不斷推廣,部署過程越來越靈活、順暢。而單元測試則是持續(xù)集成和持續(xù)交付里重要的一環(huán)。
持續(xù)集成就是 Continuous Integration(CI),也就是指從開發(fā)上傳代碼、自動(dòng)構(gòu)建和測試、最后反饋結(jié)果的過程。
更進(jìn)一步,如果自動(dòng)構(gòu)建和測試后,會(huì)自動(dòng)發(fā)布到測試環(huán)境或預(yù)發(fā)布環(huán)境,執(zhí)行更多測試(集成測試、自動(dòng)化 UI 測試等),甚至最后直接發(fā)布,那這一過程就是持續(xù)交付(Continuous Delivery,CD)。業(yè)內(nèi)有不少公司,比如亞馬遜、Esty,可以做到每天幾十甚至成百上千次生產(chǎn)環(huán)境部署,就是因?yàn)橛斜容^完善的持續(xù)交付環(huán)境。
CI 已經(jīng)是互聯(lián)網(wǎng)行業(yè)必備標(biāo)準(zhǔn),CD 也在互聯(lián)網(wǎng)行業(yè)有了越來越多的實(shí)踐,但是如果沒有單元測試這一環(huán)節(jié),CI 和 CD 的過程是有缺陷的。
怎么寫單元測試?
JUnit 簡介
基本上每種語言和框架都有不錯(cuò)的單元測試框架和工具,例如 Java 的 JUnit、Scala 的 ScalaTest、Python 的 unittest、JavaScript 的 Jest 等。上面的例子都是基于 JUnit 的,我們下面就簡單介紹下 JUnit。
JUnit 里面每個(gè) @Test 注解的方法,就是一個(gè)測試。@Ignore 可以忽略一個(gè)測試。@Before、@BeforeClass、@After、@AfterClass 可以在測試執(zhí)行前后插入一些通用的操作,比如初始化和資源釋放等等。
除了 assertEquals,JUnit 也支持不少其他的 assert 方法。例如 assertNull、assertArrayEquals、assertThrows、assertTimeout 等。另外也可以用第三方的 assert 庫比如 Spring 的 Assert 或者 AssertJ。
除了可以測試普通的代碼邏輯,JUnit 也可以進(jìn)行異常測試和時(shí)間測試。異常測試是測試某段代碼必須拋指定的異常,時(shí)間測試則是測試代碼執(zhí)行時(shí)間在一定范圍內(nèi)。
也可以對(duì)測試進(jìn)行分組。例如可以分成 contractTest 、mockTest 和 unitTest,通過參數(shù)指定執(zhí)行某個(gè)分組的測試。
這里就不做過多介紹了,想了解更多 JUnit 的可以去看 極客學(xué)院的 JUnit 教程 等資料。其他的單元測試框架,基本功能都是大同小異。
使用測試 Double
狹義的單元測試,我們是只測試單元本身。即使我們寫的是廣義的單元測試,它依然可能依賴其他模塊,比如其他類的方法、第三方服務(wù)調(diào)用或者數(shù)據(jù)庫查詢等等,造成我們無法很方便的測試被測系統(tǒng)或模塊。這時(shí)我們就需要使用測試 Double 了。
如果細(xì)究的話,測試 Double 分成好多種,比如什么 Dummies、Fakes 等等。但我認(rèn)為我們只要弄清兩類就可以了,也就是 Stub 和 Mock。
Stub
Stub 指那些包含了預(yù)定義好的數(shù)據(jù)并且在測試時(shí)返回給調(diào)用者的對(duì)象。Stub 常被用于我們不希望返回真實(shí)數(shù)據(jù)或者造成其他副作用的場景。
我們契約測試生成的、可以通過 spring cloud stubrunner 運(yùn)行的 Stub Jar 就是一個(gè) Stub。我們可以讓 Stub 返回預(yù)設(shè)好的假數(shù)據(jù),然后在單元測試?yán)锞涂梢砸蕾囘@些數(shù)據(jù),對(duì)代碼進(jìn)行測試。例如,我們可以讓用戶查詢 Stub 根據(jù)參數(shù)里的用戶 ID 返回認(rèn)證用戶和未認(rèn)證用戶,然后我們就可以測試調(diào)用方在這兩種情況下的處理邏輯了。
當(dāng)然,Stub 也可以不是遠(yuǎn)程服務(wù),而是另外一個(gè)類。所以我們經(jīng)常說要針對(duì)接口編程,因?yàn)檫@樣我們就可以很容易的創(chuàng)建一個(gè)接口的 Stub 實(shí)現(xiàn),從而替換具體的類。
public class StubNameService implement NameService {
public String get(String userId) {
return ““Mock user name””;
}
}
public class UserServiceTest {
// UserService 依賴 NameService,會(huì)調(diào)用其 get 方法
@Inject
private UserService userService;
@Test
public void whenUserIdIsProvided_thenRetrievedNameIsCorrect() {
userService.setNameService(new StubNameService());
String testName = userService.getUserName(““SomeId””);
Assert.assertEquals(““Mock user name””, testName);
}
}
不過這樣要實(shí)現(xiàn)很多 Stub 也是很麻煩的,現(xiàn)在我們已經(jīng)不需要自己創(chuàng)建 Stub 了,因?yàn)橛辛烁鞣N Mock 工具。
Mock
Mocks 指那些可以記錄它們的調(diào)用信息的對(duì)象,在測試斷言中我們可以驗(yàn)證 Mocks 被進(jìn)行了符合期望的調(diào)用。
Mock 和 Stub 的區(qū)別在于,Stub 只是提供一些數(shù)據(jù),它并不進(jìn)行驗(yàn)證,或者只是基于狀態(tài)做一些驗(yàn)證;而 Mock 除了可以做 Stub 的事情,也可以基于調(diào)用行為進(jìn)行驗(yàn)證。比如說,Mock 可以驗(yàn)證 Mock 接口被調(diào)用了不多不少正好兩次,并且調(diào)用的參數(shù)是期望的數(shù)值。
Java 里最常用的 Mock 工具就是 Mockito 了。我們來看一個(gè)簡單的例子,下面的 UserService 依賴 NameService。當(dāng)我們測試 UserService 的時(shí)候,我們希望隔離 NameService,那么就可以創(chuàng)建一個(gè) Mock 的 NameService 注入到 UserService 中(在 Spring 里只需要用 @Mock 和 @InjectMocks 兩個(gè)注解就可以完成了)
public class UserServiceTest {
@InjectMocks
private UserService userService;
@Mock
private NameService nameService;
@Test
public void whenUserIdIsProvided_thenRetrievedNameIsCorrect() {
Mockito.when(nameService.getUserName(““SomeId””)).thenReturn(““Mock user name””);
String testName = userService.getUserName(““SomeId””);
Assert.assertEquals(““Mock user name””, testName);
Mockito.verify(nameService).getUserName(““SomeId””);
}
}
注意上面最后一行,是驗(yàn)證 nameService 的 getUserName 被調(diào)用,并且參數(shù)為 ““SomeId””。
契約測試
契約測試會(huì)給每個(gè)服務(wù)生成一個(gè) Stub,可以用于調(diào)用方的單元/集成測試。例如,我們需要測試預(yù)約服務(wù)的預(yù)約操作,而預(yù)約操作會(huì)調(diào)用用戶服務(wù),去驗(yàn)證用戶的一些基本信息,比如醫(yī)生是否認(rèn)證等。
所以,我們可以通過傳入不同的用戶 ID,讓契約 Stub 返回不同狀態(tài)的用戶數(shù)據(jù),從而驗(yàn)證不同的處理流程。例如,正常的預(yù)約流程的測試用例可能是這樣的。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest@AutoConfigureStubRunner(repositoryRoot=““http://<nexus_root>””,
ids = {"“com.xingren.service:user-client-stubs:1.0.0:stubs:6565"”})public class BookingTest {
// BookingService 會(huì)調(diào)用用戶服務(wù),獲取醫(yī)生認(rèn)證狀態(tài)后進(jìn)行不同的處理
@Inject
private BookingService bookingService;
@Test
public void testBooking() {
BookingForm form = new BookingForm(
1,// doctorId
1// scheduleId
1001);// patientId
BookVO res = bookingService.book(form);
assertTrue(res.id > 0);
assertTrue(res.payStatus == PayStatus.UN_PAY);
}
}
注意上面的 AutoConfigureStubRunner 注解就是設(shè)置并啟動(dòng)了用戶服務(wù) Stub,當(dāng)然在測試的時(shí)候,我們需要把服務(wù)調(diào)用接口的 baseUrl 設(shè)置為http://localhost:6565。關(guān)于契約測試的更多內(nèi)容,請(qǐng)參考微服務(wù)環(huán)境下的集成測試探索一文。
TDD
簡單說下 Test Driven Development,也就是 TDD。左耳朵耗子就寫了一篇TDD并不是看上去的那么美,我就直接引用其介紹了。
其開發(fā)過程是從功能需求的test case開始,先添加一個(gè)test case,然后運(yùn)行所有的test case看看有沒有問題,再實(shí)現(xiàn)test case所要測試的功能,然后再運(yùn)行test case,查看是否有case失敗,然后重構(gòu)代碼,再重復(fù)以上步驟。
其實(shí)嚴(yán)格的 TDD 流程實(shí)用性并不高,左耳朵耗子本身也是持批判態(tài)度。但是對(duì)于接口定義比較明確的模塊,先寫單元測試再寫實(shí)現(xiàn)代碼還是有很大好處的。因?yàn)槟繕?biāo)清晰,而且可以立刻得到反饋。
c
文章來源:網(wǎng)絡(luò) 版權(quán)歸原作者所有文章來源:http://www.zghlxwxcb.cn/news/detail-660045.html
上文內(nèi)容不用于商業(yè)目的,如涉及知識(shí)產(chǎn)權(quán)問題,請(qǐng)權(quán)利人聯(lián)系小編,我們將立即處理文章來源地址http://www.zghlxwxcb.cn/news/detail-660045.html
到了這里,關(guān)于軟件測試技術(shù)之單元測試—工程師 Style 的測試方法的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!