TDD 챌린지 3주 차 - 테스트코드 피드백 반영하기
TDD 챌린지도 이제 마무리까지 약 일주일 정도 남은듯 하다.
최근 체력 이슈와 더불어 일감이 생겨서 썸네일 생성기에 관심을 많이 못 두고 있었던 건 사실..ㅠ
3주 차는 동준님께 테스트 코드 피드백을 받은 사항을 반영하고자 한다.
더불어 README.md 를 재구성하는 것이 이번 주 차 미션이다.
기능 확장도 조금씩 해보고 있는데, 테스트 코드를 먼저 작성하고 기능을 구현하려니 쉽지 않았다.
아직도 랜덤 값을 반환하는 함수는 테스트 코드를 어떻게 작성하면 좋을지 모르겠다는 점을 미루어보아 아직도 테스트 코드와 친해지지는 못한 것 같다. 😓
Mock 함수를 만들어야 하는지, 리턴 값을 강제로 고정 시켜야하는건지..!
E2E 테스트는 또 처음 접하다보니 해당 부분에서 원하는 테스트 코드를 작성하는 방법을 몰라서 차선(?)책인 방법을 선택했던 적도 있다.
피드백 반영하기
아무튼, 지난 2주 차에 핵심 기능(정말 작은 기능이지만..!)을 TDD로 구현해보고 챌린지를 오픈해주신 동준님께서 확인하실 수 있도록 깃허브 레포지토리를 공유드렸는데 토이 프로젝트에 대한 짧은 칭찬과 피드백을 남겨주셨다.
썸네일 생성기라니! 너무 매력적인 토이프로젝트에요~! 테스트 코드들의 길이도 적당하고, 작은 단위로 나누시려고 한 노력이 돋보입니다! 현재 코드 양이 많지 않아, 피드백 드릴 내용도 간결하게만 정리해봤어요~!
동준님께서 피드백 주신 테스트 코드 사항은 다음과 같다.
1. 하나의 테스트에서는 하나의 개념만 검증하기
하나의 테스트에서 여러 가지를 검증하게 되면 테스트가 복잡해지고, 실패했을 때 원인을 찾기 어렵게 되는데요.
예를 들어 "랜덤한 배경 색상을 만드는 genRandomColor 함수는 16진수 6자리를 반환한다"라는 테스트를 살펴볼게요.
이 테스트에서는 genRandomColor 함수가 반환하는 값의 길이와 내용을 모두 검증하고 있어요.
이를 두 개의 테스트로 분리하여 각 테스트가 검증하려는 개념을 명확히 할 수 있을 것 같아요~!
// 기존 코드
it('✨ 랜덤한 배경 색상을 만드는 genRandomColor 함수는 16진수 6자리를 반환한다', () => {
const colorCode = genRandomColor();
expect(colorCode).to.be.lengthOf(6);
const isHexCode = [...colorCode].every((code) => /^[0-9a-fA-F]$/.test(code));
expect(isHexCode).to.be.true;
});
// 개선된 코드
it('✨ 랜덤한 배경 색상을 만드는 genRandomColor 함수는 6자리를 반환한다', () => {
const colorCode = genRandomColor();
expect(colorCode).to.be.lengthOf(6);
});
it('✨ genRandomColor 함수의 반환 값은 16진수이다', () => {
const colorCode = genRandomColor();
const isHexCode = [...colorCode].every((code) => /^[0-9a-fA-F]$/.test(code));
expect(isHexCode).to.be.true;
});
이렇게 테스트를 분리함으로써 각 테스트가 검증하려는 개념이 명확해지고
테스트 실패 시에 어떤 부분이 문제인지 파악하기가 더 쉬워질 거예요~!
2. 테스트 케이스에 공통으로 사용되는 값은 상수로 관리하기
테스트 케이스를 통해 반복적으로 사용되는 값들, 예를 들어 테스트 케이스에 사용되는 문자열 "제목입니당"이나 "내용이네용" 등은 상수로 관리하는 것이 유지보수에 도움이 됩니다.
이를 통해 테스트 코드가 변경될 때, 한 곳에서만 변경하면 되므로 실수할 확률도 줄어들게 돼요~!
라고 코멘트 남겨주셨다.
// 기존 코드
it('✨ 제목 인풋 박스에 사용자가 "제목입니당"을 입력하면 썸네일 미리보기 제목에 "제목입니당"이 표시된다.', () => {
cy.get('[data-test="title-input"]').type('제목입니당');
cy.get('[data-test="title"]').should('have.text', '제목입니당');
});
// 개선된 코드
it(`✨ 제목 인풋 박스에 사용자가 "${TITLE_INPUT}"을 입력하면 썸네일 미리보기 제목에 "${TITLE_INPUT}"이 표시된다.`, () => {
const TITLE_INPUT = '제목입니당';
cy.get('[data-test="title-input"]').type(TITLE_INPUT);
cy.get('[data-test="title"]').should('have.text', TITLE_INPUT);
});
테스트 코드를 분리함으로써 테스트를 간결하게 하고 또 기존 구현한 함수를 쪼개게 되면 자연스럽게 클린 코드에 가까워 지는 것은 알고 있었지만, 그럼에도 미처 분리하지 못한 부분이 있었다는 것도 알 수 있었다.
it 함수를 사용할 때도 템플릿 리터럴을 사용하여 중복되는 텍스트를 상수로 사용하는 방법을 사용할 수 있다는 점은 미처 생각도 못했던 부분인데 새로운 인사이트를 얻을 수 있었다.
소중한 피드백 감사합니다..!
(역시 괜히 네카라쿠배가 아닌가보다 하며 챌린지 내내 드는 후광효과는 어쩔 수 없는듯 하다.)
전달받은 개선된 코드에서는 it 함수 내부에서 TITLE_INPUT을 정의하셨는데,
그러면 it 함수의 첫 번째 인자(템플릿 리터럴)로 넣어서 사용할 수 없을테니 본인은 바깥에서 빼서 사용하도록 코드를 수정했다.
const THUMBNAIL_TITLE = '제목입니당';
const THUMBNAIL_CONTENT = '내용이네용';
describe('💙 썸네일 생성기 테스트', () => {
// ... 생략
it(`✨ 제목 인풋 박스에 사용자가 "${THUMBNAIL_TITLE}"을 입력하면 썸네일 미리보기 제목에 "제목입니당"이 표시된다.`, () => {
cy.get('[data-test="title-input"]').type(THUMBNAIL_TITLE);
cy.get('[data-test="title"]').should('have.text', THUMBNAIL_TITLE);
});
it(`✨ 내용 인풋 박스에 사용자가 "${THUMBNAIL_CONTENT}"을 입력하면 썸네일 미리보기 내용에 "내용이네용"이 표시된다.`, () => {
cy.get('[data-test="content-input"]').type(THUMBNAIL_CONTENT);
cy.get('[data-test="content"]').should('have.text', THUMBNAIL_CONTENT);
});
생각해보니 굳이 전역으로 사용하지 않고 변수의 스코프를 줄이기 위해 describe 안에서 정의해서 사용해도 될 것 같다는 생각이 들었다.
테스트 코드에서 상수 정의 시 보편적으로 어디에 두는지도 급 궁금해지는 부분인데 예를 들면, 아무래도 전역 변수는 피하는 게 좋을 것 같은데 여러 함수에서 쓴다면 전역으로 선언하는 게 맞을 것 같고.. 다른 파일에 만들어놓고 import 해서 사용하는 게 나을지와 같은 부분이다.
추론해보자면 사용하는 상수가 너무 많으면 파일을 쪼갤 것 같고, 그렇지 않으면 스코프를 최대한 좁혀서 사용하지 않을까?라고 생각한다. 정답이 있다면 알고 싶은 부분이지만 모든 코드엔 정답이 없기에 그나마 클린 코드에 가깝게 작성하려면 그렇지않을까..? 하는 조심스러운 추측이자 현재 내 생각은 그렇다.
우선 동준님께서 남겨주신 피드백 사항은 반영했는데 추가로 기능 확장을 구현할 예정이라 계속 테스트 코드도 신경써서 작성해봐야겠다. 진짜 내가 사용할 수 있을 정도로는 만들어보고 싶다!
그동안 만들어 본 토이프로젝트라곤 CRUD 기능이 있는 투두리스트 정도에서 머물러있기 때문에 이번 토이 프로젝트이자 TDD는 의미가 남다르다.
현재는 정말 간단히 필수 로직만 돌아가는 상태라 예쁘게 사용하긴 어렵다는 단점이 있다.
쓰려면 쓸 수는 있겠지만, 마음에 드는 썸네일을 뽑진 못하기 때문에 챌린지 4주 차 마감 전까지는 보완해봐야겠다.