自動化測試範本

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=true

6. 測試報告和分析

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 測試必須通過

注意事項

  1. 避免過度依賴 UI 測試,優先考慮 API 測試
  2. 保持測試程式碼的可讀性和可維護性
  3. 定期檢視和清理失效的測試案例
  4. 建立穩定的測試環境和測試資料
  5. 培養團隊的自動化測試技能和意識
  6. 持續改進測試流程和工具選擇