# 한 번 작성된 테스트 코드는 영원히 유지보수 해야 한다.
- 내가 담당이라서 혼자서 유지보수할 수도 있지만, 다른 개발자가 유지보수하거나 업데이트할 수 있다. 그렇기 때문에 유지 보수성을 위해 Clean Code로 작성해야 한다.
# 내부 구현 사항을 테스트하지 않는다.
- 기능 검증
테스트는 주로 기능이 올바르게 작동하는지를 검증하는 데 초점을 맞춰야 한다. 내부 구현에 의존하게 되면, 코드 변경 시 테스트가 불필요하게 실패할 수 있다.
- 유지보수 용이성
내부 구현 로직 변경되더라도, 외부에서 제공하는 API나 기능이 동일하다면 테스트가 통과해야 한다. 내부 구현을 테스트하면, 사소한 변경에도 테스트를 수정해야 한다.
- 테스트의 독립성
테스트는 각 모듈이 독립적으로 작동하는지를 검증해야 한다. 내부 구현에 대한 의존성은 테스트 간의 독립성을 해칠 수 있다.
- 리팩토링의 용이성
코드의 리팩토링이 필요할 때, 내부 구현이 테스트에 포함되어 있다면 리팩토링 과정이 복잡해질 수 있다. 반면, 외부 인터페이스만 테스트하고 있다면, 내부 구현을 자유롭게 수정할 수 있다.
따라서, 테스트는 주로 외부 인터페이스나 결과를 검증하는 방식으로 작성하는 것이 좋다. 이는 유지보수성과 안정성을 높이고, ,코드의 품질을 개선하는 데 기여한다.
예시로 살펴보자!
예시 1: 계산기 모듈
// 계산기 모듈
class Calculator {
add(a, b) {
return a + b; // 내부 구현
}
}
// 테스트 코드
test('두 수를 더한다', () => {
const calculator = new Calculator();
expect(calculator.add(2, 3)).toBe(5); // 결과 검증
});
- add 메서드의 내부 구현(단순 덧셈)을 테스트하는 것이 아니라, 입력된 두 수의 합이 올바른지를 검증한다.
예시 2 : 사용자 인증 모듈
// 사용자 인증 모듈
class AuthService {
login(username, password) {
// 내부 로직: DB에서 사용자 검증
return username === 'user' && password === 'pass'; // 내부 구현
}
}
// 테스트 코드
test('올바른 자격 증명으로 로그인', () => {
const authService = new AuthService();
expect(authService.login('user', 'pass')).toBe(true); // 결과 검증
});
- login 메서드의 내부 로직(사용자 검증 방식)을 테스트하지 않고, 올바른 자격 증명으로 로그인이 성공하는지를 검증한다.
예시 3 : 데이터 조회 모듈
// 데이터 조회 모듈
class DataService {
fetchData() {
// 내부 구현: API 호출 및 데이터 가공
return fetch('https://api.example.com/data').then(response => response.json());
}
}
// 테스트 코드
test('데이터를 성공적으로 조회', async () => {
const dataService = new DataService();
const data = await dataService.fetchData();
expect(data).toHaveProperty('key'); // 결과 검증
});
- fetchData 메서드의 API 호출 및 데이터 가공 로직을 테스트하지 않고, 반환된 데이터가 특정 속성을 가지고 있는지를 검증한다.
# 테스트 코드에서 반복적으로 사용되어지는 코드가 있다면 재사용성을 높이기 위해서 별도의 함수나 테스트용 클래스를 만들어서 재사용성을 높여야 한다.
예시 1 : 버튼 클릭 테스트 함수
// 테스트 유틸리티 함수
const clickButtonAndCheckCount = (buttonText, expectedCount, renderFunc) => {
renderFunc();
const button = screen.getByText(buttonText);
fireEvent.click(button);
expect(screen.getByText(`Count: ${expectedCount}`)).toBeInTheDocument();
};
// Counter 컴포넌트
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
export default Counter;
// 테스트 코드
test('버튼 클릭 시 카운트가 증가한다', () => {
clickButtonAndCheckCount('Increment', 1, () => render(<Counter />));
});
예시 2 : 입력값 변경 테스트 함수
// 테스트 유틸리티 함수
const changeInputAndCheckText = (inputValue, expectedText, renderFunc) => {
renderFunc();
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: inputValue } });
expect(screen.getByText(expectedText)).toBeInTheDocument();
};
// TextInput 컴포넌트
import React, { useState } from 'react';
const TextInput = () => {
const [text, setText] = useState('');
return (
<div>
<input type="text" onChange={(e) => setText(e.target.value)} />
<p>{text}</p>
</div>
);
};
export default TextInput;
// 테스트 코드
test('입력값에 따라 텍스트가 변경된다', () => {
changeInputAndCheckText('Hello', 'Hello', () => render(<TextInput />));
});
# 배포용 코드와 철저히 분리
1. 디렉터리 구조 분리
- 테스트 파일을 벼로의 디렉터리로 분리하는 것이 가장 일반적인 방법이다.
/project
/src # 배포용 코드
index.js
utils.js
/tests # 테스트 코드
index.test.js
utils.test.js
2. 파일 확장자 사용
- 테스트 파일의 확장자를 .test.js 또는 .spec.js와 같이 명시적으로 설정하여 테스트 코드임을 구분한다. 이는 테스트 프레임워크에서 자동으로 테스트 파일을 인식하게 도와준다.
3. 빌드 도구 설정
- 빌드 도구(예 : Webpack, Babel 등)를 사용하여 배포할 때 테스트 코드를 포함하지 않도록 설정한다.
예를 들어, Webpack에서는 exclude 옵션을 사용하여 테스트 디렉터리를 제외할 수 있다.
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /tests/, // tests 폴더 제외
use: {
loader: 'babel-loader',
},
},
],
},
};
4. CI/CD 파이프라인 설정
- CI/CD 도구(예 :GitHub Actions, Travis CI 등)를 설정하여 테스트가 성공적으로 통과한 경우에만 배포하도록 한다. 이로 인해 배포 시 테스트 코드는 자동으로 무시되거나 제거된다.
5. 환경 변수 사용
- 배포 환경에서만 필요한 코드와 테스트 코드가 다르다면, 환경 변수를 사용하여 조건부로 코드를 실행할 수 있다.
if (process.env.NODE_ENV !== 'production') {
// 테스트 코드 또는 개발용 코드
}
6. 패키지 매니저 스크립트
- npm이나 yarn을 사용하여 배포 스크립트와 테스트 스크립트를 분리한다. package.json에서 다음과 같이 정의할 수 있다.
{
"scripts": {
"test": "jest",
"build": "webpack --config webpack.prod.js" // 테스트 코드 제외된 설정
}
}
# 테스트 코드를 통한 문서화
문서화 역할은 어떻게 하는 것인가?
// add 함수의 테스트
describe('add', () => {
test('1과 2를 더하면 3이 되어야 한다', () => {
expect(add(1, 2)).toBe(3);
});
test('-1과 1을 더하면 0이 되어야 한다', () => {
expect(add(-1, 1)).toBe(0);
});
test('0과 0을 더하면 0이 되어야 한다', () => {
expect(add(0, 0)).toBe(0);
});
});
1. 함수 설명 : add 함수의 코드 주석은 이 함수의 목적을 간단하게 설명하고 이를 통해 다른 개발자들이 쉽게 이해할 수 있다.
2. 테스트 설명 : 각 테스트 케이스의 설명 부분(test의 첫 번째 인자)은 이 테스트가 무엇을 검증하는지를 명확히 하고 있다. 예를 들어, "1과 2를 더하면 3이 되어야 한다."는 함수의 기대 동작을 문서화하는 역할을 한다.
3. 자동화된 검증 : 테스트가 실행될 때, 각 조건이 충족되는지를 자동으로 확인하므로, 문서화된 내용이 실제로 올바른지 검증할 수 있다.
잘 작성된 테스트 코드는 코드의 기능을 문서화할 뿐만 아니라, 코드 변경 시에도 해당 기능이 제대로 작동하는지를 검증하는 중요한 역할을 한다. 잘 작성된 테스트 코드는 팀원들이 코드를 이해하는 데 큰 도움이 된다.
'TDD' 카테고리의 다른 글
TDD - stub 방식과 mock 방식 (2) | 2024.09.04 |
---|---|
TDD 예제 2 (Dependency Injection)에 대해서 (3) | 2024.09.02 |
TDD 예제(2) (1) | 2024.08.29 |
TDD 준비 및 예제(1) (0) | 2024.08.29 |
TDD(테스트 주도 개발)에 대해서 알아보자. (1) | 2024.08.28 |