안녕하세요, 대학생 웹 개발 인턴 Garden입니다.
최근에 입사한 회사에서 프론트엔드와 백엔드 개발을 하는 팀에 소속되어 저만의 업무 로드맵을 가지게 되었는데요!
저는 인턴 기간 동안 사내 개발 테스트 관련한 모든 업무(UI부터 API, E2E, 그리고 자동화까지!) 를 진행하게 되었습니다 ;) 테스트 분야의 오너십을 갖게 되었어요! 이렇게 업무를 할 수 있도록 도와주신 팀리더 Harry, CTO Ethan 에게도 정말 감사드립니다 ㅎㅎ
아직 사내에 개발 테스트 환경이 세팅되어 있지 않아 첫 시작을 인턴인 제가 하게 된 것에 어깨가 무겁네요 하지만 저는 평소 테스트에 관심이 많았고 제가 직접 도입하게 되어 정말 기쁘게 공부해보고 있습니다.
사담은 여기까지 하고, 본격적으로 사내에 테스트 환경을 구축하고, 도입하기 전에 프론트엔드에서 테스트 개념을 정리해보고자 본 글을 작성하게 되었습니다. 테스트의 관한 모든 자료를 읽어보면서 학부생 개발자의 시선에서 정리한 자료이기에 깊은 이론적 내용은 없어 가볍게 읽어보시면 좋을 것 같습니다 :) 쉬운 개념으로 정리되었어요!
이 글은 시리즈로 출간 될 것이며, 오늘은 그 시작인 테스트의 개념을 알아볼 것입니다. 그리고 이어서 사내 테스팅 툴 선정 및 도입 이야기 , 테스트 적용 이야기 등등 더 많은 이야기로 찾아뵙겠습니다!
우선 테스트라는 것을 제가 왜 생각을 하게 되었는지 그 처음으로 돌아가보겠습니다. 작년에 우아한테크코스 프리코스를 하면서 테스트 주도 개발인 TDD / DDD 개발론을 배우고 직접 구현한 로직의 단위 테스트까지 해보게 되었습니다. 이 때 테스팅의 개념을 처음 알게 되었고 입사해서도 계속 테스팅에 관심을 가지게 해준 소중한 계기가 되었습니다.
먼저 테스트의 정의부터 상기해보고 가보겠습니다.
프로그램을 실행하여 오류와 결함을 검출하고, 애플리케이션이 요구사항에 맞게 동작하는지 검증하는 절차
제가 처음 테스트 개념을 듣고 우테코 프리코스를 하면서 가장 먼저 든 생각은 "프론트엔드 개발에 테스트가 왜 필요하지?" 였습니다. 백엔드는 테스팅이 필요할 것 같은데 프론트엔드에서는 그나마 API 부분 말고는 뭐 테스팅 할게 필요해? 라고 생각했었습니다.
하지만 우리가 디자이너의 디자인을 보고 UI를 개발하고, 화면에 잘 렌더링 되는지 확인하는 과정과 기존 코드에 새로운 기능을 추가하고 올바르게 실행되는지 확인하는 과정 모두 테스트에 해당한다는 사실을 알게 되었습니다.
이를 정리해서 프론트엔드에서 테스트 대상은 대체적으로
크게 3가지의 영역으로 볼 수 있습니다.
그렇다면 개발자들이 테스트를 하는 목적은 무엇일지 궁금했습니다.
바로 구현하면서 발생하는 오류를 예방하고 구현 요구사항이 모두 충족되는지 확인하거나, 새로운 오류가 유입되지는 않는지 여러 예외 처리를 진행할 수 있습니다. 따라서 개발자들은 자신의 서비스 품질 수준을 높이고, 지속적으로 리팩토링 과정을 거치다보니 궁극적으로 코드 품질을 높이게 됩니다.
결과론적으로 테스트는 내 코드에 대해 자신감을 가지게 됩니다.
내 코드에 자신감을 가지는 게 왜 중요할까요? 코드에 대한 자신감이 없으면 기존 버그를 수정하거나, 새로운 기능을 추가하거나, 리팩토링에 부담감이 생기에 되어 자연스레 기술 부채가 생기게 됩니다. (여기서 기술 부채란, 기술적으로 해결되어야 할 문제들을 나중으로 미루어 남아있는 것을 의미합니다.)
계속해서 기술 부채가 쌓이게 된다면 우리는 결국 파산에 이르게 될거예요,, 계속 미루다가 이자가 불어나서 거의 코드를 새로 짜는게 효율적인 정도에 이르게 된다는 점이 가장 큰 문제입니다.
우리는 테스팅을 도입해서 내 코드에 자신감을 가지고 지속적인 리팩토링과 적극적인 기술 부채 상환을 이루어 내기 위해 노력해야합니다.
먼저 최근 테스트의 유형을 정적/단위/통합/E2E 테스트로 나눠볼 수 있습니다.
아래는 Kent. C. Dodds 가 정의한 테스팅 트로피 입니다.
한 유형씩 차근 차근 살펴보겠습니다 ;)
정적 테스트에 해당하는 내용은 타입 / 린트 오류 등이 해당합니다.
이 정적 테스트는 직접 코드를 실행 시키지 않고 테스트를 하는 것인데, 이는 개발을 진행하면서 흔히 실수하는 과정인 에러를 미리 예방할 수 있도록 도와줍니다. 요약하자면 구문 오류나 나쁜 코드 스타일 등을 검증합니다.
특히 Typescript를 사용할 때 함수의 인자로 받는 파라미터의 타입 검사를 진행하는 것이 이 정적 테스트 유형에 포함 될 수 있습니다.
단위 테스트는 기능의 개별적인 단위를 테스트하는 것으로 그들이 잘 동작하는지 확인하는 과정들이 해당합니다. 하나의 함수, 메소드 , 클래스, 모듈 등이 이에 속하게 되고 이는 테스트의 제일 작은 단위라고 볼 수 있습니다. 아참, 하나의 컴포넌트 단위도 단위 테스트에 해당합니다.
단위가 작기 때문에 작성 비용도 낮고, 실행 속도도 빠르다는 장점이 있습니다. 대표적인 단위 테스팅 도구로는 Jest가 있습니다.
여기서 단위 테스트는 solitary test( 단독적 테스트) 와 sociable test( 사회, 사교적인 테스트)로 나눌 수 있습니다.
통합테스트는 두 개 이상의 모듈이 실제로 연결된 상태를 테스트 하는 것을 의미합니다. 모듈간의 연결에서 발생하는 에러를 검증하고 리팩토링의 손실을 줄일 수 있습니다. 이 테스트에서는 외부 라이브러리까지 묶어서 테스트하는 경우가 있습니다.
단위테스트를 구현하면서 이와 연결된 상하위 컴포넌트들까지 봐야하는 경우에 통합테스트로 접근하여 진행해야합니다. 큰 범위(페이지)의 테스트를 의미하게 됩니다.
이 테스트 단계에서는 데이터 통신 및 전반적인 공유 컴포넌트간 제어 문제를 식별하는데 도움이 되고, 통합 테스트 부터는 자동화 가능한 영역에 포함됩니다.
통합 테스트의 비중이 트로피 사진에서 왜 가장 클까요?
Write tests. Not too many. Mostly integration.
테스트를 작성해라! 하지만 너무 많이는 작성하지 말아라. 라고 하는데 그 이유는 테스트 커버리지가 70% 이상이게 된다면, 실제 테스트할 필요가 없는 것에 시간을 할애하게 될 수도 있기 때문입니다.
그리고 대부분 통합테스트 위주로 작성하라고 하는데 그 이유는 통합 테스트가 테스트의 속도 및 비용이 가장 균형에 맞는 테스트이기 때문입니다.
E2E 테스트는 실제 사용자가 사용하는 것과 같은 조건에서 전체 시스템을 테스트하는 것으로 프로젝트의 모든 API 서버나 DB 등의 외부 서비스들을 사용해 통합된 시스템을 테스트하는 것을 의미합니다.
우리가 흔히 기능 테스트라고 부르는 것도 E2E 테스트를 한다고 생각해도 될 것 같습니다. 가장 큰 단점은 비용도 많이 들고 속도가 느리다는 것을 알 수 있습니다.속도가 느린 점은 위에서 언급된 단위들 보다 범위가 너무 크고, 이에 따른 테스트 시나리오도 매우 다양하고, 예외가 많기 때문입니다.
아까 위에서 프론트엔드에서의 테스팅 대상은 크게 3가지로 나눠볼 수 있다고 했었는데 각각 어떻게 테스트를 진행하는지 대상별로 간단히 살펴보겠습니다.
사용자의 이벤트를 처리하는데 사용되는 테스트를 먼저 nodeJS 환경에서 진행해보겠습니다. 우선 @testing-library/react 를 사용한 테스트 예시 코드를 함께 살펴보겠습니다.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MessageButton from './MessageButton';
describe('MessageButton 컴포넌트 테스트', () => {
test('버튼 클릭 시 메시지가 표시되어야 함', async () => {
render(<MessageButton />);
// 버튼을 찾습니다.
const button = screen.getByRole('button', { name: /클릭하세요/i });
// 버튼을 클릭합니다.
await userEvent.click(button);
// 메시지가 화면에 표시되는지 확인합니다.
const message = screen.getByTestId('message');
expect(message).toHaveTextContent('버튼이 클릭되었습니다!');
});
});
사용자가 버튼을 클릭하면 메세지가 표시되는지를 테스트하면서 테스트 툴이 버튼을 찾고, 이를 클릭하고 원하는 메세지가 화면에 표시 되는지 테스트를 진행합니다.
이제는 nodejs가 아니고 브라우저 환경에서 테스팅을 진행하게 될때, 대표적으로 e2e 테스트 도구인 cypress를 사용할 수 있는데, cypress에서 제공하는 메소드로 실제 브라우저 상에 보이는 요소들을 컨트롤하여 사용자의 이벤트를 쉽게 테스트할 수 있게 됩니다.
cy
.get('#zoom-chat-textarea')
.type('가든 생일 축하해₩~~')
.type('{enter]');
실제 API를 사용해서 통신하는 것은 통합 테스트나 E2E 테스트 환경에서 주로 사용됩니다. 테스트 API서버를 직접 구축하거나 API Client를 모킹하게 되면 내가 원하는 응답을 받게 할 수 있기 때문에 다양한 상황을 테스트할 수 있다는 장점이 있습니다.
// 실제 테스트 코드
test('API 테스트', async () => {
const response = await fetch('https://garden.com', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
혼인신고: {
남편: '크리스',
아내: '피터',
},
}),
});
const 결과 = await response.json();
expect(결과).toEqual("행복하세요"); //Error: expected "망해라 ㄹㅇㅋㅋ"
});
;
////////////////////////////
// 이때 fetch같은 API 함수를 mocking해서 아래와 같이 작성합니다.
// fetch 모킹
beforeAll(() => { // 테스트가 실행되기 전에 한 번만 실행되도록 설정
jest.spyOn(global, 'fetch').mockImplementation(() =>
Promise.resolve({
json: () => Promise.resolve("행복하세요"),
})
);
});
afterAll(() => {
global.fetch.mockRestore(); // 테스트 후에 mock을 원래 상태로 복원
});
위의 코드에서 mocking이 되었으므로, response.json()의 값은 무조건 “행복하세요”를 보내게 됩니다 .
UI를 테스트하는 것에는 대표적으로 2개의 테스트 유형이 있는데요, 스냅샷 테스트와 시각적회귀 테스트가 있습니다.
UI 테스트를 요약하면 우리가 개발하면서 흔히 사용하는 storybook으로 테스트하는 것이라고 생각하면 됩니다 :)
추가적으로 프론트엔드 테스트는 테스트코드가 어디에서 실행되는지에 따라서 브라우저 환경과 node환경으로 구분지을 수 있습니다.
오늘은 프론트엔드 테스트를 본격적으로 도입하기 전, 테스트의 개념을 쉽게 알아보았습니다. 다음 포스트는 제가 직접 E2E 테스팅 도구를 채택하는 고민 과정이 담긴 글인데요, Cypress와 Playwright 중에 고민하고 최종으로 결정지은 과정을 담아서 가져올게요!
https://sejoung.github.io/2021/05/2021-05-07-solitary_test_vs_sociable_test/
믿고 보는 선댓 후감상