Skip to content

Commit 5c3a0cf

Browse files
authored
Merge pull request #24 from theintrance/hyunwook/2024-11-13
Hyunwook/2024-11-13
2 parents c26dc21 + b8494a6 commit 5c3a0cf

File tree

3 files changed

+436
-0
lines changed

3 files changed

+436
-0
lines changed
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
---
2+
title: '테스트 더블'
3+
author: 'hyunwok'
4+
---
5+
6+
- 코드의 모든 동작을 완벽하게 테스트하기 위해 종종 입력을 설정하고 Side Effect를 검증해야 하지만, 테스트에서 의존성을 실제로 사용하는 것이 항상 가능하거나 바람직한 것만은 아니다.
7+
- 의존성을 실제로 사용하는 것에 대안으로 테스트 더블을 사용하여 테스트에 더 적합하게 사용할 수 있도록 만들어진다.
8+
- 세 가지 유형의 테스트 더블, 즉 목, 스텁, 페이크에 대해 살펴본다.
9+
10+
### 10.4.1 테스트 더블을 사용하는 이유
11+
12+
#### 테스트 단순화
13+
- 의존성 자체에서 많은 매개변수와 하위 의존성을 설정해야 할 수도 있다.
14+
- 설정 외에도 하위 의존성에서 원하는 Side Effect가 발생했는지 검증해야 할 수도 있다.
15+
- 설정을 위한 코드가 많아지고, 수많은 구현 세부 정보와 밀접하게 연결될 수도 있다.
16+
```txt incorrect
17+
┌───────────────┐ ┌─────────────────┐
18+
│ 테스트 코드 │ ───▶ │ 테스트 대상 코드 │
19+
└───────────────┘ └─────────────────┘
20+
│ │
21+
│ ▼
22+
│ ┌────────────────┐
23+
├─────────────────▶ │ 의존성 │
24+
│ └────────────────┘
25+
│ │
26+
│ ┌────────────────────────────┐
27+
│ ▼ ▼
28+
│ ┌────────────────┐ ┌────────────────┐
29+
├─────────────────▶ │ 하위 의존성 1 │ │ 하위 의존성 2 │
30+
│ └────────┬───────┘ └────────┬───────┘
31+
│ │ │
32+
│ ┌─────────────┴────────────┐ ┌─────────┴─────────┐
33+
│ ▼ ▼ ▼ ▼
34+
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐
35+
└───▶ │ 더 하위 1-1 │ │ 더 하위 1-2 │ │ 더 하위 2-1 │
36+
└────────────────┘ └────────────────┘ └────────────────┘
37+
```
38+
39+
- 테스트 더블을 사용하면 테스트를 더 빠르게 실행할 수 있고, 더 간단해진다
40+
41+
```txt correct
42+
┌───────────────┐ ┌─────────────────┐
43+
│ 테스트 코드 │ ───▶ │ 테스트 대상 코드 │
44+
└───────────────┘ └─────────────────┘
45+
│ │
46+
│ ▼
47+
│ ┌────────────────┐
48+
└─────────────────▶ │ Dependency의 │
49+
│ 테스트 더블 │
50+
└────────────────┘
51+
```
52+
53+
#### 테스트로부터 외부 세계 보호
54+
- 결제를 처리하는 시스템을 사용하고 고객의 은행게좌에서 돈을 인출하는 코드를 테스트한다고 가정해 보자.
55+
- 코드가 실제로 실행되면 실제 계좌에서 돈이 실제로 인출될 것이고, 이것은 큰 문제를 일으킬 수 있기에 절대 이렇게 수행되면 안 된다.
56+
57+
```txt incorrect
58+
┌───────────────┐
59+
│ 테스트 코드 │ ───────────────┐
60+
└───────────────┘ │
61+
│ │
62+
▼ ▼
63+
┌────────────────┐ ┌────────────────┐ ┌─────────────────────┐
64+
│ 테스트 대상 코드 │ ────▶ │ BankAccount │ ────▶ │ 실제 은행의 백엔드 시스템 │
65+
└────────────────┘ └────────────────┘ └─────────────────────┘
66+
```
67+
68+
- 테스트 더블을 사용하면 테스트로부터 외부 세계를 보호할 수 있다.
69+
70+
```txt correct
71+
┌───────────────┐ │
72+
│ 테스트 코드 │ ───────────────┐ │
73+
└───────────────┘ │ │
74+
│ │ │
75+
▼ ▼ │
76+
┌────────────────┐ ┌────────────────┐ │ ┌─────────────────────┐
77+
│ 테스트 대상 코드 │ ────▶ │ BankAccount의 │ │ │ 실제 은행의 백엔드 시스템 │
78+
│ │ │ 테스트 더블 │ │ │ │
79+
└────────────────┘ └────────────────┘ │ └─────────────────────┘
80+
```
81+
82+
- 테스트 값이 실제 서버에 표시된다면 발생할 수 있는 문제들
83+
- 사용자는 이상하고 혼란스러운 값을 볼 수 있다.
84+
- 모니터링 및 로깅에 영향을 미칠 수 있다.
85+
86+
#### 외부로부터 테스트 보호
87+
- 위의 동일한 상태에서 잔액을 가져오는 테스트를 진행해 본다.
88+
- 실제 은행에서 돈을 입금하거나 인출하게 된다면 값이 변경이 되고, 이것은 테스트의 실패로 이어진다.
89+
90+
### 10.4.2 목
91+
- 목은 멤버 함수가 호출될 때 인수에 제공되는 값을 기록하는 것 외에는 어떠한 일도 수행하지 않는다.
92+
- 테스트 대상 코드에서 Side Effect를 일으키는 의존성을 시뮬레이션하는데 가장 유용하다.
93+
```java
94+
class PaymentManager {
95+
...
96+
97+
PaymentResult settleInvoice(
98+
BankAccount customerBankAccount,
99+
Invoice invoice) {
100+
customerBankAccount.debit(invoice.getBalance()); // 계좌로부터 청구서의 잔액만큼 인출, 테스트해야 하는 동작
101+
return PaymentResult.paid(invoice.getId());
102+
}
103+
}
104+
```
105+
106+
- BankAccount 인터페이스 구현
107+
108+
```java incorrect
109+
interface BankAccount {
110+
void debit(MonetaryAmount amount);
111+
void credit(MonetaryAmount amount);
112+
MonetaryAmount getBalance();
113+
}
114+
115+
class BankAccountImpl implements BankAccount {
116+
private final BankingBackend backend; // 실제 계좌와 연결
117+
...
118+
override void debit(MonetaryAmount amount) { ... }
119+
override void credit(MonetaryAmount amount) { ... }
120+
override MonetaryAmount getBalance() { ... }
121+
}
122+
```
123+
124+
- 목을 사용하는 테스트 케이스
125+
126+
```java correct
127+
void testSettleInvoice_accountDebited() {
128+
BankAccount mockAccount = createMock(BankAccount); // 목 객체 생성
129+
MonetaryAmount invoiceBalance = new MonetaryAmount(5.0, Currency.USD);
130+
Invoice invoice = new Invoice(invoiceBalance, "test-id");
131+
PaymentManager paymentManager = new PaymentManager();
132+
133+
paymentManager.settleInvoice(mockAccount, invoice);
134+
135+
verifyThat(mockAccount.debit)
136+
.wasCalledOnce()
137+
.withArguments(invoiceBalance);
138+
}
139+
```
140+
141+
- 테스트로부터 외부 세계를 보호하는 데에는 성공했지만 테스트가 비현실적이고 중요한 버그를 잡지 못할 위험이 있다.
142+
143+
### 10.4.3 스텁
144+
- 스텁은 함수가 호출되면 미리 정해 놓은 값을 반환함으로써 함수를 시뮬레이션하여 의존성을 시뮬레이션할 수 있다.
145+
- 목과 스텁은 분명한 차이가 있지만, 보통 목을 말할 때는 둘다를 지칭하고, 특정 멤버 함수를 스텁하는 데만 사용하고자 할 때조차 목을 만들어야 한다.
146+
147+
```java
148+
class PaymentManager {
149+
...
150+
151+
PaymentResult settleInvoice(
152+
BankAccount customerBankAccount,
153+
Invoice invoice) {
154+
if (customerBankAccount.getBalance().isLessThan(invoice.getBalance())) {
155+
return PaymentResult.insufficientFunds(invoice.getId());
156+
}
157+
customerBankAccount.debit(invoice.getBalance());
158+
return PaymentResult.paid(invoice.getId());
159+
}
160+
}
161+
```
162+
163+
- 계좌 잔액이 없을 때 추가된 분기 처리에 대한 테스트 케이스
164+
165+
```java correct
166+
void testSettleInvoice_insufficientFundsCorrectResultReturned() {
167+
MonetaryAmount invoiceBalance = new MonetaryAmount(10.0, Currency.USD);
168+
Invoice invoice = new Invoice(invoiceBalance, "test-id");
169+
BankAccount mockAccount = createMock(BankAccount.class); // 스텁만 필요하지만 BankAccount 목 객체 생성
170+
when(mockAccount.getBalance()).thenReturn(new MonetaryAmount(9.99, Currency.USD));
171+
PaymentManager paymentManager = new PaymentManager();
172+
173+
PaymentResult result = paymentManager.settleInvoice(mockAccount, invoice);
174+
175+
assertThat(result.getStatus()).isEqualTo(INSUFFCIENT_FUNDS);
176+
}
177+
```
178+
179+
- 이것 또한 문제가 될 수 있다.
180+
181+
### 10.4.4 목과 스텁은 문제가 될 수 있다.
182+
183+
#### 목과 스텁은 실제적이지 않은 테스트를 만들 수 있다
184+
- 예외를 던질 수 있는 실제와는 다르게 함수의 호출만을 기록하기 때문에, 코드의 버그가 발생하지 않을 수 있다.
185+
- 개발자가 실제 의존성이 어떻게 동작하는지 이해하지 못하면 목을 설정할 때 실수를 할 가능성이 크다.
186+
187+
```java incorrect
188+
void testSettleInvoice_negativeInvoiceBalance() {
189+
BankAccount mockAccount = createMock(BankAccount.class);
190+
MonetaryAmount invoiceBalance = new MonetaryAmount(-5.0, Currency.USD);
191+
Invoice invoice = new Invoice(invoiceBalance, "test-id");
192+
PaymentManager paymentManager = new PaymentManager();
193+
194+
paymentManager.settleInvoice(mockAccount, invoice);
195+
// 동작이 잘 작동한다.
196+
197+
verifyThat(mockAccount.debit)
198+
.wasCalledOnce()
199+
.withArguments(invoiceBalance);
200+
}
201+
```
202+
203+
- 실제 코드의 조건
204+
205+
```java
206+
interface BankAccount {
207+
/**
208+
* @throws ArgumentException 0보다 적은 금액으로 호출되는 경우
209+
*/
210+
void debit(MonetaryAmount amount);
211+
212+
/**
213+
* @throws ArgumentException 0보다 적은 금액으로 호출되는 경우
214+
*/
215+
void credit(MonetaryAmount amount);
216+
217+
...
218+
}
219+
220+
interface BankAccount {
221+
...
222+
223+
/**
224+
* @return 가장 가까운 10의 배수로 반내림한 계좌의 잔액
225+
* 예를 들어 실제 잔액이 19달러라면 이 함수는 10달러를 반환한다.
226+
* 이것은 보안을 위한 것인데 정확한 잔액은 보안 확인을 위한 질문으로
227+
* 은행이 사용하기 때문이다.
228+
*/
229+
MonetaryAmount getBalance();
230+
// 위의 스텁 예제에서 스텁을 구성할 때 주석문에 있는 내용을 간과했다.
231+
}
232+
```
233+
234+
#### 목과 스텁을 사용하면 테스트가 구현 세부 정보에 유착될 수 있다
235+
- 음수의 값이 들어온다면 입금이라는 가정을 추가하여 코드를 작성
236+
```java
237+
PaymentResult settleInvoice(...) {
238+
...
239+
MonetaryAmount balance = invoice.getBalance();
240+
if (balance.isPositive()) {
241+
customerBankAccount.debit(balance);
242+
} else {
243+
customerBankAccount.credit(balance.absoluteAmount());
244+
}
245+
...
246+
}
247+
248+
// 기존 테스트 코드
249+
void testSettleInvoice_positiveInvoiceBalance() {
250+
...
251+
verifyThat(mockAccount.debit)
252+
.wasCalledOnce()
253+
.withArguments(invoiceBalance);
254+
}
255+
256+
...
257+
258+
void testSettleInvoice_negativeInvoiceBalance() {
259+
...
260+
verifyThat(mockAccount.credit)
261+
.wasCalledOnce()
262+
.withArguments(invoiceBalance.absoluteAmount());
263+
}
264+
```
265+
266+
- 음수 여부를 판단하기 위한 if-else문의 반복으로 인해 transfer라는 함수로 리팩토링하고 인터페이스에 추가
267+
268+
```java incorrect
269+
interface BankAccount {
270+
...
271+
/**
272+
* 지정된 금액을 계좌로 송금한다. 금액이 0보다 적으면
273+
* 계좌로부터 인출하는 효과를 갖는다.
274+
*/
275+
void transfer(MonetaryAmount amount);
276+
}
277+
278+
PaymentResult settleInvoice(...) {
279+
...
280+
MonetaryAmount balance = invoice.getBalance();
281+
customerBankAccount.transfer(balance.negate());
282+
...
283+
}
284+
```
285+
286+
- 기존에 테스트는 debit이나 credit 함수가 호출되는지 확인하는 목을 사용하고 있었고, 이 상황에서는 불리지 않기 때문에 테스트는 실패한다.
287+
288+
### 10.4.5 페이크
289+
- 페이크는 클래스의 대체 구현체로 테스트에서 안전하게 사용할 수 있다.
290+
- 외부 시스템과 통신하는 대신 페이크 내의 멤버 변수에 상태를 저장한다.
291+
- 페이크의 요점은 코드 계약이 실제 의존성과 동일하기 때문에 실제 클래스가 특정 입력을 받아들이지 않는다면 페이크도 마찬가지라는 것이다.
292+
- 멤버 변수를 통해 상태를 추적한다.
293+
- 페이크 코드 예시
294+
```java correct
295+
class FakeBankAccount implements BankAccount {
296+
private MonetaryAmount balance;
297+
298+
FakeBankAccount(MonetaryAmount startingBalance) {
299+
this.balance = startingBalance;
300+
}
301+
302+
override void debit(MonetaryAmount amount) {
303+
if (amount.isNegative()) {
304+
throw new ArgumentException("액수는 0보다 적을 수 없음");
305+
}
306+
balance = balance.subtract(amount);
307+
}
308+
309+
override void credit(MonetaryAmount amount) {
310+
if (amount.isNegative()) {
311+
throw new ArgumentException("액수는 0보다 적을 수 없음");
312+
}
313+
balance = balance.add(amount);
314+
}
315+
316+
override void transfer(MonetaryAmount amount) {
317+
balance.add(amount);
318+
}
319+
320+
override MonetaryAmount getBalance() {
321+
return roundDownToNearest10(balance);
322+
}
323+
324+
MonetaryAmount getActualBalance() {
325+
return balance;
326+
}
327+
}
328+
```
329+
330+
#### 페이크로 인해 보다 실질적인 테스트가 이루어질 수 있다
331+
- 위에서 보았던 목을 사용하여 -5의 값을 더하는 코드는 예외를 발생시키지 않지만, 페이크를 사용한다면 예외를 발생시켜 테스트가 실패한다.
332+
333+
#### 페이크를 사용하면 구현 세부 정보로부터 테스트를 분리할 수 있다
334+
- 다른 테스트들과는 또 다른 이점은 테스트가 구현 세부 사항에 밀접하게 결합하는 정도가 덜 하다는 것이다.
335+
- 구현 세부 사항 대신 최종 계정 잔액이 정확한지 확인한다.
336+
- 결과가 같다면 테스트가 통과되기에, 구현 세부 사항과 관련해서 훨씬 더 독립적이다.
337+
- 동작을 변경하지 않는 한 테스트는 실패하지 않는다.
338+
```java correct
339+
assertThat(fakeAccount.getActualBalance())
340+
.isEqualTo(new MonetaryAmount(105.0, Currency.USD));
341+
```
342+
343+
### 10.4.6 목에 대한 의견
344+
- **목 찬성론자**(런던 학파) : 목과 스텁을 사용해서 의존성을 실제로 사용하는 것을 피해야 한다.
345+
- **고전주의자**(디트로이트 학파) : 목과 스텁은 최소한으로 사용되어야 하고 개발자는 테스트에서 의존성을 실제로 사용하는 것을 최우선으로 해야 한다.
346+
347+
- 목 접근법은 시험 대상 코드가 **어떻게** 하는가를 확인하는 반면, 고전주의 접근법은 코드를 실행하는 최종 결과가 **무엇인지** 확인하는 경향이 있다.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
title: '테스트 철학으로부터 신중하게 선택하라'
3+
author: 'hyunwok'
4+
---
5+
6+
- **테스트 주도 개발** : TDD는 실제 코드를 작성하기 전에 테스트 케이스를 먼저 작성하는 것을 지지한다.
7+
- 실제 코드는 테스트만 통과하도록 최소한으로 작성하고 이후에 구조를 개선하고 중복을 없애기 위해 리팩토링을 진행한다.
8+
- **행동 주도 개발** : BDD의 핵심은 사용자, 고객, 비즈니스의 관점에서 소프트웨어가 보여야 할 행동을 식별하는데 집중하는 것이다.
9+
- 이런 원하는 동작은 소프트웨어가 개발될 수 있는 형식으로 포착되고 기록된다.
10+
- 테스트는 소프트웨어 자체의 속성보다는 이러한 원하는 동작을 반영해야 한다.
11+
- **수용 테스트 주도 개발** : ATDD는 BDD와 비슷하지만 소프트웨어가 전체적으로 필요에 따라 작동하는지 검증하기 위해 자동화된 수락 테스트를 만드는 것이 다르다.

0 commit comments

Comments
 (0)