J서비스 프론트엔드 테스트코드 적용기
제가 현재 진행하고있는 프로젝트는 Angular 기반의 웹과 앱을 서비스하고 있는 프로젝트입니다. 최초 설계 당시 Angular 스타일이 아니라 급하게 작업 된 프로젝트기에 Angular의 기본 개념의 적용이 힘들었고 그로 인해서 테스트코드 실행 시 에러 발생으로 테스트코드 적용은 다음 프로젝트를 기약했습니다.
이번에 진행한 feature 단위에 프로젝트에서 프로젝트 목표 중 하나를 테스트코드 적용으로 잡았고 간단하게나마 해당 프로젝트에 대해서는 테스트코드를 적용했고 현재 테스트코드를 통해서 테스트와 병행하면서 같이 개발을 진행하고 있습니다.
내부 개발자들에게 공유한 내용인데 실제로 테스트코드를 적용하면서 겪었던 내용을 공유하기 위해 포스팅해 봅니다.
도대체 어떤 내용을 테스트코드로 작성해야 할까?
실무에 테스트코드를 적용하기 전 인강이나 혹은 블로그를 통해서 테스트코드에 대한 내용은 많이 접했습니다. 하지만 실제로 우리 프로젝트의 소스에 넣어보려고 하니 무엇부터 해야하고 어떤 코드부터 테스트코드를 작성할 수 있는지 감이 없다보니 매번 도입을 하고 싶다에서 끝났고 실제로 실무 프로젝트에 적용해서 개발과 병행하고 있는 프로젝트는 이번 프로젝트가 처음입니다.
처음 테스트코드를 동작, 작동 그리고 능숙하게 작성하기까지 시간이 오래걸렸고 저는 동료 개발자들에게 다음과 같은 기준으로 테스트코드 작성을 권했습니다. 물론 아직까지 저도 다음의 기준으로 테스트코드를 작성하고있습니다.
- 테스트 하기 귀찮은가?
- 테스트 하기 오래 걸리는가?
- 테스트 하기 어려운가?
- 테스트 하기 힘든가?
- 테스트 하기 복잡한가?
기존에 돌아가는 코드에 테스트코드를 적용하는 일은 쉽지 않습니다. 또한 한 번도 작성해보지 않은 테스트코드를 능숙하게 작성하기까지는 시간이 오래 걸립니다. 그래서 저는 테스트코드의 이점을 이해하고 직접 느낄 수 있도록 "테스트 하기 어려운가 혹은 귀찮은가?"를 기준으로 테스트코드를 작성하고 있습니다.
물론 저도 아직은 서비스의 비즈니스 로직과 검증이 필요한 수준의 로직, 그리고 테스트가 어려운 컴포넌트의 컴포넌트 정도를 테스트코드로 작성하고있습니다. 다만 위의 내용을 기준으로 잡은것은 약 한 달간의 테스트코드 코딩을 해보니 테스트하기 어렵고 까다로운 기능일수록 테스트코드의 장점을 더욱더 크게 와닿았습니다.
그래서 저는 처음 테스트코드를 작성했을 때 UI에서 테스트하기 귀찮은 이름 입력값에 대한 Valid 체크 메소드를 테스트코드로 작성했습니다.
it('#isValidName 아이 이름 벨리데이션 테스트', () => {
const testDatas = [
{value: '', result: false, description: '아무것도 입력하지 않음(공백)'},
{value: '김 희만', result: false, description: '공백이 포함된 이름'},
{value: '김a희만', result: false, description: '이름에 영어가 포함됨'},
{value: '김', result: false, description: '입력된 이름이 한 글자'},
{value: '김희만', result: true, description: '정상적인 이름'}
];
testDatas.forEach(testData => {
expect(service.isValidChildName(testData.value)).toBe(testData.result, testData.description);
});
});
it('#isValidDateOfBirth 아이 생년월일 벨리데이션 테스트', () => {
const testDatas = [
{value: '20162011', result: false, description: '범위에 포함하지 않는 달 입력(20월)'},
{value: '20160235', result: false, description: '범위에 포함하지 않는 일 입력(32일)'},
{value: '2016-02-11', result: true, description: '하이픈(-)이 존재하는 데이터 타입'},
{value: '2016-0211', result: true, description: '하이픈(-)이 1개만 존재하는 데이터 타입'},
{value: '20160211', result: true, description: '올바른 입력 타입'},
{value: '201021', result: false, description: '올바르지 않은 타입(누락)'},
{value: '', result: false, description: '올바르지 않은 타입(공백)'},
{value: '201021245', result: false, description: '올바르지 않은 타입(YYYYMMDD 타입보다 더 많은 입력)'},
{value: '201609022', result: false, description: '올바르지 않은 타입(YYYYMMDD 타입보다 더 많은 입력)'},
];
testDatas.forEach(testData => {
expect(service.isValidDateOfBirth(testData.value)).toBe(testData.result, testData.description);
});
상단의 코드처럼 이름과 생년월일에 대한 입력값을 매번 UI의 팝업을 노출시킨 뒤 테스트하곤 했습니다. 그러다보면 내가 어디까지 테스트를 진행했는지 까먹게 되고 로직이 변경되면 기존에 테스트했던 내용을 다시 테스트해야하는 상황이 발생하곤 하는데요.
해당 코드를 제가 이번 프로젝트에서 처음 적용하게 되었는데 실제로 얼마 지나지 않아서 테스트코드의 강력함을 경험했습니다.
아이 생년월일에 대한 정합성 체크를 Date 객체를 통해서 했는데 사파리 브라우저와 크롬 브라우저에서의 동작이 미세하게 달랐고 입력된 날짜가 현재 존재하는 날짜인지 체크가 추가로 필요했습니다. moment.js 사용을 결정했고 Date 객체에서 moment 사용으로 전환까지 5분도 안걸리는 시간에 변경을 완료했고 기존 작성했던 테스트 케이스에 대한 테스트까지 모두 검증이 완료된 상태로 말이죠
초반에 테스트코드가 능숙해지기 전까진 러닝커브로 인해서 기존 개발 속도 대비해서 체감상 3배 이상 걸렸고 대부분의 에러가 실제 작성한 코드가 아닌 테스트 코드 작성에서의 에러가 많이 발생했습니다. 또한 테스트 코드를 작성하면서 테스트 코드 난이도를 낮추기 위해서 컴포넌트를 중간중간 리팩토링을 진행해야 했고 서비스 분리 등 실제 개발에 소요된 시간보다는 테스트 코드 작성을 위한 시간을 많이 사용했습니다. 테스트 코드를 실제로 프로젝트에 적용한 지 글을 작성하는 시점에서는 대략 5주 정도가 지났고 지금 돌이켜보자면 작성된 테스트 코드에 대한 코드는 요구사항 변경이 있더라도 기존 로직에 대한 검증이 개발과 같이 이루어지기 때문에 변경에 필요한 시간이 줄었고 작성한 지 오래된 코드라도 테스트 코드와 주석을 통해서 해당 코드가 어떤 기능을 해야 하는지 바로 확인이 되어 오히려 실제 개발부터 운영까지의 프로세스에서 보자면 시간이 더욱더 단축되었습니다.
현재는 제법 익숙해져 더욱더 난이도있는 비즈니스 로직에서의 테스트코드와 Angular DI 정책상 SpyObj 사용이 필요한 경우가 있는데 SpyObj를 이용해서 DI를 회피하면서 유닛테스트를 진행하는 등 제가 진행하고 있는 부분에 대해서는 더욱 더 많은 범위의 테스트 코드가 적용되고 있습니다. 테스트 코드를 작성하면서 얻을 수 있는 이점을 나열하면서 이 글을 마무리하겠습니다~
Testable Code
테스트 코드의 작성이 가능하다는 것은 테스트 코드의 작성이 가능한 코드를 작성했다는 뜻입니다. 테스트 코드가 가능한 코드란 서로의 관심사가 분리되어 각자의 관심사에서 동작하는 코드를 의미하는데 관심사가 뭉쳐있다면 테스트 코드의 난이도는 급격하게 상승합니다.
Self-Descriptive
레거시 혹은 내가 아닌 다른 사람이 작성한 코드를 분석하는 과정에서 가장 힘든 점은 비즈니스 로직에 대한 판단과 묵어있는 히스토리를 찾아내고 현재 사용하는지 혹은 사용하지 않는지 판단하는 과정이 매우 힘들고 가독성 좋은 코드를 작성하라 하지만 어떻게 작성해야 가독성이 좋은지는 판단이 어렵습니다. 대신 비즈니스 로직 혹은 검증이 필요한 로직을 테스트 코드와 함께 작성한다면 그 자체가 해당 로직에 대한 문서가 될 수 있습니다.
API 언제 나와? 나 그동안 무슨 작업을 해야 하지?
요구사항이 자주 변하는 스타트업 특성상 프론트엔드에서 변경이 없더라도 DB설계 혹은 데이터의 변경이 자주 일어나는 업무가 있을 수 있습니다. 테스트 코드를 작성한다면 해당 업무에 대한 로직이 변경되더라도 기존에 작성한 테스트 케이스에 대한 동작을 보장할 수 있고 API가 나오기 전 UI뿐 아니라 전체적인 프로세스에 대한 개발을 선행할 수 있습니다.
Cross-Browsing 환경에서의 검증
IE, Firefox, Chrome, Safari, Webview 등 브라우저의 종류는 다양해집니다. 자바스크립트 엔진의 배포로 인해서 많은 브라우저가 탄생했고 자바스크립트라는 동일한 스크립트 언어를 사용하지만 실제 엔진 환경에 따라서 크롬에서 동작하는 코드가 사파리에서는 정상적으로 동작하지 않을 수 있습니다. 매번 개발하는 기능과 수정하는 기능의 테스트를 크롬,사파리,웹뷰 등 여러 브라우저에서 테스트한다면 테스트를 진행하는 시간도 무시할 수 있는 수준이 아닙니다. 테스트 코드를 작성한다면 테스트 케이스에 대한 검증을 원하는 브라우저 환경에서 검증하면서 작업을 할 수 있습니다.
귀찮은 테스트…
프로그램 빌더에서는 각 컴포넌트에 따라 변경되는 컴포넌트가 달라지고 프로그램 빌더를 통해 생성된 데이터의 구성에 따라서 컴포넌트 혹은 여러 조건들이 변경됩니다. 작업을 진행하면서 경우의 수가 100가지 정도 된다면 매번 기능을 수정 혹은 개발하면서 100가지 경우의 수 * 지원하는 브라우저의 수만큼 테스트를 진행해야 합니다. 하지만 관심사를 분리하고 본인의 관심사에 따른 비즈니스 로직을 테스트 코드로 작성한다면 실제로 UI상에서 개발자가 인터렉션을 일으키면서 테스트를 진행해야 하는 경우의 수가 줄어듭니다. 테스트 코드를 경험해보니 실제 대부분의 예외처리 혹은 조건에 따른 변경은 90% 이상 테스트 코드로 커버가 가능하고 기능의 변경 혹은 요구사항의 변경이 있더라도 이미 작성된 테스트 코드를 통해서 새롭게 변경된 경우의 수만 테스트를 진행하면 됩니다.