공부

실용적인 프론트엔드 테스트 전략 - 1

출처 meetup.toast.com/posts/174

 

실용적인 프론트엔드 테스트 전략 (1) : TOAST Meetup

실용적인 프론트엔드 테스트 전략 (1)

meetup.toast.com

 

 

개발자와 테스트

 

- 테스트의 정의 : 애플리케이션이 요구 사항에 맞게 동작하는지를 검증하는 행위

 


자동화 테스트의 중요성

 

- 개발자가 직접 버튼을 클릭하고 ui변경을 확하는 식의 테스트는 대부분 반복적인 작업이다. 반복된 작업을 매번 수동으로 진행하면 애플리케이션이 복잡해질수록 테스트에 대한 비용이 증가하게 된다. 곧 테스트를 소홀히 하게 되며, 이는 결국 애플리케이션의 품질 저하로 이어진다. 또한 코드를 수정할 때마다 매번 관련된 기능을 테스트해야 하는 부담 때문에 코드 개선을 망설이게 되고, 이는 코드의 품질 또한 저하시킨다.

 

- 반복된 테스트 작업을 코드로 작성해서 자동화한다면 테스트에 대한 비용이 줄고, 테스트가 누락되거나 잘못 검증하는 등의 실수도 방지할 수 있다. 또한 코드 수정에 대한 두려움이 없어져 적극적으로 리팩토링 등의 코드 개선을 할 수 있게 되고, 이는 곧 코드 품질의 향상으로 이어진다.

 


테스트의 기회비용

 

- 모든 테스트에 대해 자동화 테스트를 작성할 필요는 없다. 테스트 코드를 작성하고 유지보수 하는데 비용이 들기 때문이다. 그러므로 투입된 비용에 비해 얻는 효과가 적다면 차라리 수동으로 테스트하는 것이 더 낫다. 가끔 테스트 커버리지를 억지로 100%로 맞추려고 하거나, 중요한 로직이 없는 단순한 코드까지 과하게 테스트하는데, 이는 더 중요한 곳에 투자할 소중한 비용을 낭비하는 것이다.

 

- 기존에 작성된 테스트도 불필요하다고 생각되면 제거하는게 더 낫다. 테스트코드도 애플리케이션이 변화함에 따라 계속해서 관리해 주어야 하기 때문이다.


좋은 테스트의 조건

 

- 실행 속도가 빨라야 한다.

테스트의 실행속도가 빠르다는 것은 코드를 수정할 때마다 빠른 피드백을 받을 수 있다는 의미이고, 이는 개발 속도를 빠르게 하고, 테스트를 더 자주 실행할 수 있도록 한다.

 

- 내부 구현 변경 시 깨지지 않아야한다.

이 말은 "인터페이스를 기준으로 테스트를 작성하라" 또는 "구현 종속적인 테스트를 작성하지 말라"는 지침과 같은 맥락이다. 좀 더 넓은 관점에서는 테스트의 단위를 너무 작게 쪼개는 경우도 해당된다. 작은 리팩토링에도 테스트가 깨진다면 코드를 개선할 때 믿고 의지할 수 없을 뿐 아니라, 오히려 테스트를 수정하는 비용을 발생시켜 코드 개선을 방해하는 결과를 낳게 한다.

(내부 구현 변경시 깨지는게 어떤 경우인지 잘 모르겠다)

 

- 버그를 검출할 수 있어야 한다.

잘못된 코드를 검증하는 테스트는 실패해야한다. 테스트가 기대하는 결과를 구체적으로 명시하지 않거나 예상 가능한 시나리오를 모두 검증하지 않으면 제품 코드에 있는 버그를 발견하지 못할 수 있다. 테스트 명세는 구체적이어야 하며, 모의 객체의 사용은 최대한 지양하는 것이 좋다.

 

- 테스트의 결과가 안정적이어야 한다.

