完成了前面四個模組的開發、重構與協作技巧後,今天我們正式邁入第五模組——「品質把關、自動化與資安實務」。
在現代敏捷開發與持續整合(CI/CD)流程中,單元測試(Unit Test)是確保代碼品質、防止改動引入回歸錯誤(Regression Bug)的最後一道防線。然而,很多工程師在專案裡往往不愛寫測試,原因很簡單:
今天我們就來示範:如何讓 ChatGPT / Copilot 成為你的「自動化 QA 工程師」,只需提供業務方法,就能在幾秒內自動產出符合 AAA 模式、整合 JUnit 5 + Mockito 的高覆蓋率測試案例!
想要讓 AI 產出嚴謹且可直接運行的測試類別,不要只丟一句「幫我寫測試」,請使用以下標準規範模板:
【單元測試提示詞範本】
請扮演資深 Java QA 自動化測試專家。以下是一段等待測試的 Spring Boot / Java 業務代碼:
[貼上既有的 Service 或業務邏輯方法]
請使用 JUnit 5 與 Mockito 框架為此方法產出完整的測試類別,要求如下:
@ExtendWith(MockitoExtension.class),嚴格採用 AAA 模式(Arrange, Act, Assert)標註每個測試區塊。assertThat(...))以提升測試可讀性。Mockito.verify(...) 確認外部相依套件被呼叫的次數與參數正確性。假設我們有一個銀行轉帳的核心業務方法,涉及金額扣款、收款與帳號餘額檢查:
[待測業務代碼]
public class TransferService {
private final AccountRepository accountRepository;
public TransferService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("轉帳金額必須大於 0");
}
if (fromAccountId.equals(toAccountId)) {
throw new IllegalArgumentException("轉出與轉入帳號不可相同");
}
Account fromAccount = accountRepository.findById(fromAccountId)
.orElseThrow(() -> new AccountNotFoundException("找不到轉出帳號"));
Account toAccount = accountRepository.findById(toAccountId)
.orElseThrow(() -> new AccountNotFoundException("找不到轉入帳號"));
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException("帳戶餘額不足");
}
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
}
將上述代碼與模板交給 AI 後,生成的測試類別結構分明、職責單一:
[AI 生成的完整單元測試]
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class TransferServiceTest {
@Mock
private AccountRepository accountRepository;
@InjectMocks
private TransferService transferService;
@Test
@DisplayName("轉帳成功:餘額充足時應正確扣除轉出帳戶並增加轉入帳戶餘額")
void transfer_Success() {
// [Arrange] 準備測試資料與 Mock 行為
Long fromId = 1L;
Long toId = 2L;
BigDecimal amount = new BigDecimal("500");
Account fromAccount = new Account(fromId, new BigDecimal("1000"));
Account toAccount = new Account(toId, new BigDecimal("200"));
when(accountRepository.findById(fromId)).thenReturn(Optional.of(fromAccount));
when(accountRepository.findById(toId)).thenReturn(Optional.of(toAccount));
// [Act] 執行待測方法
transferService.transfer(fromId, toId, amount);
// [Assert] 驗證結果與狀態變更
assertThat(fromAccount.getBalance()).isEqualByComparingTo("500");
assertThat(toAccount.getBalance()).isEqualByComparingTo("700");
// 驗證儲存方法被呼叫了兩次
verify(accountRepository, times(2)).save(any(Account.class));
}
@Test
@DisplayName("轉帳失敗:轉帳金額小於等於 0 時應拋出 IllegalArgumentException")
void transfer_AmountZeroOrNegative_ThrowsException() {
// [Act & Assert]
assertThatThrownBy(() -> transferService.transfer(1L, 2L, BigDecimal.ZERO))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("轉帳金額必須大於 0");
// 驗證未查詢資料庫
verifyNoInteractions(accountRepository);
}
@Test
@DisplayName("轉帳失敗:轉出與轉入帳號相同時應拋出 IllegalArgumentException")
void transfer_SameAccount_ThrowsException() {
// [Act & Assert]
assertThatThrownBy(() -> transferService.transfer(1L, 1L, new BigDecimal("100")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("轉出與轉入帳號不可相同");
verifyNoInteractions(accountRepository);
}
@Test
@DisplayName("轉帳失敗:轉出帳戶餘額不足時應拋出 InsufficientBalanceException")
void transfer_InsufficientBalance_ThrowsException() {
// [Arrange]
Long fromId = 1L;
Long toId = 2L;
Account fromAccount = new Account(fromId, new BigDecimal("100")); // 只有 100
Account toAccount = new Account(toId, new BigDecimal("500"));
when(accountRepository.findById(fromId)).thenReturn(Optional.of(fromAccount));
when(accountRepository.findById(toId)).thenReturn(Optional.of(toAccount));
// [Act & Assert]
assertThatThrownBy(() -> transferService.transfer(fromId, toId, new BigDecimal("500")))
.isInstanceOf(InsufficientBalanceException.class)
.hasMessage("帳戶餘額不足");
// 驗證未觸發任何 save 更新操作
verify(accountRepository, never()).save(any(Account.class));
}
}
when(...).thenReturn(...)、verify(..., times(2)) 這些繁複的語法,由 AI 自動根據邏輯推導完成,工程師只需檢視資料 mock 是否合乎現實。verifyNoInteractions(accountRepository) 或 never().save(...),能精確確認當業務失敗時,系統沒有意外更動到資料庫。今天我們示範了如何透過結構化 Prompt,讓 AI 在幾秒鐘內產出具備 AAA 架構、整合 JUnit 5 與 Mockito 的高品質測試案例,讓原本枯燥耗時的單元測試撰寫變得無痛且高效。