[Week 1-2] 테스트코드를 활용한 효율적인 리팩토링
2-1) 무엇을 테스트할 것인가?
오늘 챌린지에선 리펙터링시 테스트 코드를 활용할 것을 소개하고 있다.
개념으로만 테스트 주도 개발(TDD)이 중요하다고만 알고 있었는데 특히 프론트 엔드에서 무엇을 고려하여 진행해야 하는지 알 수 있었다.
UI 테스트는 진행하지 않는다.
테스트를 계획할 때 무엇을 테스트 할 것인지 명확하게 파악을 해야 하는데 UI 테스트는 중요하지 않다.
컴포넌트 렌더링 순서를 확인하거나, 태그와 요소들의 간격 등은 굳이 테스트할 필요가 없다.
요구사항의 "사용자 시나리오"에 집중해야 한다.
UI, Style 부분보다 "기능"에 더 중점을 두고 테스트를 해야한다. 회원가입 form에서 input이 제대로 되는지, 혹은 필수 항목들을 모두 작성하였을 때 회원가입 버튼이 활성화되는지 "기능"이 input의 padding, margin보다 우선시되어야 한다.
결국 테스트시 UI/UX보단 기능에 중점을 두어야 한다는 것이다.
input에 입력할 때 value가 정상적으로 변하는지
회원 가입 버튼을 눌렀을 때 이벤트 핸들러가 동작하는지
서버와 주고 받을 수 있도록 HTTP 요청이 잘 이루어지는지
테스트를 위한 테스트를 하지 말 것
테스트의 주목적은 기능이 정확하게 작동하는지 확인하고, 오류를 찾아내는 것이기 때문에 이 목적을 잃어버리지 않도록 주의해야 한다.
테스트 자체에 너무 몰입하여 본래의 목적을 벗어나 비효율적/불필요한 테스트를 지양해야 한다.
이 정도면 통합테스트 아닌가?
- 프론트 엔드는 유닛 테스트와 통합 테스트의 경계가 애매하다.
가장 작은 단위, 함수나 메서드 하나하나를 독립적으로 테스트하는 유닛 테스트와, 회원가입을 위한 이벤트 핸들러, 버튼 작동 함수, HTTP 요청 등 복합적으로 검증해야 하는 통합 테스트.
이 테스트의 경우 '회원가입'을 위한 유닛 테스트인지 혹은 여러 기능이 함께 잘 작동하는지 확인하는 통합 테스트인지 구분이 애매할 수 있다.
프론트 엔드에선 여러 유닛이 서로 상호작용하는 방식 때문에 유닛 테스트와 통합 테스트의 경계가 애매해질 수 있다. 따라서 상황에 따라, 팀의 판단에 따라 테스트를 진행해야 한다.
2-2) 어떻게 테스트할 것인가?
Given - When - Then
True, False 리턴
예상 결괏값과 실제 결과값 비교
테스트는 한 번에 하나의 테스트만
Given - When - Then은 테스트를 작성할 때 많이 사용되는 패턴이다.
Given : 테스트의 초기 상태를 설정
When : 테스트하려는 동작을 실행
Then : 결과를 검증
describe("LoginButton Component", () => {
// Given
// 로그인 버튼 컴포넌트를 렌더링하고, 클릭 시 호출되는 함수를 mock 함수로 설정
it("calls onClick function when the button is clicked", () => {
const mockOnClick = jest.fn(); // mock function for onClick
const wrapper = shallow(<LoginButton onClick={mockOnClick} />); // render the button with mock onClick function
// When
// 버튼의 클릭 이벤트를 시뮬레이션
wrapper.find("button").simulate("click"); // simulate a click event
// Then
// mock 함수가 호출되었는지 확인하여 클릭 이벤트가 정상적으로 발생했는지 테스트
expect(mockOnClick).toHaveBeenCalled(); // check if the mock function has been called
});
});
그리고 테스트 함수의 테스트 성공 여부를 확인하기 위해 불린 값을 리턴하거나, 예상값과 결괏값을 비교하고, 하나의 테스트에선 한 가지 기능만 테스트해야 다른 테스트의 영향을 받지 않아 신뢰성을 떨어뜨리지 않을 수 있다.
단위테스트
단위 테스트는 가장 작은 단위를 테스트하는 것이므로 함수, 메서드, 컴포넌트들을 독립적으로 테스트한다.
그리고 JavaScript의 테스트를 위한 라이브러리인 Jest를 많이 사용한다.
Jest
By ensuring your tests have unique global state, Jest can reliably run tests in parallel. To make things quick, Jest runs previously failed tests first and re-organizes runs based on how long test files take.
jestjs.io
// add.js
function add(a, b) {
return a + b;
}
module.exports = add;
// add.test.js
const add = require('./add');
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
입력받은 두 수의 합을 return 하는 함수를 테스트하기 위한 Jest 예문, expect로 실제 결괏값과 toBe의 예상 결괏값을 비교한다.
통합테스트
통합 테스트는 여러 컴포넌트나 모듈이 함께 작동하는 경우를 테스트할 때 사용한다.
컴포넌트 간의 상호작용이 정상적으로 되는지 확인하기 위한 단위 테스트보다 더 넓은 범위의 테스트이다.
// user.js
let users = {};
function createUser(id, name) {
users[id] = { id, name };
return users[id];
}
function getUser(id) {
return users[id];
}
module.exports = { createUser, getUser };
// user.test.js
const { createUser, getUser } = require('./user');
describe('User Integration Test', () => {
test('Create a user and then retrieve it', () => {
// Given
const id = '1';
const name = 'John Doe';
// When
const newUser = createUser(id, name);
const retrievedUser = getUser(id);
// Then
expect(newUser).toEqual(retrievedUser);
});
});
https://sjquant.tistory.com/70
정말로 단위 테스트를 통합 테스트보다 많이 작성해야 할까?
테스트 피라미드 통합 테스트를 줄이고, 단위 테스트를 많이 작성해야 한다는 말 들어보셨나요? 이러한 주장을 하면 보통 리팩터링의 저자 마틴 파울러의 테스트 피라미드를 예시로 사용합니다
sjquant.tistory.com
E2E 테스트
엔드 투 엔드(E2E) 테스트는 유저의 관점에서 전체 플로우를 테스트하는 방법이다.
쇼핑몰 페이지에 접속할 때부터 (회원가입 - 로그인 - 상품 검색 - 장바구니 추가 - 주문 - 결제) 모든 과정들에 오류가 없는지 확인해야 한다. 특히 유저가 경험할 수 있는 모든 시나리오를 검증해야 한다.
https://fe-developers.kakaoent.com/2023/230209-e2e/
E2E 테스트 도입 경험기 | 카카오엔터테인먼트 FE 기술블로그
방경민(kai) 사용자들에게 보이는 부분을 개발한다는 데서 프론트엔드 개발자의 매력을 듬뿍 느끼고 있습니다.
fe-developers.kakaoent.com
모든 경우의 수를 테스트하기 위해선 테스트가 복잡하기 때문에 빠른 대응이 어렵고, 시간이 많이 소요된다.
그래서 주요한 플로우를 테스트하는 데에 초점을 맞추고, 세부적인 부분은 단위 테스트나 통합 테스트로 검증하는 것으로 단점을 보완할 수 있다.
2-3) 프론트 엔드 TDD
가능하긴 하다.
- 디자인 시스템이 갖추어져 있다면 매우 수월함
- Atomic component 조합만 충분히 해도 가능
하지만 꼭 해야 하나?
- 애자일 조직 특성에 맞지 않음
- TDD를 해야만 검증이 되는 건 아니다
디자인이 완성되어 있다면 테스트 시 컴포넌트의 로직을 쉽게 파악할 수 있으므로 쉽게 할 수 있다.
Atomic component는 웹페이지를 만드는 가장 작은 단위인 컴포넌트를 말한다. 이러한 Atomic 컴포넌트들을 조합하여 복잡한 UI를 구성하는 방식을 말하는데, 이를 'Atomic Design'이라고도 한다. 예를 들어, 버튼, 입력 필드, 체크박스 등이 Atomic component가 될 수 있다.
TDD(Test-Driven Development)란, 테스트 코드를 먼저 작성하고 그 테스트를 통과하는 코드를 작성하는 개발 방법론이기 때문에 Atomic component를 사용하면 각 컴포넌트가 독립적으로 동작하여 각 컴포넌트에 대해 독립적인 테스트를 작성하고, 이를 기반으로 실제 코드를 작성하는 TDD를 적용할 수 있다.
마지막으로, 테스트를 위한 속성(data-testid, data-cy 등)을 잘 설정해 주면, 테스트 코드에서 해당 컴포넌트를 쉽게 찾아 테스트를 수행할 수 있다.
하지만, 빠른 피드백과 요청사항의 변화에 유연하게 대응하는 것을 중요하게 생각하는 애자일 방법론에선 테스트 코드를 먼저 작성해야 하는 TDD 방법론이 항상 효율적이지는 않을 수 있다.
https://oliveyoung.tech/blog/2023-10-30/wcare-tdd-development/
W CARE 서비스 프론트엔드를 TDD로 개발해본 후기 | 올리브영 테크블로그
TDD로 개발한 구체적인 방법 및 느낀 점에 대해서 소개합니다.
oliveyoung.tech
https://www.youtube.com/watch?v=L1dtkLeIz-M&t=1764s
📝 오늘의 3줄 요약
1. 테스트 시 중요한 것은 "눈코입의 간격"보다 "눈코입의 기능"이다.
2. 작은 컴포넌트의 단위 테스트와 서로 연결되어 있는 하나의 기능을 테스트하는 통합 테스트
3. TDD를 적용한다고 무조건 좋은 것은 아니므로 상황에 맞게 적절히 사용하자.