테스트 코드의 원칙을 다시 상기하자.

 

# 한 번 작성된 테스트 코드는 영원히 유지보수 해야 한다.

- 내가 담당이라서 혼자서 유지보수할 수도 있지만, 다른 개발자가 유지보수하거나 업데이트할 수 있다. 그렇기 때문에 유지 보수성을 위해 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