초기 코드
@Test
void calculateBalance_ShouldReturnCorrectBalance() {
AccountId accountId = new AccountId(1L);
// Given
// 1P has a balance of 100
Account account = AccountTestData.defaultAccount()
.withAccountId(accountId)
.withBaselineBalance(Money.of(100L))
.withActivityWindow(new ActivityWindow(
// 2P send 200 money to 1P
ActivityTestData.defaultActivity()
.withOwnerAccountId(new AccountId(2L))
.withSourceAccountId(new AccountId(2L))
.withTargetAccountId(new AccountId(1L))
.withMoney(Money.of(200L))
.build(),
// 1P send 300 money to 3P
ActivityTestData.defaultActivity()
.withOwnerAccountId(new AccountId(1L))
.withSourceAccountId(new AccountId(1L))
.withTargetAccountId(new AccountId(3L))
.withMoney(Money.of(300L))
.build()
)
).build();
// When
Money balance = account.calculateBalance();
// Then
Assertions.assertEquals(Money.ZERO, balance);
}
calculateBalance_ShouldReturnCorrectBalance()라는 테스트 코드를 작성하려 했습니다.
해당 코드는 calculateBalance()가 ActivityWindow에 담긴 Activity들의 연산을 수행하고, 올바른 결과를 반환함을 보장해야 합니다.
[유효성 판단 책임은 다른 클래스에 위임합니다. e.g.) 마이너스 계좌, 음수 단위의 돈 입금 등]
하지만, 코드를 들여다보면 이는 타인을 대상으로 출금하는 활동(outgoing to others activity)만을 테스트합니다. 이는 다음과 같은 경우의 유효성을 보장하진 못합니다.
e.g.) 타인으로부터의 입금, 자신에게 입금, 자신에게 출금 등
생긴 고민은, 이를 모두 단일 테스트로 구현하는 것은 곱 연산의 꼴로 코드를 비대하게 만들고, 결국 Code Bloat를 일으킨다고 생각했습니다. 그래서 절충방안을 찾다가 @ParameterizedTest를 접하게 되었습니다.
해당 애노테이션은 테스트간 사용될 매개변수를 파라미터로 지정하여, 코드를 재사용하며 다양한 상황을 테스트할 수 있었습니다.
수정 코드
@ParameterizedTest
@CsvSource({
"1, 300, 1, 1, 1, 1, 1, 1", // 자신과의 거래 => 변동 X
"1, 600, 2, 2, 1, 3, 3, 1", // 타인의 입금 => 300 + 200 + 100
"1, 0, 1, 1, 2, 1, 1, 3" // 타인에게 출금 => 300 - 200 - 100
})
void calculateBalance_ShouldReturnCorrectBalance(
long testAccountId,
long expectedBalance,
long ownerAccountId1,
long sourceAccountId1,
long targetAccountId1,
long ownerAccountId2,
long sourceAccountId2,
long targetAccountId2) {
// Given
AccountId accountId = new AccountId(testAccountId);
Account account = AccountTestData.defaultAccount()
.withAccountId(accountId)
.withBaselineBalance(Money.of(300L))
.withActivityWindow(new ActivityWindow(
ActivityTestData.defaultActivity()
.withOwnerAccountId(new AccountId(ownerAccountId1))
.withSourceAccountId(new AccountId(sourceAccountId1))
.withTargetAccountId(new AccountId(targetAccountId1))
.withMoney(Money.of(200L))
.build(),
ActivityTestData.defaultActivity()
.withOwnerAccountId(new AccountId(ownerAccountId2))
.withSourceAccountId(new AccountId(sourceAccountId2))
.withTargetAccountId(new AccountId(targetAccountId2))
.withMoney(Money.of(100L))
.build()
)
).build();
// When
Money balance = account.calculateBalance();
// Then
Assertions.assertEquals(Money.of(expectedBalance), balance);
}
(CSV형식 이외에도, @MethodSource를 통해 파라미터를 객체화 할 수 있었습니다. 이는 더 나은 가독성 및 구조화를 보장합니다.)
사용하면서 고민한 점은, 테스트 코드는 (하나의 목적을 가진) 단일 테스트만을 수행함이 권장된다는 점이었습니다.
그래서 ParameterizedTest로 묶을 테스트의 "범주"를 정해야했고, 동시에 내가 작성한 코드가 원칙을 위배하지 않는지 찾아야 했습니다.
분리해야 할 경우
e.g.) 예외 상황 테스트 (Exception Handling), 경계 값 테스트 (Boundary Testing), 다중 계정 테스트 (Multi-Account Transactions), 비정상 입력 검증 테스트 (Invalid Input Testing) 등
제 코드는 owner를 포함한 다수를 대상으로 하기에, 다중 계정 테스트로도 볼 수 있나 궁금했습니다.
결과적으로는, "목적"이 다르기에 포함되지 않는다고 판단했습니다. 목적은 "다양한 상황에 대해 calculateBalance가 정상적인 결과를 반환함"을 보장하는 것이며, 여러 계정이 얽혔을 때의 처리과정 보장(트랜잭션 등)이 아니기 때문입니다.
이는 일반 테스트 혹은 발리데이션 테스트에 가까웠기에, 확장에 주의하며 일단은 이 형식을 유지해보기로 했습니다.
'미니멀 개발일기' 카테고리의 다른 글
Intelij IDEA 백그라운드 화면 바꿈 (0) | 2025.01.08 |
---|---|
local port와 rest-assured port 동기화 (0) | 2025.01.05 |
Test Data Builder Class를 적용하기 전의 고찰 (0) | 2024.12.10 |
Junit5 디펜던시 추가에 4시간을 쓴 경험 (0) | 2024.12.04 |
백준 3407번(맹세) 풀이 - Java 메모리 제한이 심한 문제 (1) | 2024.10.25 |