原文地址:https://alphahinex.github.io/2024/08/11/java-backend-test-automation/
description: "本文介紹了 Java 后端自動(dòng)化測(cè)試相關(guān)概念怀估、工具和示例代碼吮便。"
date: 2024.08.11 10:26
categories:
- Test
tags: [Java, Automation test]
keywords: JUnit, DbUnit, Mockito, JaCoCo, Maven
自動(dòng)化測(cè)試相關(guān)概念
測(cè)試用例
測(cè)試用例是一組輸入愉耙、執(zhí)行條件和預(yù)期結(jié)果的集合跌榔,用于驗(yàn)證軟件系統(tǒng)的正確性象踊。
自動(dòng)化測(cè)試
自動(dòng)化測(cè)試是指使用自動(dòng)化工具或腳本來(lái)執(zhí)行測(cè)試用例对湃,以減少人工測(cè)試的工作量哟沫,提高測(cè)試效率和準(zhǔn)確性鸠珠。
測(cè)試金字塔
測(cè)試金字塔 是一種指導(dǎo)自動(dòng)化測(cè)試策略的框架巍耗,它建議在不同層次上分配不同數(shù)量和類型的測(cè)試,以確保成本效益渐排、減輕團(tuán)隊(duì)負(fù)擔(dān)并提高測(cè)試準(zhǔn)確性炬太。這個(gè)概念最初由 Mike Cohn 提出,主要分為三個(gè)層次:?jiǎn)卧獪y(cè)試(Unit Tests)飞盆、服務(wù)測(cè)試(Service Tests娄琉,也稱為集成測(cè)試)、以及用戶界面測(cè)試(UI Tests)吓歇。越靠近塔底的測(cè)試類型執(zhí)行的速度越快孽水、越穩(wěn)定(不易發(fā)生變化);越靠近塔尖的測(cè)試類型編寫(xiě)成本越高城看、收益越低女气。
人們對(duì)測(cè)試金字塔中測(cè)試的類型有不同的劃分,但是總體的思想是一致的测柠。
單元測(cè)試
單元測(cè)試是金字塔的基礎(chǔ)層炼鞠,它們不依賴外部資源(如數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)等)快速轰胁、獨(dú)立谒主,并且數(shù)量眾多,專注于單個(gè)代碼單元的行為驗(yàn)證赃阀。
集成測(cè)試
集成測(cè)試位于中間層霎肯,測(cè)試不同組件之間的交互,數(shù)量相對(duì)較少。
用戶界面測(cè)試
UI測(cè)試或端到端測(cè)試位于金字塔的頂層观游,覆蓋從用戶角度的完整交互流程搂捧,但數(shù)量最少,因?yàn)樗鼈兂杀靖咔揖S護(hù)難度大懂缕。
Mocking & Stubbing
Mocking(模擬)是指創(chuàng)建一個(gè)模擬對(duì)象來(lái)代替實(shí)際的依賴對(duì)象允跑。這個(gè)模擬對(duì)象會(huì)按照測(cè)試的需要來(lái)行為,通常用于驗(yàn)證被測(cè)試代碼是否按照預(yù)期與依賴項(xiàng)交互搪柑。
Stubbing(存根)與 Mocking 類似聋丝,但更側(cè)重于提供預(yù)定義的返回值或行為,而不是驗(yàn)證交互拌屏。Stub 對(duì)象用于替換實(shí)際的依賴對(duì)象潮针,以便在測(cè)試中控制或預(yù)測(cè)它們的輸出。
在實(shí)際的軟件開(kāi)發(fā)中倚喂,Mocking 和 Stubbing 通常結(jié)合使用每篷,以創(chuàng)建一個(gè)可控的測(cè)試環(huán)境。
區(qū)別
- 目的:Mocking 主要用于驗(yàn)證代碼與依賴項(xiàng)的交互端圈,而 Stubbing 主要用于控制測(cè)試環(huán)境焦读,提供可預(yù)測(cè)的輸出。
- 行為:Mock 可以在測(cè)試中模擬更復(fù)雜的行為舱权,如條件返回或引發(fā)異常矗晃,而 Stub 通常只提供簡(jiǎn)單的固定返回值。
- 驗(yàn)證:Mock 對(duì)象可以在測(cè)試后驗(yàn)證方法是否被正確調(diào)用宴倍,包括調(diào)用次數(shù)和參數(shù)张症,而 Stub 通常不進(jìn)行這種驗(yàn)證。
測(cè)試覆蓋率
測(cè)試覆蓋率鸵贬,反映了測(cè)試用例對(duì)軟件代碼的覆蓋程度俗他,通常以百分比來(lái)表示。
測(cè)試覆蓋率是一種度量標(biāo)準(zhǔn)阔逼,用于衡量測(cè)試是否覆蓋了代碼的各個(gè)部分兆衅,例如語(yǔ)句覆蓋、分支覆蓋嗜浮、條件覆蓋羡亩、路徑覆蓋等。
測(cè)試覆蓋率越高危融,意味著測(cè)試用例覆蓋的代碼越多畏铆,但并不意味著測(cè)試用例的質(zhì)量越高,100% 的測(cè)試覆蓋率也不能保證軟件完全沒(méi)有缺陷吉殃,所以在設(shè)計(jì)測(cè)試用例時(shí)及志,應(yīng)該注重測(cè)試用例的質(zhì)量片排。
測(cè)試驅(qū)動(dòng)開(kāi)發(fā)
測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(Test-Driven Development寨腔,簡(jiǎn)稱TDD)是一種軟件開(kāi)發(fā)流程速侈,其核心理念是先編寫(xiě)測(cè)試用例,再編寫(xiě)能夠通過(guò)這些測(cè)試用例的代碼迫卢。TDD的目的是確保代碼的可測(cè)試性倚搬、可維護(hù)性和質(zhì)量。
自動(dòng)化測(cè)試常用工具
Build Tool
通常情況下乾蛤,構(gòu)建工具(如 Maven每界、Gradle)會(huì)在項(xiàng)目構(gòu)建過(guò)程中自動(dòng)執(zhí)行測(cè)試用例。
以 Maven 為例家卖,可在 https://start.spring.io/ 生成一個(gè) Spring Boot 項(xiàng)目眨层,解壓后可以找到一個(gè) src/test/java/com/example/demo/DemoApplicationTests.java
測(cè)試類:
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DemoApplicationTests {
@Test
void contextLoads() {
}
}
使用 Maven 運(yùn)行測(cè)試用例:
$ mvn test
...
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.example.demo.DemoApplicationTests
...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.843 s -- in com.example.demo.DemoApplicationTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
...
執(zhí)行 mvn package
命令時(shí)也會(huì)自動(dòng)執(zhí)行測(cè)試用例,如果測(cè)試用例失敗上荡,構(gòu)建過(guò)程會(huì)終止趴樱。如果需要跳過(guò)測(cè)試用例,可以使用 -DskipTests
參數(shù):
mvn package -DskipTests
JUnit
JUnit 是一個(gè) Java 編程語(yǔ)言的單元測(cè)試框架酪捡,用于編寫(xiě)和運(yùn)行重復(fù)測(cè)試叁征。JUnit 提供了注解和斷言來(lái)編寫(xiě)測(cè)試用例,可以方便地進(jìn)行測(cè)試驅(qū)動(dòng)開(kāi)發(fā)逛薇。
當(dāng)前 Junit 的主要版本是 JUnit 5捺疼,上一個(gè)主要版本 JUnit 4 的最后發(fā)布版 4.13.2 是 2021 年發(fā)布的。
JUnit5
不同于之前版本的 JUnit永罚,JUnit 5 是由三個(gè)不同的子項(xiàng)目組成的模塊化測(cè)試框架:
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
-
JUnit Platform
負(fù)責(zé)在 JVM 中啟動(dòng)測(cè)試框架啤呼。它定義了 TestEngine API 用來(lái)開(kāi)發(fā)可在其平臺(tái)上運(yùn)行的測(cè)試框架。 -
JUnit Jupiter
包含了對(duì) JUnit 5 新注解的支持呢袱,并提供了一個(gè)能夠運(yùn)行 JUnit 5 測(cè)試用例的TestEngine
實(shí)現(xiàn)官扣。 -
JUnit Vintage
提供了用于運(yùn)行 JUnit 3 和 JUnit 4 的測(cè)試用例的TestEngine
實(shí)現(xiàn)。
JUnit 5 常用注解 | 作用 | JUnit 4 對(duì)應(yīng)注解 |
---|---|---|
@Test |
標(biāo)記一個(gè)方法是測(cè)試方法 | @Test |
@BeforeEach |
在每個(gè)測(cè)試方法之前都執(zhí)行的方法 | @Before |
@AfterEach |
在每個(gè)測(cè)試方法之后都執(zhí)行的方法 | @After |
@BeforeAll |
在所有測(cè)試方法之前執(zhí)行一次的方法产捞,需要 static
|
@BeforeClass |
@AfterAll |
在所有測(cè)試方法之后執(zhí)行一次的方法醇锚,需要 static
|
@AfterClass |
@Disabled |
禁用測(cè)試類或方法 | @Ignore |
更多注解可見(jiàn) 2.1. Annotations 。
JUnit5 基礎(chǔ)注解
package com.example.demo;
import org.junit.jupiter.api.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DemoApplicationTests {
private static Logger LOGGER = LoggerFactory.getLogger(DemoApplicationTests.class);
@BeforeAll
static void setup() {
LOGGER.info("@BeforeAll - executes once before all test methods in this class");
}
@BeforeEach
void init() {
LOGGER.info("@BeforeEach - executes before each test method in this class");
}
@AfterEach
void tearDown() {
LOGGER.info("@AfterEach - executed after each test method.");
}
@AfterAll
static void done() {
LOGGER.info("@AfterAll - executed after all test methods.");
}
@Test
void contextLoads() {
}
@DisplayName("Single test successful")
@Test
void testSingleSuccessTest() {
LOGGER.info("Success");
}
@Test
@Disabled("Not implemented yet")
void testShowSomething() {
}
}
$ mvn test
...
17:34:33.848 [main] INFO com.example.demo.DemoApplicationTests -- @BeforeAll - executes once before all test methods in this class
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.2)
2024-08-08T17:34:34.251+08:00 INFO 35736 --- [demo] [ main] com.example.demo.DemoApplicationTests : Starting DemoApplicationTests using Java 17.0.2 with PID 35736 (started by alphahinex in /Users/alphahinex/Desktop/demo)
2024-08-08T17:34:34.253+08:00 INFO 35736 --- [demo] [ main] com.example.demo.DemoApplicationTests : No active profile set, falling back to 1 default profile: "default"
2024-08-08T17:34:34.957+08:00 INFO 35736 --- [demo] [ main] com.example.demo.DemoApplicationTests : Started DemoApplicationTests in 1.063 seconds (process running for 2.405)
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2024-08-08T17:34:35.904+08:00 INFO 35736 --- [demo] [ main] com.example.demo.DemoApplicationTests : @BeforeEach - executes before each test method in this class
2024-08-08T17:34:35.911+08:00 INFO 35736 --- [demo] [ main] com.example.demo.DemoApplicationTests : @AfterEach - executed after each test method.
2024-08-08T17:34:35.933+08:00 INFO 35736 --- [demo] [ main] com.example.demo.DemoApplicationTests : @BeforeEach - executes before each test method in this class
2024-08-08T17:34:35.934+08:00 INFO 35736 --- [demo] [ main] com.example.demo.DemoApplicationTests : Success
2024-08-08T17:34:35.935+08:00 INFO 35736 --- [demo] [ main] com.example.demo.DemoApplicationTests : @AfterEach - executed after each test method.
2024-08-08T17:34:35.940+08:00 INFO 35736 --- [demo] [ main] com.example.demo.DemoApplicationTests : @AfterAll - executed after all test methods.
[WARNING] Tests run: 3, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 2.521 s -- in com.example.demo.DemoApplicationTests
[INFO]
[INFO] Results:
[INFO]
[WARNING] Tests run: 3, Failures: 0, Errors: 0, Skipped: 1
...
從輸出的日志信息可以看到坯临,@BeforeAll
和 @AfterAll
的日志只打印了一次焊唬,@BeforeEach
和 @AfterEach
的日志在每個(gè)沒(méi) @Disabled
的 @Test
方法執(zhí)行前后都會(huì)打印。
JUnit5 斷言
斷言是測(cè)試用例最重要的組成部分看靠。
斷言可以用來(lái)驗(yàn)證方法的行為是否符合預(yù)期赶促,并在斷言失敗時(shí)使測(cè)試用例失敗,進(jìn)而體現(xiàn)到最終的測(cè)試報(bào)告中挟炬。
可以說(shuō)沒(méi)有斷言的測(cè)試用例沒(méi)有任何意義鸥滨,因?yàn)闇y(cè)試用例始終會(huì)執(zhí)行通過(guò)嗦哆。
JUnit 5 的斷言都包含在 org.junit.jupiter.api.Assertions
類的靜態(tài)方法中,并支持了 Lambda 表達(dá)式等 Java 新特性婿滓,常見(jiàn)的斷言包括:
-
assertTrue
:用于驗(yàn)證條件是否為true
老速。 -
assertFalse
:用于驗(yàn)證條件是否為false
。 -
assertNull
:用于驗(yàn)證對(duì)象是否為null
凸主。 -
assertNotNull
:用于驗(yàn)證對(duì)象是否不為null
橘券。 -
assertEquals
:用于驗(yàn)證兩個(gè)對(duì)象是否相等。 -
assertNotEquals
:用于驗(yàn)證兩個(gè)對(duì)象是否不相等卿吐。 -
assertArrayEquals
:用于驗(yàn)證兩個(gè)數(shù)組是否相等旁舰。 -
assertSame
:用于驗(yàn)證兩個(gè)對(duì)象是否是同一個(gè)對(duì)象。 -
assertNotSame
:用于驗(yàn)證兩個(gè)對(duì)象是否不是同一個(gè)對(duì)象嗡官。 -
assertThrows
:用于驗(yàn)證方法是否拋出了指定的異常箭窜。 -
assertAll
:用于組合多個(gè)斷言,當(dāng)其中一個(gè)斷言失敗時(shí)衍腥,后續(xù)斷言不會(huì)執(zhí)行磺樱。
@Test
void groupAssertions() {
int[] numbers = {0, 1, 2, 3, 4};
assertNotNull(numbers);
assertAll("numbers",
() -> assertEquals(0, numbers[0]),
() -> assertSame(3, numbers[3]),
() -> assertArrayEquals(new int[]{0, 1, 2, 3, 4}, numbers)
);
}
JUnit5 假設(shè)
假設(shè)用來(lái)在測(cè)試方法中定義前提條件,如果假設(shè)不成立紧阔,則測(cè)試方法會(huì)被忽略坊罢。
JUnit 5 的假設(shè)方法包含在 org.junit.jupiter.api.Assumptions
類中,有三類靜態(tài)方法:
-
assumeTrue
:假設(shè)條件為true
擅耽,否則忽略測(cè)試方法活孩。 -
assumeFalse
:假設(shè)條件為false
,否則忽略測(cè)試方法乖仇。 -
assumingThat
:假設(shè)條件為true
憾儒,否則忽略測(cè)試方法。
@Test
void testOnlyOnCiServer() {
assumeTrue("CI".equals(System.getenv("ENV")));
// remainder of test
}
JUnit5 驗(yàn)證異常
JUnit 5 中不再使用之前的 @Test(expected = …)
和 ExpectedException
規(guī)則來(lái)設(shè)定期待拋出的異常乃沙。異常的驗(yàn)證都通過(guò) Assertions.assertThrows(…)
方法實(shí)現(xiàn):
@Test
void shouldThrowException() {
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
throw new UnsupportedOperationException("Not supported");
});
assertEquals("Not supported", exception.getMessage());
}
@Test
void assertThrowsException() {
String str = null;
assertThrows(IllegalArgumentException.class, () -> {
Integer.valueOf(str);
});
}
DbUnit
DbUnit 是一個(gè) JUnit 4 的擴(kuò)展起趾,可以在測(cè)試過(guò)程中基于 XML 數(shù)據(jù)集管控測(cè)試數(shù)據(jù)庫(kù)中數(shù)據(jù)狀態(tài),最后的發(fā)布版本是 2024年06月02日 的 v2.8.0警儒。
基本思路是繼承 DBTestCase
基類后训裆,通過(guò)實(shí)現(xiàn) getDataSet()
方法,將準(zhǔn)備的 XML 格式數(shù)據(jù)文件加載到測(cè)試庫(kù)中蜀铲,之后通過(guò) org.dbunit.Assertion
中的斷言進(jìn)行數(shù)據(jù)驗(yàn)證边琉。
如果想在 JUnit 5 中使用 DbUnit,需要在依賴中添加 JUnit 4 和 JUnit Vintage 引擎:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
以下是一個(gè)使用 DbUnit 的示例:
com.example.demo.DataSourceDBUnitTest
:
package com.example.demo;
import org.dbunit.Assertion;
import org.dbunit.DataSourceBasedDBTestCase;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.Test;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class DataSourceDBUnitTest extends DataSourceBasedDBTestCase {
@Override
protected DataSource getDataSource() {
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setURL(
"jdbc:h2:mem:default;MODE=LEGACY;DB_CLOSE_DELAY=-1;init=runscript from 'classpath:dbunit/schema.sql'");
dataSource.setUser("sa");
dataSource.setPassword("sa");
return dataSource;
}
@Override
protected IDataSet getDataSet() throws Exception {
return new FlatXmlDataSetBuilder().build(getClass().getClassLoader()
.getResourceAsStream("dbunit/data.xml"));
}
@Override
protected DatabaseOperation getSetUpOperation() {
return DatabaseOperation.REFRESH;
}
@Override
protected DatabaseOperation getTearDownOperation() {
return DatabaseOperation.DELETE_ALL;
}
@Test
public void testGivenDataSetEmptySchema_whenDataSetCreated_thenTablesAreEqual() throws Exception {
IDataSet expectedDataSet = getDataSet();
ITable expectedTable = expectedDataSet.getTable("CLIENTS");
IDataSet databaseDataSet = getConnection().createDataSet();
ITable actualTable = databaseDataSet.getTable("CLIENTS");
Assertion.assertEquals(expectedTable, actualTable);
}
}
src/test/resources/dbunit/schema.sql
:
CREATE TABLE IF NOT EXISTS CLIENTS
(
`id` int AUTO_INCREMENT NOT NULL,
`first_name` varchar(100) NOT NULL,
`last_name` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE IF NOT EXISTS ITEMS
(
`id` int AUTO_INCREMENT NOT NULL,
`title` varchar(100) NOT NULL,
`produced` date,
`price` float,
PRIMARY KEY (`id`)
);
src/test/resources/dbunit/data.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<CLIENTS id='1' first_name='Charles' last_name='Xavier'/>
<ITEMS id='1' title='Grey T-Shirt' price='17.99' produced='2019-03-20'/>
<ITEMS id='2' title='Fitted Hat' price='29.99' produced='2019-03-21'/>
<ITEMS id='3' title='Backpack' price='54.99' produced='2019-03-22'/>
<ITEMS id='4' title='Earrings' price='14.99' produced='2019-03-23'/>
<ITEMS id='5' title='Socks' price='9.99'/>
</dataset>
更多 DbUnit 用法可參見(jiàn) Introduction to DBUnit 及 Getting Started 等文檔记劝。
個(gè)人感覺(jué) Spring Framework 下的 Spring TestContext Framework 中所提供的 Executing SQL Scripts 方式面向 SQL变姨,相比 XML 更加直觀,且無(wú)需引入三方依賴厌丑,對(duì) JUnit 版本也沒(méi)有限制定欧。
Mockito
Mockito 是 Java 生態(tài)常用的 Mock 框架渔呵,用于創(chuàng)建和配置 Mock 對(duì)象,以及驗(yàn)證測(cè)試中的行為砍鸠。Mockito 會(huì)被 Spring Boot Starter 自動(dòng)依賴扩氢,無(wú)需額外引入。
org.mockito.Mockito
類中常用的靜態(tài)方法包括:
-
mock
:創(chuàng)建一個(gè) Mock 對(duì)象睦番。 -
verify
:驗(yàn)證 Mock 對(duì)象的行為类茂。 -
spy
:創(chuàng)建一個(gè)部分 Mock 的對(duì)象,真實(shí)方法會(huì)被調(diào)用托嚣,但依然可以進(jìn)行驗(yàn)證和 stub。 -
when
:配置 Mock 對(duì)象的行為厚骗。
@Test
void mockAndVerify() {
List<String> mockedList = mock(List.class);
mockedList.add("one");
mockedList.add("two");
mockedList.add("two");
mockedList.add("three");
verify(mockedList).add("three");
verify(mockedList, times(2)).add("two");
verify(mockedList, atLeastOnce()).add("three");
verify(mockedList, atMost(3)).add("one");
}
@Test
void spyAndStub() {
List<String> list = new ArrayList<>();
List<String> spiedList = spy(list);
spiedList.add("one");
spiedList.add("two");
spiedList.add("three");
assertEquals(3, spiedList.size());
when(spiedList.get(0)).thenReturn("first");
assertEquals("first", spiedList.get(0));
assertEquals("two", spiedList.get(1));
}
JaCoCo
JaCoCo 是 Java 的代碼覆蓋率工具示启,可與 Maven 或 Gradle 集成,用于生成代碼覆蓋率報(bào)告领舰。
在 Maven 中使用 JaCoCo 插件夫嗓,只需在 pom.xml
中添加以下配置:
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
執(zhí)行 mvn test
后,JaCoCo 會(huì)生成一個(gè) target/site/jacoco/index.html
的代碼覆蓋率報(bào)告冲秽。
示例代碼
完整示例代碼可見(jiàn):https://github.com/AlphaHinex/java-test-demo