어제 성공한 테스트가 오늘은 실패하거나, 특정기기에서만 성공하는 테스트는 신뢰할 수 없다. 테스트는 외부 환경의 영향을 최소화해서 언제 어디서 실행해도 동일한 결과를 보장해야 한다.

 

- 의도가 명확히 드러나야 한다.

제품코드와 같이 테스트코드도 기계가 아닌 사람이 읽기 좋은 코드가 되어야 한다. 테스트코드를 보고 한 눈에 어떤 내용을 테스트하는지를 파악할 수 있어야한다. 그렇지 않으면 추후에 해당 코드를 수정하거나 제거하기가 어려워져 관리 비용이 늘어나게 된다.

 


테스트 전략의 중요성

 

- 위에서 설명한 좋은 테스트의 요소를 모두 만족하는것은 어렵다. 각각의 요소들이 서로 상충되는 경우가 있다. 예를들어 테스트를 작은 단위로 작성하면 비교적 실행 속도가 빠르고 모든 시나리오를 검증하는 것이 쉽지만, 작은 단위의 변경에도 테스트가 깨지게 되어 유지보수 비용이 증가하고, 모의 객체의 사용이 늘어나서 버그를 검출하기 어려워진다.

 

- 모든 요소를 100% 만족하는 테스트를 작성하는 것은 사실상 불가능하다. 그래서 전략이 필요하다.

 


테스트 도구의 중요성

 

- 프론트엔드 테스트의 전략을 세울 때는 도구의 역할 또한 아주 중요하다. 기존 End to End 도구를 사용한 테스트는 사용자의 관점에서 테스트할 수 있어 내부 구현에 영향을 거의 받지 않지만, 테스트 코드가 복잡하고 실행이 느리며 결과가 안정적이지 않다는 단점이 있었다. 하지만 테스트 코드가 복잡하고 실행이 느리며 결과가 안정적이지 않다는 단점이 있었다. 최신 E2E 도구인 Cypress를 사용하면 기존 E2E 테스트의 장점은 유지하면서도 직관적이고 빠르고 안정적인 테스트를 작성할 수 있다. 즉 테스트의 도구 발전이 더 나은 테스트 전략을 세울 수 있도록 도와준다.

 


예제 : 할일 관리 애플리케이션

 

github.com/tastejs/todomvc

 

tastejs/todomvc

Helping you select an MV* framework - Todo apps for React.js, Ember.js, Angular, and many more - tastejs/todomvc

github.com

todomvc에서 examples 디렉토리의 -> react-hooks를 설치

 


위 todoList의 실행흐름

 

1. 기본 UI 출력

2. api 서버에 "할 일 목록"을 요청한 다음 응답 데이터를 리덕스 스토어에 저장한다.

3. 저장된 스토어의 값에 따라 할 일 목록을 UI로 표시한다.

4. 사용자가 인풋 상자를 클릭한 다음 "낮잠 자기"라고 입력 후 엔터키를 입력한다.

5. api 서버에 "할 일 추가"를 "낮잠 자기"라는 데이터와 함께 요청한다.

6. 요청이 성공하면 리덕스 스토어의 할 일 목록에 "낮잠 자기"를 추가한다.

7. 저장된 스토어의 값에 따라 UI를 갱신한다.

 

- 1, 3, 7은 모습(뷰)에 해당하고 2,4,5,6은 현재 상태를 변경하는 모델에 해당한다. 이러한 분류를 하는 이유는 각 부분을 테스트할 때 다른 전략을 사용하기 때문이다. 특히 시각적 요소에 대한 테스트는 코드를 이용해 자동화하기가 어렵기 때문에 애플리케이션의 상태변경을 시각적 요소와 같이 테스트하면 작성과 유지보수에 많은 비용이 든다. 그러므로 애플리케이션을 설계할 때 이 둘을 분리해서 테스트할 수 있는 구조를 만드는 것이 중요하다.

 


시각적 요소의 테스트

 

HTML 비교하기

 

