自動化測試範本
Prompt 目標
指導 AI 建立完整的自動化測試框架,包含各層級的自動化測試實作。
角色設定
你是一位資深自動化測試工程師,具備豐富的測試框架設計和實作經驗,熟悉各種自動化測試工具和最佳實務。
任務描述
請協助我為 {專案名稱} 建立完整的自動化測試框架和測試案例。
專案自動化背景
- 專案名稱: {填入專案名稱}
- 應用類型: {填入應用類型,如:Web應用、API服務、微服務}
- 技術棧: {填入技術棧,如:Spring Boot + React、.NET Core + Angular}
- 測試目標: {填入自動化測試目標}
- 現有工具: {填入現有的測試工具和框架}
自動化測試要求
請按照以下結構建立自動化測試:
1. 測試框架設計
- 框架架構設計
- 工具選型評估
- 專案結構規劃
- 配置管理設計
2. 單元測試自動化
- 測試類別設計
- Mock 策略規劃
- 測試資料準備
- 斷言策略設計
3. 整合測試自動化
- API 測試框架
- 資料庫測試設計
- 外部服務模擬
- 契約測試實作
4. UI 測試自動化
- Page Object 模式
- 元素定位策略
- 測試資料驅動
- 跨瀏覽器測試
5. CI/CD 整合
- 測試執行策略
- 報告生成機制
- 失敗處理流程
- 測試結果分析
6. 維護和擴展
- 測試程式碼品質
- 框架擴展性設計
- 效能最佳化
- 文檔和培訓
輸出格式
# {專案名稱} 自動化測試框架
## 1. 框架架構設計
### 1.1 整體架構圖測試執行層 ├── UI Tests (Selenium/Playwright) ├── API Tests (REST Assured/Postman) └── Unit Tests (JUnit/TestNG) | 測試工具層 ├── 測試資料管理 ├── 測試環境配置 └── 測試報告生成 | 基礎設施層 ├── CI/CD 整合 (Jenkins/GitHub Actions) ├── 測試環境管理 (Docker/K8s) └── 測試資料庫 (TestContainers)
### 1.2 技術選型
#### 單元測試框架
**選擇:** JUnit 5
**理由:**
- 現代化的 Java 測試框架
- 豐富的擴展機制
- 良好的 IDE 支援
- 活躍的社群維護
**相依套件:**
```xml
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.6.1</version>
<scope>test</scope>
</dependency>API 測試框架
選擇: REST Assured 理由:
- 直觀的 DSL 語法
- 豐富的驗證功能
- JSON/XML 解析支援
- 與 JUnit 良好整合
UI 測試框架
選擇: Selenium WebDriver + Page Object Model 理由:
- 業界標準的 Web 自動化工具
- 多瀏覽器支援
- 成熟的生態系統
- 豐富的第三方工具
1.3 專案結構
src/test/java/
├── unit/ # 單元測試
│ ├── service/
│ ├── repository/
│ └── util/
├── integration/ # 整合測試
│ ├── api/
│ ├── database/
│ └── external/
├── e2e/ # 端對端測試
│ ├── pages/ # Page Object
│ ├── flows/ # 業務流程
│ └── scenarios/ # 測試場景
├── common/ # 共用組件
│ ├── base/ # 基礎類別
│ ├── config/ # 配置管理
│ ├── data/ # 測試資料
│ └── utils/ # 工具類別
└── resources/
├── config/ # 環境配置
├── testdata/ # 測試資料檔
└── reports/ # 報告模板2. 單元測試實作
2.1 測試基礎類別
測試基礎配置
@ExtendWith(MockitoExtension.class)
abstract class BaseUnitTest {
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@AfterEach
void tearDown() {
// 清理資源
}
}測試工具類別
public class TestDataBuilder {
public static User createValidUser() {
return User.builder()
.id(UUID.randomUUID().toString())
.username("testuser")
.email("test@example.com")
.firstName("Test")
.lastName("User")
.build();
}
public static Product createValidProduct() {
return Product.builder()
.id(UUID.randomUUID().toString())
.name("Test Product")
.price(BigDecimal.valueOf(99.99))
.category("Electronics")
.build();
}
}2.2 Service 層測試範例
@ExtendWith(MockitoExtension.class)
class UserServiceTest extends BaseUnitTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Test
@DisplayName("應該成功建立新使用者")
void shouldCreateUserSuccessfully() {
// Given
CreateUserRequest request = CreateUserRequest.builder()
.username("newuser")
.email("newuser@example.com")
.firstName("New")
.lastName("User")
.build();
User expectedUser = TestDataBuilder.createValidUser();
when(userRepository.save(any(User.class))).thenReturn(expectedUser);
// When
User actualUser = userService.createUser(request);
// Then
assertThat(actualUser).isNotNull();
assertThat(actualUser.getUsername()).isEqualTo(request.getUsername());
assertThat(actualUser.getEmail()).isEqualTo(request.getEmail());
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail(expectedUser);
}
@Test
@DisplayName("當使用者名稱已存在時應該拋出例外")
void shouldThrowExceptionWhenUsernameExists() {
// Given
CreateUserRequest request = CreateUserRequest.builder()
.username("existinguser")
.email("test@example.com")
.build();
when(userRepository.existsByUsername("existinguser")).thenReturn(true);
// When & Then
assertThatThrownBy(() -> userService.createUser(request))
.isInstanceOf(DuplicateUsernameException.class)
.hasMessage("Username already exists: existinguser");
verify(userRepository, never()).save(any(User.class));
}
}3. API 測試實作
3.1 API 測試基礎設定
@TestMethodOrder(OrderAnnotation.class)
public abstract class BaseApiTest {
protected static final String BASE_URL = "http://localhost:8080/api/v1";
protected static final String CONTENT_TYPE = "application/json";
@BeforeAll
static void setUpClass() {
RestAssured.baseURI = BASE_URL;
RestAssured.defaultParser = Parser.JSON;
}
@BeforeEach
void setUp() {
// 設定測試資料
}
protected String getAuthToken() {
return given()
.contentType(CONTENT_TYPE)
.body("""
{
"username": "admin@example.com",
"password": "password123"
}
""")
.when()
.post("/auth/login")
.then()
.statusCode(200)
.extract()
.path("data.accessToken");
}
}3.2 使用者 API 測試
class UserApiTest extends BaseApiTest {
private String authToken;
@BeforeEach
void setUp() {
super.setUp();
authToken = getAuthToken();
}
@Test
@Order(1)
@DisplayName("應該成功取得使用者清單")
void shouldGetUsersSuccessfully() {
given()
.header("Authorization", "Bearer " + authToken)
.queryParam("page", 1)
.queryParam("limit", 10)
.when()
.get("/users")
.then()
.statusCode(200)
.body("success", equalTo(true))
.body("data", hasSize(greaterThan(0)))
.body("meta.pagination.page", equalTo(1))
.body("meta.pagination.limit", equalTo(10));
}
@Test
@Order(2)
@DisplayName("應該成功建立新使用者")
void shouldCreateUserSuccessfully() {
String requestBody = """
{
"username": "testuser_%s",
"email": "testuser_%s@example.com",
"firstName": "Test",
"lastName": "User",
"role": "user"
}
""".formatted(
System.currentTimeMillis(),
System.currentTimeMillis()
);
given()
.header("Authorization", "Bearer " + authToken)
.contentType(CONTENT_TYPE)
.body(requestBody)
.when()
.post("/users")
.then()
.statusCode(201)
.body("success", equalTo(true))
.body("data.username", notNullValue())
.body("data.email", notNullValue())
.body("data.id", notNullValue());
}
@Test
@Order(3)
@DisplayName("當請求資料無效時應該返回 400")
void shouldReturn400WhenRequestIsInvalid() {
String invalidRequest = """
{
"username": "",
"email": "invalid-email"
}
""";
given()
.header("Authorization", "Bearer " + authToken)
.contentType(CONTENT_TYPE)
.body(invalidRequest)
.when()
.post("/users")
.then()
.statusCode(400)
.body("success", equalTo(false))
.body("error.code", equalTo("VALIDATION_ERROR"));
}
}4. UI 測試實作
4.1 Page Object 模式實作
基礎 Page 類別
public abstract class BasePage {
protected WebDriver driver;
protected WebDriverWait wait;
public BasePage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
PageFactory.initElements(driver, this);
}
protected void waitForElement(WebElement element) {
wait.until(ExpectedConditions.visibilityOf(element));
}
protected void clickElement(WebElement element) {
waitForElement(element);
element.click();
}
protected void enterText(WebElement element, String text) {
waitForElement(element);
element.clear();
element.sendKeys(text);
}
}登入頁面 Page Object
public class LoginPage extends BasePage {
@FindBy(id = "username")
private WebElement usernameField;
@FindBy(id = "password")
private WebElement passwordField;
@FindBy(css = "button[type='submit']")
private WebElement loginButton;
@FindBy(css = ".error-message")
private WebElement errorMessage;
public LoginPage(WebDriver driver) {
super(driver);
}
public LoginPage enterUsername(String username) {
enterText(usernameField, username);
return this;
}
public LoginPage enterPassword(String password) {
enterText(passwordField, password);
return this;
}
public DashboardPage clickLogin() {
clickElement(loginButton);
return new DashboardPage(driver);
}
public String getErrorMessage() {
waitForElement(errorMessage);
return errorMessage.getText();
}
public boolean isDisplayed() {
return usernameField.isDisplayed() && passwordField.isDisplayed();
}
}4.2 UI 測試基礎設定
public abstract class BaseUITest {
protected WebDriver driver;
protected String baseUrl;
@BeforeEach
void setUp() {
baseUrl = System.getProperty("base.url", "http://localhost:3000");
driver = createWebDriver();
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
}
@AfterEach
void tearDown() {
if (driver != null) {
driver.quit();
}
}
private WebDriver createWebDriver() {
String browser = System.getProperty("browser", "chrome");
return switch (browser.toLowerCase()) {
case "firefox" -> {
WebDriverManager.firefoxdriver().setup();
FirefoxOptions options = new FirefoxOptions();
if (Boolean.parseBoolean(System.getProperty("headless", "false"))) {
options.addArguments("--headless");
}
yield new FirefoxDriver(options);
}
case "edge" -> {
WebDriverManager.edgedriver().setup();
yield new EdgeDriver();
}
default -> {
WebDriverManager.chromedriver().setup();
ChromeOptions options = new ChromeOptions();
if (Boolean.parseBoolean(System.getProperty("headless", "false"))) {
options.addArguments("--headless");
}
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
yield new ChromeDriver(options);
}
};
}
}4.3 登入流程測試
class LoginFlowTest extends BaseUITest {
@Test
@DisplayName("應該能夠成功登入系統")
void shouldLoginSuccessfully() {
// Given
driver.get(baseUrl + "/login");
LoginPage loginPage = new LoginPage(driver);
// When
DashboardPage dashboardPage = loginPage
.enterUsername("admin@example.com")
.enterPassword("password123")
.clickLogin();
// Then
assertThat(dashboardPage.isDisplayed()).isTrue();
assertThat(dashboardPage.getWelcomeMessage()).contains("Welcome");
}
@Test
@DisplayName("當登入資訊錯誤時應該顯示錯誤訊息")
void shouldShowErrorMessageForInvalidCredentials() {
// Given
driver.get(baseUrl + "/login");
LoginPage loginPage = new LoginPage(driver);
// When
loginPage
.enterUsername("invalid@example.com")
.enterPassword("wrongpassword")
.clickLogin();
// Then
assertThat(loginPage.getErrorMessage()).isEqualTo("Invalid username or password");
}
}5. CI/CD 整合
5.1 Maven 配置
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<junit.version>5.9.0</junit.version>
<selenium.version>4.11.0</selenium.version>
<rest-assured.version>5.3.1</rest-assured.version>
</properties>
<dependencies>
<!-- 測試相依套件 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>${selenium.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<groups>unit</groups>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<groups>integration</groups>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</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>5.2 GitHub Actions 配置
name: 自動化測試流水線
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 設定 Java 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: 快取 Maven 相依性
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
- name: 執行單元測試
run: mvn test -Dgroups=unit
- name: 產生測試報告
run: mvn jacoco:report
- name: 上傳覆蓋率報告
uses: codecov/codecov-action@v3
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: testpass
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: 設定 Java 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: 執行整合測試
run: mvn verify -Dgroups=integration
env:
DB_URL: jdbc:postgresql://localhost:5432/test
DB_USERNAME: postgres
DB_PASSWORD: testpass
e2e-tests:
runs-on: ubuntu-latest
needs: integration-tests
steps:
- uses: actions/checkout@v3
- name: 設定 Java 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: 建置應用程式
run: mvn package -DskipTests
- name: 啟動應用程式
run: |
java -jar target/*.jar &
sleep 30
- name: 執行 E2E 測試
run: mvn test -Dgroups=e2e -Dheadless=true6. 測試報告和分析
6.1 測試結果報告
Allure 報告整合
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-junit5</artifactId>
<version>2.21.0</version>
<scope>test</scope>
</dependency>測試案例註解
@Epic("使用者管理")
@Feature("使用者註冊")
class UserRegistrationTest {
@Test
@Story("成功註冊新使用者")
@Severity(SeverityLevel.CRITICAL)
@Description("驗證使用者能夠使用有效資料成功註冊新帳號")
void shouldRegisterUserSuccessfully() {
// 測試實作
}
}6.2 測試度量儀表板
關鍵指標追蹤
- 測試執行通過率
- 程式碼覆蓋率趨勢
- 測試執行時間分析
- 失敗測試分類統計
品質門檻設定
quality_gates:
unit_test_coverage: 80%
integration_test_pass_rate: 100%
e2e_test_pass_rate: 95%
critical_bugs: 0
## 自動化測試最佳實務
### 測試金字塔原則
- **70% 單元測試**: 快速、可靠、容易維護
- **20% 整合測試**: 驗證組件間協作
- **10% UI 測試**: 驗證端對端使用者旅程
### 測試設計原則
- **獨立性**: 每個測試都能獨立執行
- **可重複性**: 多次執行得到相同結果
- **快速回饋**: 盡可能縮短測試執行時間
- **明確斷言**: 清楚表達測試預期結果
### 維護策略
- 定期重構測試程式碼
- 移除重複和過時的測試
- 持續更新測試工具和框架
- 建立測試程式碼審查機制
## 品質檢查清單
- [ ] 測試框架架構設計合理
- [ ] 測試分層策略明確
- [ ] Page Object 模式實作正確
- [ ] API 測試覆蓋完整
- [ ] 測試資料管理完善
- [ ] CI/CD 整合順暢
- [ ] 測試報告清楚詳細
- [ ] 錯誤處理機制健全
- [ ] 測試維護成本可控
- [ ] 團隊技能培訓完整
## 使用範例
### 範例執行命令
```bash
# 執行所有單元測試
mvn test -Dgroups=unit
# 執行特定測試類別
mvn test -Dtest=UserServiceTest
# 執行整合測試
mvn verify -Dgroups=integration
# 執行 UI 測試 (無頭模式)
mvn test -Dgroups=e2e -Dheadless=true
# 產生測試報告
mvn allure:report測試結果分析
監控以下關鍵指標:
- 測試通過率應維持在 95% 以上
- 單元測試覆蓋率應達到 80% 以上
- 測試執行時間不應超過 30 分鐘
- 關鍵路徑的 E2E 測試必須通過
注意事項
- 避免過度依賴 UI 測試,優先考慮 API 測試
- 保持測試程式碼的可讀性和可維護性
- 定期檢視和清理失效的測試案例
- 建立穩定的測試環境和測試資料
- 培養團隊的自動化測試技能和意識
- 持續改進測試流程和工具選擇