테스트 코드 작성은 프로덕트가 배포되기 전 사전에 결함을 찾는데 도움을 줍니다.
하지만 테스트 코드를 작성했다고 프로덕트에 오류가 없다고 말할 순 없습니다.
보통 테스트 커버리지 100%를 채울 수 없고, 테스트 코드 또한 사람에 의해 작성되기 때문입니다.
1. 말뿐이 아닌 동작하는 명세
테스트 코드는 개발자가 작성한 코드의 실제 동작 예시를 구현함으로,
다른 개발자가 코드를 읽을 때 코드의 동작의도를 명확하게 파악하는데 도움을 줍니다.
테스트 코드를 작성하다보면, 결합도가 높은 컴포넌트가 페이지 등을 만났을 때 테스트 작성의 곤란함을 느끼게 됩니다. 이러한 곤란함은 기존 코드의 설계에 대해 고민하게 하고, 결합도를 낮추는 가이드가 됩니다.
거대한 모듈 혹은 페이지를 리팩터링 해서 여러가지 파일과 컴포넌트로 나눠야하는 일이 생겼을 때, 리팩터링 후 기존의 구현이 문제가 생기진 않을까, 잘 작동할까 걱정해본적 있지 않으신가요? 또한 리팩터링 후 기존 코드로 진행했던 QA를 개발자가 다시 한번 진행 해야 할 것 입니다. 테스트 코드는 “결과의 변경 없이” 코드를 수정하는데 도움을 줍니다. 마찬가지로 타인이 작성한 코드를 유지보수하고 수정 할 때에도, 기존의 동작이 제대로 구현되는지 확인하고 안전하게 새로운 기능을 추가 할 수 있습니다.
같이 협업을 하다보면, 프로젝트 전체에서 사용되고 있는 Util 함수를 수정해야 할 때도 있고, 이미 선언되어서 사용되고 있는 Interface의 속성을 Optional로 바꾸거나 타입을 변경해야 하는 경우도 있습니다. 이런 때에 공통 모듈을 수정 후 전체 테스트를 돌림으로써 기존에 의도된 코드들이 정상으로 작동하는데 도움을 얻을 수 있습니다.
function Button({ onClick }: { onClick: () => void }) {
return <button onClick={onClick}>클릭</button>;
}
describe('버튼 유닛 테스트', () => {
it('버튼을 클릭하면 전달받은 함수를 실행한다..', () => {
// 먼저 vitest를 사용해서 가상 함수를 선언합니다.
const handleClick = vi.fn();
// Button 컴포넌트를 node기반의 가상환경에서 렌더하면서 위에서 만든 함수를 전달합니다.
const { getByText } = render(<Button onClick={handleClick} />);
// 클릭 텍스트를 가진 dom을 찾아서 클릭 합니다.
fireEvent.click(getByText(/클릭/i));
// 위에서 선언한 handleClick 함수가 1번 실행되었는지 확인합니다.
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
function LoginPage() {
const [email, setEmail] = useState('');
const [submitted, setSubmitted] = useState(false);
const handleSubmit = () => {
if (email) setSubmitted(true);
};
return submitted ? <p>로그인 성공</p> : (
<div>
<input placeholder="email" onChange={e => setEmail(e.target.value)} />
<button onClick={handleSubmit}>로그인</button>
</div>
);
}
describe('로그인 페이지 통합테스트', () => {
it('이메일을 입력하고 로그인을 누르면 "로그인 성공" 메시지가 출력된다..', () => {
// 먼저 로그인 페이지를 node기반의 가상환경에서 렌더합니다.
render(<LoginPage />);
// placeHolder가 email인 것을 찾고 value로 이메일을 넣습니다.
fireEvent.change(screen.getByPlaceholderText(/email/i), {
target: { value: 'user@example.com' }
});
// 로그인 버튼을 찾아서 클릭합니다.
fireEvent.click(screen.getByText(/로그인/i));
// 로그인 성공 메시지가 화면에 출력됐는지 확인합니다.
expect(screen.getByText(/로그인 성공/i)).toBeInTheDocument();
});
});
단일 책임 원칙
it('탭 클릭시 탭이 변경되고, 변경된 탭에 대한 내용이 표시된다')
-> X
it('탭 클릭시 탭이 변경 된다')
it('선택된 탭에 대한 내용이 표시된다')
-> O
코드 컨벤션
*.test.ts
또는 *.spec.ts
를 사용합니다it()
함수 또는 test()
함수를 사용합니다.tests
디렉토리를 사용합니다.__test__
, __mock__
을 사용합니다.테스트 코드 위치
Ex)
src/components/button/Button.tsx
src/components/button/tests/Button.spec.tsx
Ex)
src/test/setupTest.ts
src/utils/test/setupTest.ts
Statement Coverage : 각 구문이 최소 1회 이상 실행됐는지 측정
Branch Coverage : 조건문(if/else)의 각 분기가 실행됐는지 측정
Function Coverage : 정의된 함수들이 최소 1회 이상 호출됐는지 측정
Line Coverage : 각 라인이 최소 1회 이상 실행됐는지 측정
에러나 버그를 회피하라는 것은 아닙니다.
내부 구현보다 인터페이스에 초점을 맞춰야 한다는 얘기입니다.
프론트 테스트 코드는 자바스크립트 자체를 테스트하기 보다, 사용자 중심으로 테스트 해야합니다.
예시를 들어보겠습니다.
나쁜예 -> it('button을 클릭했을때 표시되는 modal의 postion은 "absolute"이다')
좋은예 -> it('button을 클릭했을때 modal이 생성된다')
나쁜예 -> it('checkBox를 클릭했을때 checkBox의 checked 값이 true 이다')
좋은예 -> it('checkBox를 클릭했을때 Checked Icon이 화면에 표기된다.')
나쁜예 -> it(fetching시 isLoading의 상태가 'true'이다)
좋은예 -> it(fetching시 화면에 LoadingSpinner 컴포넌트가 생성된다)
사용자 측면과 요구사항의 결과는 같은데,
코드 변경에 따라 테스트가 계속해서 깨진다면 유지보수 하기 어려운 테스트가 됩니다.
Ex) 테스트 하지 않아도 되는 코드들
function Label (label:string){
return <label>{label}</label>
}
const isString => (value) => typeof value === 'string';
테스트 코드는 견고한 프로덕트를 만드는데 도움이 되지만,
동시에 개발자의 리소스를 더 사용하게 될 수 밖에 없습니다.
배포된 버전에 문제가 있는데, 테스트 코드를 수정하고 있기는 어려울 것 입니다.
중간 지점을 잘 찾아서 좋은 도구로 사용했으면 좋겠습니다.