- MVC 패턴에서 뷰를 테스트할때 HTML의 구조를 테스트하는 경우가 많다. 사실 시각적 표현을 결정하는 요소가 HTML과 CSS인데, CSS에 정의된 스타일은 보통 동적으로 제어되는 일이 드물기 때문이다.

 

- 가장 단순한 형태의 검증은 예상되는 HTML 구조를 문자열 형태로 비교하는 것이다.

 

import React from 'react';
import { render } from 'react-dom';
import prettyHTML from 'diffable-html';
import { Header } from '../components/header';

it('Header component - HTML', () => {
  const el = document.createElement('div');
  render(<Header />, el);

  const outputHTML = prettyHTML(`
    <header class="header">
      <h1>todos</h1>
      <input class="new-todo" placeholder="What needs to be done?" value="" />
    </header>
  `);

  expect(prettyHTML(el.innerHTML)).toBe(outputHTML);
});

- differable-html 라이브러리는 HTML 문자열을 비교할 때 다른 부분을 더 파악하기 쉽도록 기존 문자열을 특정한 포맷에 맞게 변경해준다.

 

 


스탭샷 테스트(HTML)

 

- 위와 같이 HTML 문자열을 비교할 때 예상되는 HTML 문자열을 개발자가 미리 작성하기는 사실 쉽지 않다. 더 크고 복잡해진 컴포넌트를 테스트해야한다면 직접 HTML을 상상해서 쓰는건 힘들고 보통 실제 컴포넌트가 생성한 HTML을 복사해서 테스트코드에 붙여넣는다.

 

- 이런 방식은 예상되는 결과를 먼저 작성하는 일반적인 테스트 주도 개발 방식과 다르다. 코드 작성시 빠른 피드백을 주어 개발 속도를 향상시키는 기능은 거의 없고, 사실상 회귀 테스트의 역할만을 한다고 볼 수 있다. 또한 코드를 수정할 때마다 매번 콘솔에서 결과값을 복사해서 코드로 붙여넣는 과정도 번거롭다. 그래서 최근에는 Jest등의 테스트 도구에서 지원하는 스냅샷 테스트를 사용해서 이런 문제를 해결한다.

 

- 스냅샷 테스트는 예상되는 데이터를 직접 코드로 작성하지 않고, 처음 실행된 결과물을 파일로 저장해두는 방식을 쓴다. 그 후론 테스트를 실행할 때마다 기존에 저장된 파일의 내용과 현재 실행된 결과를 비교한다. 위의 방식과 마찬가지로 회귀 테스트의 역할만 하지만, 예상 결과를 직접 코드로 관리하는 번거로움은 없애준다.

 

import React from 'react';
import { render } from 'react-dom';
import prettyHTML from 'diffable-html';
import { Header } from '../components/header';

it('Header component - Snapshot', () => {
  const el = document.createElement('div');
  render(<Header />, el);

  expect(el.innerHTML).toMatchSnapshot();
});

 

사용 예시

 

 

exports[`Header component - Snapshot 1`] = `
"
<header class=\\"header\\">
  <h1>
    todos
  </h1>
  <input class=\\"new-todo\\"
         placeholder=\\"What needs to be done?\\"
         value
  >
</header>
"
`;

스냅샷은 위와 같이 저장된다.

 

 


스냅샷 테스트 (가상 DOM)

 

- 사실 리액트 컴포넌트가 반환하는 것은 실제 HTML이 아닌 리액트 엘리먼트라고 하는 가상의 DOM구조이다. 실제 HTML의 생성 및 변경은 react-dom 모듈의 역할이기 때문에 엄밀히 말하자면 특정 컴포넌트에 대한 테스트의 범위에 포함되지 않는다. 그래서 리액트 컴포넌트를 테스트할 때는 컴포넌트가 반환하는 리액트 엘리먼트의 트리 구조를 테스트하는 경우가 많다.

 

- 리액트에서는 컴포넌트 테스트를 돕기 위해 react-test-renderer 라이브러리를 제공하고 있으며, 이 라이브러리를 사용하면 컴포넌트를 실제로 렌더링 할 필요 없이 컴포넌트의 동작을 테스트할 수 있다.

 

