Testable Code란
쉽게 테스트할 수 있는 코드를 의미한다.
이는 코드가 독립적이고 예측 가능하게 동작하여 단위 테스트, 통합 테스트 등을 쉽게 수행할 수 있도록 작성된 코드를 가리킨다.
Testable Code는 소프트웨어의 품질을 높이고 유지 보수성을 향상시키기 때문에 소프트웨어 개발에서 매우 중요하다.
이 내용을 이해하기 위해서는 먼저 테스트하기 어려운 코드가 무엇인지 알아보자.
Index
1. 테스트하기 어려운 코드
1-1. 제어할 수 없는 값에 의존하는 경우
1-2. 외부에 영향을 주는 경우
2. 테스트하기 좋은 코드로 개선
2-1. 제어할 수 없는 값에 의존하는 경우
2-2. 외부에 영향을 주는 경우
3. 개인 소감
1. 테스트하기 어려운 코드
테스트 작성이 어려운 이유는 구현부 코드가 테스트에 적합하지 않게 설계되었기 때문이다.
테스트에 용이하게 설계된 경우, Mock 없이도 쉽게 테스트할 수 있다.
테스트가 어렵다고 느끼는 경우는 주로 멱등성이 보장되지 않는 순수 함수가 아닐 때 발생한다.
즉, 코드를 여러 번 실행해도 항상 같은 결과가 나오지 않으면 테스트하기 어렵게 느껴진다.
구체적인 예로 두 가지 경우가 있다.
1-1. 제어할 수 없는 값에 의존하는 경우
- 실행 시 마다 반환값이 다른 경우 (ex. Math.random(), new Date())
- 사용자 입력에 의존하는 경우(ex. readLine)
- 외부 SDK에 의존하는 경우 (ex. PG사 라이브러리)
class Car {
...
go() {
if (랜덤값뽑기() > 3) {
앞으로전진();
}
}
}
예시 코드에서는 랜덤값을 반환하는 함수 때문에 테스트가 어렵다.
제어할 수 없는 값으로 인해 매번 동일한 결과를 보장하지 않기 때문에 멱등성이 보장되는 순수 함수가 아니다.
1-2. 외부에 영향을 주는 경우
- 출력(ex. console.log)
- 외부 메시지 발송(ex. 이메일 발송, 메세지 큐)
- 데이터베이스에 의존하는 경우
- 외부 API/cookie/localStorage를 사용하는 경우
예시
class Order {
...
async cancel() {
const cancelOrder = new Order();
cancelOrder.amount = this.amount;
cancelOrder.description = this.description;
await DB에저장(cancelOrder);
}
}
예시 코드에서 데이터베이스에 의존하는 함수는 테스트를 어렵게 만든다. 이유는 다음과 같다:
- DB 스키마가 존재해야 하고 테스트 환경 설정이 필요함
- 테스트 이후 DB를 초기화해야 함
- 테스트 속도가 느려짐
이 중 외부 환경 구축과 속도 저하가 가장 큰 문제다.
2. 테스트하기 좋은 코드로 개선
테스트하기 좋은 코드로 개선하려면 멱등성이 보장되는 순수 함수로 만들어야 한다.
이를 위해 제어 불가능한 값이나 외부 의존성을 없애거나, 불가능할 경우 적절한 위치로 이동시켜야 한다.
controller - service - repository- domain
내부에 테스트하기 어려운 코드가 포함되면 전체적으로 테스트가 어려워진다.
테스트하기 어려운 코드는 바깥쪽의 controller 단에 위치시키는 것이 최적이다.
이를 통해 내부 로직들이 멱등성을 보장할 수 있도록 하고, 테스트 용이성을 높일 수 있다.
2-1. 제어할 수 없는 값에 의존하는 경우
// 테스트하기 어려운 코드
class Car {
/* ... */
go() {
if (랜덤값뽑기() > 3) {
앞으로전진();
}
}
}
// 테스트하기 좋은 코드
class Car {
/* ... */
go(randomNumber) {
if (randomNumber > 3) {
앞으로전진();
}
}
}
랜덤 값을 생성하는 로직을 go 메서드 내부가 아닌 인자로 전달받는 형태로 수정하면,
랜덤 생성 로직을 클래스 밖으로 분리할 수 있다.
// 이와 같은 형태로 사용할 수 있다.
const car = new Car();
car.go(랜덤값뽑기());
// 이와 같은 형태로 테스트할 수 있다.
car.go(4이상의수)
expect(/* car의 주행거리 */).toBe(/* 1보 전진됨 */)
이렇게 수정하면 테스트 시 랜덤 숫자 대신 지정한 숫자를 전달하여 쉽게 테스트할 수 있다.
2-2. 외부에 영향을 주는 경우
// 테스트하기 어려운 코드
class Order {
/* ... */
async cancel() {
const cancelOrder = new Order();
cancelOrder.amount = this.amount;
cancelOrder.description = this.description;
await DB에저장(cancelOrder);
}
}
// 테스트하기 좋은 코드
class Order {
/* ... */
cancel() {
const cancelOrder = new Order();
cancelOrder.amount = this.amount;
cancelOrder.description = this.description;
return cancelOrder;
}
}
DB 의존 로직을 제거하고 객체만 반환하도록 수정하면,
외부 의존(DB) 로직은 도메인 계층이 아닌 서비스 계층에 위치하게 된다.
// 수정 이전의 Service 로직
class OrderService {
/* ... */
async cancel(orderId:number) {
const order = await orderRepository.findById(orderId);
await order.cancel(); // ⚠️ Order와 OrderService 모두 데이터베이스에 의존
}
}
// 수정 이후의 Service 로직
class OrderService {
/* ... */
async cancel(orderId:number) {
const order = await orderRepository.findById(orderId);
const cancelOrder = order.cancel(); // 👍🏻 Order는 데이터베이스에 의존하지 않음
await DB에저장(cancelOrder);
}
}
수정 전에는 도메인 계층과 Service 계층 모두 데이터베이스에 의존했으나,
수정 후에는 Service 계층에서만 데이터베이스에 의존하게 되어 테스트가 어려운 범위가 축소되었다.
async, await가 포함된 로직은 외부에 의존하는 로직이다.
위 로직이 있다면 최대한 바깥쪽으로 빼낼 수 있는지 고민해야 한다.
3. 개인 소감
테스트 코드에 대해서도 클린한 코드를 만드는 것에 대한 필요성을 알게 되었다.
테스트하기 용이하게 만들려면 최대한 모듈화시켜서 기능마다 의존하지 않고 독립적으로 실행되도록 하는 것이 중요한 것 같다.
이미 구현부부터 복잡하게 만들어진 코드에 대한 테스트 코드를 작성하기는 쉽지 않겠지만 개발 초기 단계에 기능 위주로 작게 쪼개어 하나씩 테스트 해보면 좋을 것 같다.
그리고 테스트하기 쉽게 만들어진 코드는 모듈화되어 있어 하나의 기능 수정이 다른 기능에 영향을 미치지 않기에
유지 보수에 있어서도 도움이 될 것 같다.
https://devhanyoung.tistory.com/7
Testable Code
유닛 테스트를 작성하다 보면, 내 코드가 테스트 불가능한(혹은 어려운) 경우가 있다. 이럴 때면 당혹감과 함께 여러가지 의문이 밀려든다. '왜 내 코드는 테스트하기 어려울까?' '어떻게 수정해
devhanyoung.tistory.com
'Computer Science > Method' 카테고리의 다른 글
레이어드 아키텍처 (0) | 2024.12.02 |
---|---|
클린 아키텍처 (2) | 2024.11.27 |
TDD (Test-Driven Development)란? (1) | 2024.11.08 |