https://snowman-seolmini.tistory.com/88
TDD 예제(2)
두 번째 비동기 통신 예제로 살펴보는 독립성을 유지한 간략한 테스트 코드를 작성해보자.테스트 코드를 작성할 때 어떤 측면을 검증할 지를 정하는 것이 첫 번째다. 1. 함수의 호출 여부 확인
snowman-seolmini.tistory.com
에서 테스트 코드를 작성해봤는데, 좀 더 복잡한 상태의 메서드라면 테스트 코드를 작성하는 일이 즐거운 일이 되지 않을 것 같다....이를 mock을 남발한 테스트 코드라고 한다고 한다.
const ProductService = require('../product_service.js');
const ProductClient = require('../product_client.js');
jest.mock('../product_client.js');
describe('ProductService', () => {
const fetchItems = jest.fn(async () => [
{ item: 'Milk', available: true },
{ item: 'banana', available: false },
]);
ProductClient.mockImplementation(() => {
return {
fetchItems,
};
});
let productService;
beforeEach(() => {
productService = new ProductService();
});
it('should filter out only available items', async () => {
const items = await productService.fetchAvailableItems();
expect(items.length).toBe(1);
expect(items).toEqual([{ item: 'Milk', available: true }]);
});
});
위의 테스트 코드를 좀 stub 방식을 사용한다면 좀 더 간략하게 줄일 수 있다고 한다.
의존성 주입(Dependency Injection) 원칙은 클래스가 자신의 의존성을 직접 생성하기보다는 외부에서 주입받아야 한다. (이를 통해 클래스의 재사용성과 테스트 용이성을 높일 수 있다는데...어렵따. 하다 보면 이해하면서 익숙해지겠지. )
class ProductService {
constructor() {
this.productClient = new ProductClient();
}
async fetchAvailableItems() {
const items = await this.productClient.fetchItems();
return items.filter((item) => item.available);
}
}
여기서 ProductService 클래스는 ProductClient의 인스턴스를 자신의 생성자 내에서 직접 생성하고 있다.
* 코드 분리 : ProductService는 더 이상 ProductClient를 직접 생성하지 않기 때문에, ProductClient의 변경 사항에 영향을 받지 않는다. ProductService는 오직 fetchItems 메서드가 있어야 하는 객체만 필요하다.
* 클래스의 책임 분리 : ProductService는 ProductClient의 생성 책임을 지지 않으며, ProductClient의 객체를 주입받아 사용하는 책임만 가진다. 이는 클래스의 책임을 명확하게 분리하는 데 도움이 된다.
ProductService가 ProductClient에 대한 직접적인 의존성을 가지므로, ProductClient의 변경이 ProductService에 영향을 미칠 수 있다. 즉, 두 클래스 간의 결합도가 강해진다.
개선 방안
- 의존성 주입을 사용한다면 생성자에서 ProductClient 인스턴스를 외부에서 주입받도록 변경할 수 있다.
class ProductService {
constructor(productClient) {
this.productClient = productClient;
}
async fetchAvailableItems() {
const items = await this.productClient.fetchItems();
return items.filter((item) => item.available);
}
}
stub 형식의 의존성 주입을 받는 형태로 이를 " 의존성 주입 패턴(Dependency Injection Pattern) "이라고 부른단다.
이 패턴은 객체 간의 의존성을 외부에서 주입하여 모듈과의 결합도를 낮추고 유연성을 높이는 설계 방식이다.
각설하고 코드로 비교해보자!
# 기존 코드
1. ProductClient:
class ProductClient {
async fetchItems() {
const response = await fetch('http://example.com');
return response.json();
}
}
module.exports = ProductClient;
- 외부 API에서 데이터를 가져오는 메서드를 포함한다.
2. ProductService:
const ProductClient = require('./product_client');
class ProductService {
constructor() {
this.productClient = new ProductClient();
}
async fetchAvailableItems() {
const items = await this.productClient.fetchItems();
return items.filter((item) => item.available);
}
}
module.exports = ProductService;
- ProductClient의 인스턴스를 생성하여 API 호출한다. 이로 인해 ProductService는 ProductClient에 강하게 결합되어 있다고 볼 수 있다.
3. 테스트 코드:
const ProductClient = require('../product_client');
const ProductService = require('../product_service_no_di');
jest.mock('../product_client');
describe('ProductService', () => {
// ...
});
- ProductClient를 모킹하여 테스트를 수행하지만, 실제 의존성 주입을 사용하지 않기 때문에 유연성이 떨어지는 부분이다.
# Stub 형식의 의존성 주입 코드
1. ProductService:
class ProductService {
constructor(productClient) {
this.productClient = productClient;
}
async fetchAvailableItems() {
const items = await this.productClient.fetchItems();
return items.filter((item) => item.available);
}
}
module.exports = ProductService;
- 생성자에서 ProductClient를 매개변수로 받아서 의존성을 주입받는다. 이로 인해 ProductService는 특정 구현에 종속되지 않고, 다양한 클라이언트를 사용할 수 있다.
2.StubProductClient:
class StubProductClient {
async fetchItems() {
return [
{ item: '우유', available: true },
{ item: '바나나', available: false },
];
}
}
module.exports = StubProductClient;
- fetchItems 메서드가 고정된 데이터를 반환하는 stub 클라이언트이다. 이를 통해 테스트 환경에서 쉽게 제어할 수 있다.
3. 테스트 코드:
const ProductService = require('../product_service');
const StubProductClient = require('./stub_product_client');
describe('ProductService - Stub', () => {
let productService;
beforeEach(() => {
productService = new ProductService(new StubProductClient());
});
it('should filter out only available items', async () => {
const items = await productService.fetchAvailableItems();
expect(items.length).toBe(1);
expect(items).toEqual([{ item: '우유', available: true }]);
});
});
- ProductService를 생성할 때 StubProductClient를 주입하여 테스트를 수행한다.
# 비교 및 분석
1. 의존성 주입:
- 기존 코드에서는 ProductService가 ProductClient를 직접 생성하여 의존성을 관리한다. 반면, stub 형식의 코드에서는 ProductService가 외부에서 주입된 ProductClient를 사용한다.
이로 인해 ProductService는 다양한 클라이언트(예: 실제 API 클라이언트, stub 클라이언트 등)를 사용할 수 있어 유연성이 높아진다.
2. 테스트의 독립성:
- stub 형식의 코드에서는 테스트에서 StubProductClient를 주입하여 API 호출 없이도 ProductService의 기능을 검증할 수 있다. 외부 의존성에서 확실히 벗어날 수 있다.
3. 재사용성:
- 의존성 주입을 통해 ProductService는 다양한 환경에서 재사용 가능해진다. 테스트 환경에서는 StubProductClient를, 실제 서비스 환경에서는 실제 ProductClient를 주입하여 사용할 수 있다.
# 추가 공부...
1. ProductService:
이 서비스는 주로 제품과 관련된 작업을 수행한다. 예를 들어, 사용 가능한 제품을 가져오는 기능말이다.
2. StubProductClient:
이건 테스트를 위해 만든 특별한 클라이언트이다. 실제 API 대신에 고정된 데이터를 반환한다. 즉, 실제로 서버에 요청하지 않고도 제품 데이터를 테스트할 수 있게 해준다.
3. 전체 코드 역할
- new StubProductClient()는 StubProductClient의 새로운 인스턴스를 생성하는 코드이다. 이 인스턴스는 fetchItems 메서드를 가지고 있으며, 이 메서드는 고저왼 제품 데이터를 반환한다.
- new ProductService(new StubProductClient())는 productService의 새로운 인스턴스를 생성한다.
여기서 중요한 점은 ProductService가 StubProductClient의 인스턴스를 매개변수로 받는다는 것이다.
# 이해를 돕기 위한 비유
친구에게 음료수를 주고 싶다고 가정해 보자.
**ProductService**는 나고,
**StubProductClient**는 내가 친구에게 주기 위해 미리 준비해 놓은 음료수이다.
여기서 new ProductService(new StubProductClient())는 내가 친구에게 음료수를 주기 위해 미리 준비한 음료수(스텁)를 사용하는 상황이다.
#요약
new StubProductClient()는 고정된 데이터를 제공하는 객체를 만든다.
new ProductService(new StubProductClient())는 그 객체를 사용하여 ProductService를 만든다.
이 방식으로 ProductService는 실제 API에 의존하지 않고도 동작할 수 있다. 즉, 테스트를 쉽게 할 수 있게 해주는 것이다
'TDD' 카테고리의 다른 글
리액트에서 테스트는? (1) | 2024.09.05 |
---|---|
TDD - stub 방식과 mock 방식 (2) | 2024.09.04 |
테스트 코드의 원칙을 다시 상기하자. (0) | 2024.08.29 |
TDD 예제(2) (1) | 2024.08.29 |
TDD 준비 및 예제(1) (0) | 2024.08.29 |