import React from 'react';
import renderer from 'react-test-renderer';
import { Header } from '../components/header';

it('Header component - Snapshot', () => {
  const tree = renderer.create(<Header />).toJSON();

  expect(tree).toMatchSnapshot();
});

 

 


HTML 구조 비교의 문제점

 

1. 구현 종속적인 테스트

좋은 테스트의 조건 중 하나는 "내부 구현 변경 시 깨지지 않아야 한다"이다. 즉 테스트를 할 때는 결과값을 "어떻게' 만들어내는지가 아니라 결과물이 "무엇"인지를 검증해야 한다. 하지만 HTML은 시각적 요소의 결과물이 아닌 시각적 요소를 표현하기 위한 내부 구현 방식, 즉 "어떻게"에 가깝다. 시각적 요소의 최종 결과물은 HTML 구조가 아닌 화면에 표시되는 이미지이기 때문이다.

이러한 구현 종속적인 테스트는 작은 변경에도 깨지기 쉬워 관리 비용을 증가시킨다. header 태그 대신 div태그를 사용하거나, 클래스이름은 new-todo 에서 add-todo로 변경하면 실제 결과 이미지에 변화가 없어도 테스트가 깨진다. HTML이나 CSS를 고칠 때 마다 테스트 코드를 갱신시켜야 하며, 이로 인해 개발 속도가 오히려 느려진다.

 

2. 의도가 드러나지 않는 테스트

HTML 구조는 실제 화면에 그려지는 이미지를 그대로 나타내지 않는다. 비록 CSS까지 함께 테스트한다 해도 복잡한 HTML과 CSS의 코드를 보고 실제의 이미지를 머릿속에 정확하게 그려내는 것은 사실상 불가능하다. 결국 브라우저에 표시된 결과를 실제 눈으로 확인한 다음에야, 지금 생성된 HTML이 실제 원하던 결과라는 것을 확신할 수 있다.

이러한 테스트 코드는 고나리가 어렵다. 다른 개발자나, 심지어 자신조차 나중에 코드를 볼 때 어떤 의도를 갖고 있는지를 파악하기가 어렵다. 결국, 테스트가 깨질 때마다 별다른 생각없이 실행 결과를 복사해서 붙여넣거나 스냅샷을 갱신하게 되고, 이는 테스트의 신뢰도와 효과를 떨어뜨린다.

 


시각적 테스트 자동화의 어려움

 

- 결국 시각적 요소는 실제 화면에 표시되는 이미지를 픽셀 단위로 비교하지 않는 이상 효과적인 테스트라고 하기 어렵다.

 

- 새로운 대안 : 스토리북 (storybook.js.org/)

 

 


스토리북 : UI 개발 환경

 

- 공식 홈페이지에서는 스토리북을 "UI 개발 환경" 이라고 소개한다. 테스트 도구 보다는 UI개발을 위한 더 나은 환경을 제공해주는 도구에 가깝다. 일종의 컴포넌트 갤러리라고 할 수 있는데, 애플리케이션에서 사용되는 모든 컴포넌트의 조합을 페이지별로 등록해 놓고 편리하게 눈으로 확인할 수 있도록 네비게이션을 제공한다.

image.toast.com/aaaadh/real/2019/techblog/2.png

 

- 글쓴이가 생각하는 최고의 시각적 검증 도구는 "개발자의 눈"이다. 결과물을 시각적으로 검증하는 행위는 자동화하지 않고, 사람의 눈에 맞기되, 스토리북은 검증을 위한 준비작업을 최대한 자동화한다.

 

 

'공부' 카테고리의 다른 글

실용적인 프론트엔드 전략 3  (0) 2020.12.10
실용적인 프론트엔드 테스트 전략 - 2  (0) 2020.12.09
lazy loading  (0) 2020.12.08
프레젠테이션/컨테이너 컴포넌트  (0) 2020.12.08
타입스크립트  (0) 2020.12.07