2020 NHN FE Test 교육

dongwon kim
39 min readMar 8, 2020

--

프런트앤드 테스팅(NHN 김동우) 교육을 듣고 느낀 후기를 정리하였습니다.
TOAST FORWARD용 자료이고 NHN의 지적 재산입니다.

NHN 김동우, "프런트엔드 테스팅",  https://nhnent.dooray.com/share/posts/9jRYF1fxRwiCvwi6VM0VgA

1월 15일 날 진행했던 교육으로 판교에 있는 NHN으로 가서 교육을 수강했습니다. 이론과 실습의 적절한 조화여서 여지껏 들었던 테스트 교육 중 최고였습니다.

좀 더 깔끔하게 정리한 notion 페이지

Speaker : FE 개발랩 김동우 님 (dongwoo.kim@nhn.com)

frontend-testing-source.zip
frontend-testing-solution.zip

Front-end Testing

FE 테스팅은 자료가 없는 편. 어떤 것이 좋은 테스팅인지 알 수가 없다. 우리 팀에서 어떻게 해야 할지 고민을 많이 했고 경험해왔던 것들을 축적해서 외부에 오픈하는 중.

Jest로 할것이고 현재 가장 많이 쓰이는 도구이다.
FE에서 가장 많이 쓰임. Facebook에서 지원함.

What is Test?

내가 만든 것(Application)이 고객의 요구사항에 부합하는지 검증하는 행위.

Example of Test

  • DB에 데이터를 입력하는 API를 개발 -> API 호출 -> DB값 검증
  • 디자인 시안에 맞게 HTML/CSS를 작성 -> 브라우저에서 실제 렌더링된 결과를 확인
  • 새로운 기능을 추가하기 위해 기존 모듈을 리팩토링 -> 영향을 받는 다른 모듈의 실행 결과를 확인
  • 버그를 수정하기 위해 기존 함수를 수정 -> 버그가 수정 확인 & 영향을 받는 다른 모듈의 실행 결과를 확인
  • 개발 환경에서 테스트된 어플리케이션을 리얼 환경에 배포 -> 배포 과정에서 발생한 문제가 없는지 확인

Why do write Test?

  • 사람이 수행해야 하는 반복된 테스트 자동화(부화 테스트).
  • 사람보다 더 신뢰할 수 있음.
  • 사람보다 더 빠르기 때문에 시간 절약 가능.

Weekness

  • 감각적인 요소(시각, 청각) 등 사용자 경험과 관련된 문제를 찾아낼 수 없다.
  • 실제 환경에서 벌어지는 다양한 환경을 자동화 하기는 어렵다.(네트워크, 디바이스 관련)

⇒ 내가 보기에는 위의 약점(완벽한 테스트가 아니다)이 너무 critical하기에 굳이 또 한번 작성해야 하는 가란 생각이 든다. 리소스와 기회비용의 낭비가 되는 듯하다. 개발하기 위해서 시간도 중요한 요소이기에.

개발자가 테스트를 작성해야하는 이유

제품 품질

  • 프로그램 퀄리티에 대한 책임이 있음, qa 넘기기 전 검수는 개발자의 몫
  • 자동화된 테스트를 작성해 두지 않으면 어플리케이션이 복잡해질 수록 테스트 비용이 증가.

코드 품질

  • 코드 품질을 관리하기 위해선 리펙토링 등의 개선 작업이 필요.
  • 이 과정에서 기존에 잘 동작하던 프로그램을 망칠 수 있기 때문에 적극적으로 코드를 개선하지 않게 됨.
  • 신뢰할 수 있는 자동화된 테스트가 있으면 적극적으로 코드를 개선할 수 있음.
  • 두려움 ⇒ 자신감

TDD(Test Driven Development)

  • 테스트 주도 개발로 테스트를 먼저 작성한 후 구현 코드를 작성하는 개발 방법론
  • 실제 구현 코드를 사용하는 입장에서 먼저 생각할 수 있기 때문에 디자인에 도움을 줄 수 있음.
  • TDD가 항상 좋은 디자인을 보장하진 않음. ⇒ 사고를 도와주는 방법론
  • TDD ≠ 자동화 테스트(TDD≠단위 테스트)
  • TDD가 적절하지 않은 프로그램도 있음(UI개발 등)
  • When TDD Doesn’t work — Uncle Bob

now we have two places where TDD is impractical or inappropriate. The physical boundary, and the layer just in front of that boundary that requires human interaction to fiddle with the results.

테스트의 종류

범위에 따라

  1. 단위(Unit) 테스트
  2. 통합(Intergration) 테스트
  3. E2E(End to End) 테스트
    1. 기능(Functional) 테스트
    2. 시스템(System) 테스트
    3. UI(User Interface) 테스트

로 분류되고

그외

  • 회귀(Regression) 테스트
  • 성능(Performance) 테스트

로 분류할 수 있다.

단위 테스트

특징

  • 모듈 (함수/클래스) 단위의 테스트
  • 테스트할 부분의 코드를 다른 시스템으로부터 분리 시킨 채 테스트
  • 작성 비용이 적게 들고 실행 속도가 빠름.
  • 실패했을 때 문제가 생긴 부분을 비교적 정확하게 파악할 수 있음.
  • 경우에 따라 한 두개의 단위를 모아서 하나의 단위로 취급하기도 함.

Sociable vs Solitary

Sociable Tests : 의존성이 있는 다른 코드들과 함께 테스트.

Solitary Tests : 테스트 더블을 이용해 완벽하게 분리시킨 채 테스트.

통합 테스트

특징

  • 주로 단위 테스트보다 큰 범위의 테스트를 의미
  • 개별 모듈(함수,클래스)들이 연결되어 제대로 상호 작용하는지를 테스트
  • 단위 테스트에 비해 작성이 어렵고 실행 속도가 느림.
  • 단위 테스트에 비해 실패 시 문제가 생긴 부분을 정확히 파악하기 어려움

Narrow vs Broad

  • 좁은 통합 테스트 : 테스트 더블을 이용해 외부 서비스를 실제 구동하지 않고 테스트
  • 넓은 통합 테스트 : 의존성이 있는 모든 외부 서비스를 사용하여 테스트

잘게 쪼갤수록 테스트해야하는 단위가 넓어진다.

여담 : Although I prefer to focus the definition on the interaction of separately built modules, I do occasionally see “integration test” used to mean anything bigger than a unit test. And for some users of solitary unit tests, I’ve seen them describe sociable unit tests as “integration tests”.

E2E테스트

  • FE 개발자가 잘 하진않고 Qa가 주로 함.
  • 실제 사용자가 사용하는 것과 같은 조건에서 전체 시스템을 테스트
  • API 서버, DB 등의 외부 서비스들을 모두 사용하여 통합된 시스템을 테스트
  • 단위/통합 테스트에 비해 작성이 어렵고 실행속도가 가장 느림.
  • 문제가 생긴 부분을 정확히 파악하기가 가장 어려움. ⇒ 가장 높은 단계의 테스트 이므로
  • 기능 테스트와 비슷한 의미로 사용 됨.

(G)UI 테스트

  • 백엔드 시스템을 Mocking한 채 UI만 테스트할 수도 있다.

자바스크립트 테스팅 환경

테스트 러너

  • 테스트 파일을 읽어들여 실행하고, 결과를 출력
  • 파일이 변경된 경우 자동으로 실행해주는 watch 등의 기능 제공
  • Reporter를 지정해서 원하는 형태로 결과를 출력
  • Node 환경에서 실행 : Mocha, Jest, AVA
  • 브라우저 환경에서 실행 : Karma

카르마보다 Node가 조금 더 빠르다.

테스트 프레임워크

  • 사용자가 테스트 코드를 작성할 수 있는 기반을 제공
  • 프레임 워크에 의해 제공된 함수를 이용해서 테스트 코드를 작성
    ⇒ 프레임 워크가 자동으로 테스트를 실행하고 결과를 수집해서 출력.
  • 테스트 Spec 들을 그룹핑하거나 공통 사전 작업 등을 처리해 줌.
  • Mocha, Jasmine(Jest), AVA
  • 예제 (Jasmine)
describe(‘calcuations’, ()=>{ 
let a,b;
beforeEach(()=>{
a = 10;
b = 20;
});
it('sum two number', ()=>{
expect(a+b).toBe(30);
});
it('multiply two number', ()=>{
expect(a*b).toBe(200);
});
}

단언(Assertion)

  • 다양한 스타일의 Assertion을 사용할 수 있도록 API를 제공
  • 대부분 테스트 프레임워크에 포함된 형태로 사용(Jasmine, Jest, AVA)
  • Mocha의 경우 별도의 라이브러리인 Chai를 사용
  • 예제 (Jasmine)
expect(obj).not.toBeNull();
expect(obj).toEqual({
name: ‘Kim’,
age: 30
});
expect(result).toBe(true);
expect(result).toBeTruthy();
expect(spy).toHaveBeenCalled();

⇒ 위의 코드랑 차이점은 한 줄로 단언해서 결과를 확인한다는 점이다.

테스트 더블

테스트를 하기 위해 실제 코드 대신에 사용하는 객체/모듈/함수로 Dummy, Stub, Mock, Spy 등을 통칭

테스트 더블을 작성하기 쉽게 도와주는 라이브러리 ⇒ Sinon, Jasmine, Jest

브라우저 환경 vs Node.js 환경

브라우저에서 테스트 실행

  • 실제 브라우저 환경에서 테스트 코드를 실행
  • 브라우저의 모든 API를 사용해서 테스트 가능
  • 실제 브라우저를 실행해야 하기 때문에 번거로움(Headless 브라우저 사용) ⇒ Headless gui 없이 실행시키는 환경.
  • 테스트 파일 별로 별도의 브라우저에서 테스트 하기가 어려움(속도 문제)
  • 빈 페이지를 만들고 모든 스크립트 파일 및 CSS등을 include 해서 테스트(번들 과정 필요)
  • 브라우저 호환성 테스트 가능
  • 개발 시 : 빠른 피드백을 위해 Headless 브라우저를 사용
  • 빌드 시 : CI 서버 및 Webdriver와 연동하여 여러 개의 브라우저에서 테스트

Node.js에서 테스트 실행

  • Node.js 환경에서 테스트 코드를 실행(Mocha, Jest 등)
  • 브라우저에 비해서 가볍기 때문에 실행 속도가 빠름.
  • 개별 테스트 파일을 별도의 프로세스에서 실행할 수 없음(병렬 실행 가능)
  • 브라우저 API 대신 JSDom을 이용해서 테스트 ⇒ JSDom = 가짜 브라우저.
  • 실제 렌더링을 해 주지 않으므로, 렌더링 관련 테스트 불가능
  • 브라우저 호환성 테스트 불가능.

실습 : Jest 설치 및 사용

Jest

  • 페이스북에서 만든 JS 테스팅 라이브러리
  • 현재 페이스북 내의 모든 JS 테스트에 사용됨.
  • 테스트 러너, 구조화, 단언, 테스트 더블 등의 기능을 모두 포함.
  • Node 환경에서 JSDom을 이용해 테스트(브라우저 테스트 불가)
  • 테스트 병렬로 수행해서 속도를 높임.
  • Jasmine과 호환되는 단언 API 형식(처음엔 Jasmine 사용⇒ 현재 자체 구현)
  • Zero Configuration : 설정없이 간단하게 실행할 수 있음.

Why?

  • 단위, 통합 테스트를 실제 브라우저에서 실행해야 할 이유가 많이 사라짐.
    - Babel 등의 트랜스파일러가 보편화되고, 브라우저간 차이가 줄어듬.
  • 단위 테스트는 빠른 피드백이 중요 ⇒ 가볍고 빠른 Node 환경이 적합.
  • 개별 테스트를 별도의 프로세스에서 실행 ⇒ 안전한 테스트 환경
  • 모듈 Mockin, 스냅샷 테스팅
  • 가장 활발하게 발전하고 있고, 페이스북의 지원이 있음.
  • E2E 테스트 도구와 함께 사용하면 단점 보완 가능.

test / it

이름만 다르고 기능은 동일

test('test content', ()=>{
// test code
});
it('test content', ()=>{
// test code
});

describe

테스트를 구조화하기 위해 사용, 관련있는 테스트들끼리 그룹으로 묶어줌.

describe('모듈 A', () => {
test('테스트 1', () => {
// ...
});

test('테스트 2', () => {
// ...
});
});
describe('모듈 B', () => {
test('테스트 1', () => {
// ...
});

test('테스트 2', () => {
// ...
});
});

expect()

값을 검증하기 위한 다양한 matcher 들을 제공

toBe, toEqual

레퍼런스 체크(참조가 다른경우 안됨), 체크 값비교(내용이 똑같은지 비교)

위와 같이 테스트 할 수 있다.

테스트에 실패했을 경우

위와 같은 에러를 뿜는다. [4,3]을 기대했으나 2를 받은 상황.

실습 : TDD (add, swap)

Red — Green — Refactor

  1. Red : 실패하는 테스트 케이스를 작성
  2. Green : 해당 테스트 케이스가 성공하도록 코드를 작성
  3. Refactor : 기능을 유지한 채 내부 구조와 코드를 개선
// util.jsexport function add(a = 0, b = 0) {
return a + b;
}

export function swap(a) {
if (a.length !== 2) {
return a;
} else {
let newObj = [ ...a ];
newObj.unshift(newObj.pop());
return newObj;
}
}

// util.spec.js
import { add, swap } from '../src/util';

describe('add()', () => {
test('인자가 없으면 0을 반환한다.', () => {
expect(add()).toBe(0);
});
test('인자가 하나이면, 인자 그대로 반환한다.', () => {
expect(add(5)).toBe(5);
});
test('인자가 두 개이면 두 인자를 더한 결과를 반환한다', () => {
expect(add(3, 5)).toBe(8);
});
});
describe('swap()', () => {
test('배열의 인자가 두 개가 아닌 경우, 기존 배열을 그대로 반환한다.', () => {

const arr1 = [ 1 ], arr2 = [ 2, 4, 5 ];
expect(swap(arr2)).toEqual(arr2);
expect(swap(arr1)).toEqual(arr1);
});
test('배열 내의 두 요소의 순서를 바꾸어 새로운 배열을 반환한다.', () => {
expect(swap([ 3, 4 ])).toEqual([ 4, 3 ]);
});
test('변경된 배열은 기존 배열과 다른 새로운 배열이다.', () => {
const arr = [ 1, 2 ];
expect(swap(arr)).not.toBe(arr);
});
});

beforEach

test가 실행되기 전에 실행됨.

테스트를 위한 Setup 작업을 수행하는 코드.

afterEach

테스트가 끝난후에 실행됨

테스트 이후에 초기 상태를 복구하는 등의 코드를 작성.

Testing 전략

좋은 테스트란?

  1. 실행 속도가 빨라야 한다.
    - 빠른 피드백 -> 개발 속도를 빠르게 해 줌
    - 너무 느리면 테스트를 자주 실행하지 않게 됨
  2. 내부 구현 변경 시 실패하지 않아야 한다.
    - 리팩토링할 때 테스트가 깨진다면? -> 오히려 코드 개선을 방해
    - 구현 종속적인 테스트를 작성하지 않는다
    — 내부 구현을 모른채 테스트를 작성(BlackBox 테스팅)
    — 인터페이스를 기준으로 테스트를 작성한다.
    — 자주 변하는 로직과 변하지 않는 로직을 구분 (ex: 모델과 뷰를 분리)
  3. 버그를 검출할 수 있어야 한다.
    - 소스 코드에 버그가 있어도 검출하지 못한다면 잘못된 테스트
    - 테스트가 기대하는 결과를 구체적으로 명시하지 않으면 버그를 검출할 수 없음
    - 테스트 더블의 사용을 최소화한다. -> 과하게 사용하면 연결 과정에서의 버그를 검출할 수 없음
  4. 테스트의 결과가 안정적이어야 한다.
    - 특정 환경에서만 실패하거나, 간헐적으로 결과가 달라지는 테스트는 신뢰할 수가 없음
    - 외부 환경의 영향을 최소화해서 동일한 결과를 최대한 보장할 수 있어야 함
    - 현재 시간, 네트워크 상태, 외부 프로세스 등은 모의 객체나 별도의 도구를 사용해서 직접 조작할 수 있어야 함.
  5. 의도가 명확히 드러나야 한다.
  • 가독성 : -> “사람이 읽기 좋은 코드
    “기계가 읽기 좋은 코드”
  • 테스트 코드도 실제 코드와 동일한 기준으로 품질 관리를 해야 함
  • 테스트 코드를 보고 한 눈에 어떤 내용을 테스트하는지를 파악할 수 있어야 함.
  • 공통 로직, Fixture, Mock 등은 분리해서 관리

ROI (Return on investment)

테스트 코드 작성과 유지보수는 비용이다

  • 테스트가 없는 것보다는 있는 게 무조건 낫다?
  • 테스트는 많을 수록 좋다?
  • 불필요한 테스트나 잘못 짜여진 테스트는 차라리 없는게 나음

비용 대비 효과가 충분한가?

  • 테스트를 작성하는 비용에 비해 얻을 수 있는 효과가 더 큰가가 중요
  • 로직이 거의 없는(trivial) 코드는 따로 테스트하지 않아도 됨
    - 동어 반복적인 테스트를 피하자
  • 테스트 범위에 대한 조절이 필요 (단위 테스트 vs 통합 테스트 vs E2E 테스트)
    - 모든 모듈에 대해 단위 테스트를 작성하는 것은 비효율적
    - 모든 테스트 케이스를 E2E 테스트로만 검증하는 것도 비효율적

커버리지 100%를 목표로 하는 것은 비효율적

  • 라이브러리 등은 100% 커버리지 가능
  • 복잡한 어플리케이션의 경우 적절한 선을 잘 찾는 것이 중요

테스팅 도구와 테스팅 방법론은 아직 성숙한 상태가 아님

  • 특정 방법론이나 도구에 집착하지 말 것
  • 발전하는 테스팅 도구들을 눈여겨 볼 필요도 있음

테스트는 가능한 한 적게 사용하는 것이 Best!
⇒ 많으면 낭비일수도 있다.

스냅샷

기능 이전의 테스트와 같은 값을 가지는 지 비교하여 확인

테스팅 전략의 중요성

  • 좋은 테스트의 요소 중 여러 개를 동시에 만족하기는 어려움 (서로 상충됨)
    예) 테스트 단위가 작으면
    - 장점: 실행속도가 빠르고 엣지 케이스 검증이 쉬움
    - 단점: 작은 단위의 변화에도 깨짐, 모의 객체 사용이 늘어 버그 검출이 어려워짐
  • 프로젝트의 특성에 따라 더 중요한 가치를 판단해서 전략을 세워야 함
  • 테스팅 ROI를 고려해서 가장 효율적인 전략을 세우는 것이 필요

프런트엔드 테스트

프런트엔드의 구성 요소

시각적(visual) 표현

  • 화면에 표시되는 비주얼 요소를 디자인 요구사항에 맞게 구현
  • 레이아웃 / 색상 / 폰트 / 이미지 / 애니메이션 등
  • 주로 HTML(DOM) / CSS 에 의해 결정

사용자 입력 처리

  • 사용자에 의한 마우스/키보드 입력 등을 요구사항에 맞게 처리
  • 주로 DOM에 바인딩된 이벤트 핸들러에 의해 처리

어플리케이션 상태 관리

  • 사용자 입력 등에 의해 변경되는 어플리케이션의 상태를 관리
  • Routing / 팝업 표시-숨김 / 읽기-편집 모드 변경 / 에러 메시지 표시
  • 주로 순수 자바스크립트에 의해 처리

서버와의 통신

  • REST API / Socket 등으로 서버와 통신하며 어플리케이션 상태를 동기화
  • 주로 브라우저 API 혹은 라이브러리를 사용해서 비동기 로직을 수행

프런트엔드 요소별 테스트 전략

시각적 표현

  • 실제 화면을 픽셀 단위로 테스트 -> 눈으로 직접 확인 or 자동 스크린샷 테스트
  • HTML 구조를 테스트 -> HTML 구조를 직접 입력 or 스냅샷 테스트
  • ~특정 DOM 요소의 상태만 테스트 (버튼의 상태 / 텍스트) -> 시각적 테스트 아님~

사용자 입력 처리

  • 자바스크립트 API를 사용한 이벤트 시뮬레이션
  • 라이브러리(jquery, React)를 이용한 이벤트 시뮬레이션
  • E2E 도구를 이용한 이벤트 시뮬레이션

서버와의 통신

  • 실제 API 서버를 이용 -> 통합/E2E 테스트
  • Ajax 통신 모듈을 Mocking / 가상 API 서버를 구축 -> 단위/통합 테스트
  • 서비스 레이어를 분리해서 Mocking — 단위/통합 테스트

어플리케이션 상태 관리

  • 어플리케이션의 상태를 관리하는 레이어만 분리해서 테스트 -> 단위 테스트
  • 상태와 바인딩 된 DOM 요소의 상태를 테스트 -> 통합 테스트

프런트엔드 테스트 전략: 고려할 점

어플리케이션의 종류

  • 비주얼 요소가 중요한가 (예: 차트)
  • 모든 브라우저에서 테스트해야 하는가 (예: 에디터)

어플리케이션의 규모 및 복잡성

  • 간단한 라이브러리 (예: 데이트 피커)
  • 복잡한 라이브러리 (예: 그리드)
  • 복잡한 웹 서비스 (예: 두레이, 토스트 파일)

팀 구성

  • 별도의 QA팀이 있는가
  • 서버-클라이언트를 모두 통제할 수 있는가

이벤트 시뮬레이션 방식

순수 자바스크립트

  • createEvent() 혹은 CustomEvent 생성자를 이용해서 이벤트를 생성
  • dispatchEvent() 를 이용해 이벤트 발생
  • 시뮬레이션이 어려운 이벤트 (mousemove, mouseover 등) 는 테스트 불가

라이브러리/프레임워크

  • jQuery 등의 DOM 라이브러리 사용시 trigger() 등을 통해 간단하게 시뮬레이션 가능
  • React는 자체적인 이벤트 시스템을 갖기 때문에 React 에서 제공하는 이벤트 시뮬레이션 함수를 사용
  • Angular, Vue 등도 이벤트 시뮬레이션 도구 제공
  • 시뮬레이션이 어려운 이벤트 (mousemove, mouseover 등) 는 테스트 불가

E2E (Selenium Webdriver)

  • 실제 브라우저의 행위를 시뮬레이션
  • 커스텀 이벤트를 발생시키는 것보다 더 실제 환경에 가깝게 테스트 가능
  • e2e를 최근에 많이 함.
  • 대부분 통합 테스트 할거면 사용하자.

UI 테스트(counter)

Fixture 셋팅 및 초기화

테스트 코드

let $container;beforeEach(() => {
$container = $('<div>');
$container.appendTo(document.body);
createUICounter($container, {
initVal: 10,
min: 8,
max: 12
});
});
afterEach(() => {
document.body.innerHTML = '';
});

테스트 전략1 : HTML 구조 테스트

전략

테스트 코드

  • test/uiCounter/html.spec.js

테스트 전략 2 : 스냅샷 테스트

  • 현재의 HTML 구조를 그대로 파일로 저장한 후, 변경될 때마다 테스트 실패
  • 실제 결과는 파일 내부를 직접확 확인해 보아야 함.
  • HTML을 직접 비교하는 것보다 간편함.
  • 회귀 테스트의 역할

테스트 코드

  • test/uiCounter/snapshot.spec.js
it('생성시 버튼과 초기값을 렌더링한다.', () => {
expect($container.html()).toMatchSnapshot();
});
it('+ 버튼 클릭시 1 증가한다.', () => {
$el.find('.btn-inc').click();
expect($container.html()).toMatchSnapshot();
});
it('Max값인 경우 + 버튼이 disabled 상태가 되며 클릭해도 증가하지 않는다.',() => {
$el.find('.btn-inc').click();
$el.find('.btn-inc').click();
$el.find('.btn-inc').click();
expect($container.html()).toMatchSnapshot();
});

스냅샷 파일

  • test/uiCounter/snapshots/snapshot.spec.js.snap
<button type="button"
class="btn btn-secondary btn-dec"
>
-
</button>
<span class="value">
11
</span>
<button type="button"
class="btn btn-primary btn-inc"
>
+
</button>
`;

전략 1,2에 대한 평가

세부 구현 테스트

  • 실제 UI에 대한 결과물은 픽셀로 표현되는 화면이지 HTML이 아님
  • HTML은 세부 구현 방식에 대한 내용
  • HTML은 동일해도 CSS에 따라 결과물(픽셀)이 완전히 달라질 수 있음
  • HTML이 변경되어도 결과물(픽셀)은 동일할 수 있음
  • 테스트 코드(HTML 구조)를 보고 의도(실제 화면)을 파악하기 어려움

스냅샷 테스트

  • 테스트 코드를 보고 의도를 파악하기 어려움
  • 테스트 의도를 코드로 입력하지 않음 -> 회귀 테스트의 용도로만 사용
  • 스냅샷 데이터의 Diff를 보아도 의도를 파악하기 어려울 때가 있음
    습관적인 업데이트 -> 테스트의 신뢰성 감소

Selenium Webdriver

무겁고 다루기 어렵다.

e2e에서는 제왕이다.

시각적 테스트

시각적 테스트의 어려움

  • 시각적 요소에 대한 테스트는 자동화하기가 어려움
  • 스크린샷을 직접 찍어 비교하는 방식이 가장 테스트의 목적에 부합함
  • 원하는 아웃풋을 미리 만들어내기가 어렵기 때문에 “회귀(Regression)” 테스트만 가능
  • 디자인 시안이 케이스별로 잘 정리되어 있다면 기대값으로 사용 가능
  • 하지만, 브라우저 여백, 이미지 사이즈 등 기술적으로 극복해야 할 부분이 많음
  • 또한, 브라우저나 OS에 따라 렌더링 방식이 다르기 때문에 기술적 한계가 있음
    - 동일한 이미지인데 픽셀이 미세하게 다른 경우가 많음
  • 변경된 부분을 정확히 감지하고, 의도된 변경인지 아닌지 파악하기가 어려움
    - 변경점과 이력 관리를 위한 별도의 도구 필요

스토리북(Storybook)

  • 애플리케이션에서 분리된 별도의 환경에서 UI만 개발할 수 있도록 도와주는 도구
    - 시각적 표현에 대한 개발만 빠르게 진행할 수 있음
  • 컴포넌트 방식의 개발에 잘 어울림 (React, Vue, Angular)
  • 컴포넌트 갤러리로 활용
  • 디자이너/기획자와의 커뮤니케이션 도구로써 유용함
  • Airbnb Dates
  • Wix Style
  • 스토리북을 사용하면 시각적 테스트를 수동으로 진행할 때 더 효율적임

시각적 회귀(regression) 테스트

  • 최근 기술적 한계를 극복한 다양한 테스트 도구가 출시되어 발전하는 중
  • AI 등을 활용해서 의미있는 차이만 감지
  • 대규모 스냅샷 파일의 이력 관리, 이미지 직접 비교, 스냅샷 갱신 등의 관리를 도와줌
  • 스토리북 / Crypess / 셀레니움 등의 도구와 연동해서 사용할 수 있음
  • Percy, Applitools, Chromatic

시각적 요소와 기능적 요소 분리하기

  • 시각적 요소와 기능을 같이 테스트하는 것은 테스트의 복잡도를 증가시킴
  • 시각적 요소를 스토리북과 같은 도구로 분리하고, 기능과 상태 변경만 테스트하는 것이 효율적임
  • 실제 UI의 “컨텐츠(상태)” 만을 테스트 (시각적 요소 제외)
  • 버튼의 존재 유무 / 버튼의 상태 / 화면상의 텍스트 등
  • HTML 구조와의 종속성을 최소화하기
  • 기능만을 위한 별도의 HTML 속성, 클래스 등을 사용
  • CSS 셀렉터 사용 시 태그, 자식 선택자 등을 지양

dom-testing-library

The more your tests resemble the way your software is used, the more confidence they can give you.

특징

  • CSS 셀렉터를 지양하고 사용자가 볼 수 있는 텍스트 위주의 셀렉터를 제공

getByText, getByLabelText, getByAltText, getByTitle, getByValue...

  • 텍스트를 사용할 수 없는 경우에는 data-test-id 속성을 사용하도록 권장

getByTestId

테스트 코드가 DOM 구조의 변경에 영향을 받지 않도록 하기 위함

  • 이벤트를 시뮬레이션할 수 있는 함수를 제공

fireEvent

  • DOM이 변경되거나 특정 단언(Assertion)이 성공할 때까지 기다릴 수 있는 함수를 제공
  • wait, waitForElement, waitForDomChange
  • 비동기 로직을 테스트할 때 사용
  • it(‘examples of some things’, async () => { const famousWomanInHistory = ‘Ada Lovelace’ const container = getExampleDOM()
it(‘examples of some things’, async () => {
const famousWomanInHistory = ‘Ada Lovelace’
const container = getExampleDOM()
// Label 텍스트로 검색 (없으면 에러 발생)
const input = getByLabelText(container, 'Username')
input.value = famousWomanInHistory
// 실제 텍스트로 검색 (없으면 에러 발생)
getByText(container, 'Print Username').click()
await wait(() =>
// [data-testid="printed-username"]인 요소 검색 -> 없으면 계속 시도 (timeout 까지)
expect(queryByTestId(container, 'printed-username')).toBeTruthy() )
});

jest-dom


import '@testing-library/jest-dom/extend-expect';
it('test', () => {
// visibility 검사
expect(getByText(container, '+')).toBeVisible();
expect(getByText(container, '-')).toBeVisible();

// DOM 문서(body)에 포함되었는지 여부를 확인
expect(getByText(container, '10')).toBeInTheDocument();

// 특정 클래스를 갖는지 검사
expect(getByText(container, '+')).toHaveClass('.btn-inc');
expect(getByText(container, '-')).toHaveClass('.btn-dec');

// Disabled 상태 검사
expect(getByText(container, '+')).toBeDisabled();
expect(getByText(container, '-')).toBeDisabled();

// 특정 텍스트를 포함하는지를 검사
expect(getByTestId(container, 'value')).toHaveTextContent('10');
});

실습: counter 테스트

목표

  • jquery를 사용하지 않고 테스트한다.
  • dom-testing-library와 jest-dom를 사용해서 UI를 테스트하는 방법을 익힌다.
  • 다음의 테스팅 전략을 수행한다.
  • 시각적 요소는 테스트하지 않는다.
  • 사용자의 관점에서 보이는 텍스트 위주로 테스트한다.
  • fireEvent 함수를 이용해 이벤트를 발생시킨다.

진행 방법

  • 스크립트 : npm run test:uiCounter
  • 테스트 파일 : test/uiCounter/dom.spec.js

결과 평가

  • 전략1,2와 비교해서 장/단점은 무엇인가

Mocking

모듈 Mocking

import {createCounter} from '../../src/counter';
jest.mock('../../src/counter');
it('생성시 주어진 옵션으로 counter를 생성한다.', () => {
createCounter.mockImplementation(() => ({
val: () => 10,
isMin: () => false,
isMax: () => false
}));
const options = {
initVal: 10,
min: 8,
max: 12
};
createUICounter(container, options); expect(createCounter).toHaveBeenCalledWith(options);
});

Mock(Spy) 함수 생성

test('jest.fn()', () => {
const mockFn = jest.fn();
mockFn(1, 'one');
mockFn(2, 'two');

expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith(1, 'one');
expect(mockFn).toHaveBeenCalledWith(2, 'two');

expect(mockFn.mock.calls[0]).toEqual([1, 'one']);
expect(mockFn.mock.calls[1]).toEqual([2, 'two']);
});

추가학습 : counter 단위 테스트(Mock)

Mock을 이용해 UI만 테스트

  • 기존 createCount 테스트와 겹치지 않게 View만 테스트하기
  • createCount를 Mocking 해서 테스트

테스트 코드

  • test/uiCounter/unit.spec.js

단점

  • 테스트 코드가 더 장황해짐
  • 세부 구현에 대한 의존도가 높아짐
  • 리팩토링할 때 테스트가 실패할 확률이 높아짐

실습 : 서버 API 통신 (Counter)

예제 설명

구성

  • 카운터 로직을 서버에서 처리 (parcel 번들러에서 서버 로직 구현)
  • Ajax 요청을 위해 axios 라이브러리 사용
  • 기존 UI 카운터는 서버의 데이터를 받아서 렌더링만 하도록 변경

실행 방법

  • npm run app:serverCounter

Ajax Mocking

이유

  • 실제 API 서버를 사용해서 테스트하면 데이터를 조작하기가 어려움
  • Ajax 요청을 Mocking하면 API 명세만 갖고도 개발/테스트가 가능

방법

  1. Ajax를 호출하는 서비스 레이어 모듈을 Mocking
  2. Ajax를 호출하는 라이브러리와 호환되는 Mock 라이브러리를 사용

3. 별도의 가상 API 서버를 구현

E2E 테스트

Selenium Webdriver

Selenium RC(Remote Control)

  • Selemium1.0
  • 자바스크립트를 브라우저 내부에 삽입해서 제어하는 방식
  • 자바스크립트의 샌드박스를 벗어나지 못함
  • 모든 종류의 브라우저를 완벽하게 지원하지 않음
  • 더이상 지원되지 않음

Selenium WebDriver

  • Selemium 2.0
  • 브라우저 외부에서 제어하는 방식
  • 네이티브 수준에서 브라우저를 직접 제어
  • WebDriver 명세를 따르는 각 브라우저별 웹드라이버를 설치해서 사용
  • 대부분의 브라우저 및 실행환경을 지원
  • FirefoxDriver / ChromeDriver / InternetExplorerDriver / SafariDriver / Appium

WebDriver 명세

Selenium Grid

  • 허브 역할을 하는 머신이 노드 역할을 하는 모든 머신에게 테스트를 지시한 후 결과를 모아서 반환
  • 테스트를 다양한 환경의 머신에서 동시에 수행할 수 있는 환경을 제공

활용

  • QA 개발자 모두가 사용
  • WebDriver 클라이언트의 언어는 어떤 언어든 사용 가능

Java / C# / Ruby / Python / Javascript

Selenium Webdriver 라이브러리

Protractor

WD

  • http://admc.io/wd
  • JsonWireProtocol 를 자바스크립트로 호출할 수 있는 API만을 제공
  • 테스트 러너 / Assertion 등의 지원 없음 : 테스트 환경 별도 구축 필요
  • 플러그인 지원 없음 / 문서는 기본적인 수준
  • Repl 지원
  • Promise(Q) 기반 / 체이닝 형식의 API
  • 제너레이터 기반의 라이브러리도 제공 (Yiewd)
browser
.get(“http://www.google.com")
.elementById(‘q’)
.sendKeys(‘webdriver’)
.elementById(‘btnG’)
.click()

seleniumn-webdriverjs

driver.get(‘http://www.google.com'); driver.findElement(webdriver.By.id(‘q’)).sendKeys(‘webdriver’); driver.findElement(webdriver.By.id(‘btnG’)).click();

NightWatch

  • http://nightwatchjs.org
  • Mocha 기반의 테스트 러너 지원
  • Assertion 라이브러리 선택 가능
  • 커뮤니티 활성화 & 문서화 잘 되어 있음
  • SauceLabs / BrowserStack 과 같은 외부 서비스 지원
browser
.url(‘http://www.google.com')
.setValue(‘#q’, ‘webdriver’)
.click(‘#btnG’);

WebdriverIO

  • http://webdriver.io
  • 테스트 러너 지원
  • Assertion 라이브러리 선택 가능 (Jasmine 플러그인 지원)
  • 커뮤니티 활성화 & 문서화 잘 되어 있음 (NightWatch 보다 더)
  • SauceLabs / BrowserStack 과 같은 외부 서비스 지원
  • Static 웹서버 + Webpack 통합 지원
  • Visual Regression 테스트 지원
  • Jenkins 통합 지원
  • Repl 인터페이스 지원
  • 확장성 좋음
client
.url(‘http://google.com')
.setValue(‘#q’, ‘webdriver’)
.click(‘#btnG’)

단위/통합 테스트 vs E2E 테스트

단위/통합 테스트의 단점

  • 실제 사용자의 환경에서 발생하는 버그를 검출하기 어려움
  • 각 모듈간의 연결에서 발생하는 버그를 검출하기 어려움
  • Jest 등의 Node 환경에서 테스트하는 경우 실제 화면을 볼 수가 없음

E2E 테스트의 단점

  • 속도가 느림
  • 테스트 코드를 작성하기가 번거로움
  • 실행 환경에 대한 통제가 쉽지 않아 안정성이 떨어짐

해결책

  • 사용자의 관점에서 테스트를 하면서도 빠르고 사용하기 쉽고 안정적인 환경이 필요
  • 최근 E2E 테스트의 단점을 극복한 다양한 도구들이 출시

최신 E2E 도구들

Cypress

test cafe

  • https://devexpress.github.io/testcafe/
  • 프록시 서버를 통해 스크립트를 페이지에 주입해서 사용
  • 모든 브라우저에서 사용 가능 (모바일, 클라우드 브라우저 포함)
  • Test Cafe Studio
  • GUI를 통해 손쉽게 테스트 관리 및 작성 (유료)

실습1: ui-counter

목표

  • cypress로 UI의 상태를 테스트하는 방법을 익힌다.
  • 기존에 작성했던 ui-counter의 테스트와 동일한 명세를 cypress로 작성한다.
  • Jest로 작성한 테스트와의 장단점을 비교해본다.

진행 방법

  • cypress 실행 :
  • npm run cypress:open
  • 서버 실행
  • npm run app:uiCounter
  • 테스트 파일
  • cypress/integration/uiCounter.spec.js

서버 목킹

// 목킹 활성화
cy.server();
// /counter (GET) 요청에 대한 응답값 지정
cy.route('/counter', {
value: 10,
isMin: false,
isMax: false
});
// counter/inc (PUT) 요청에 대한 응답값 지정
cy.route('/counter/inc', {
value: 10,
isMin: false,
isMax: false
});

실습2: server-counter

목표

  • cypress로 서버 API를 목킹한 후 UI의 상태를 테스트하는 방법을 익힌다.
  • 기존에 작성했던 server-counter의 테스트와 동일한 명세를 cypress로 작성한다.
  • Jest로 작성한 테스트와의 장단점을 비교해본다.

진행 방법

  • cypress 실행 :
  • npm run cypress:open
  • 서버 실행
  • npm run app:serverCounter
  • 테스트 파일
  • cypress/integration/serverCounter.spec.js

정리

프런트엔드 테스트 전략

시각적 요소 vs 기능적 요소

  • 시각적 요소를 HTML이나 CSS를 이용해 테스트하는 것은 지양한다.
  • 스토리북 등의 도구를 사용하여 시각적 요소의 다양한 상태를 테스트하기 쉽게 관리한다.
  • 이미지 비교 방식의 테스트 도구와 스토리북을 결합하면 시각적 테스트를 자동화할 수 있다.
  • 기능적 요소를 테스트할 때는 최대한 HTML 구조의 변화에 영향을 받지 않도록 한다.

테스트 단위

  • 핵심 알고리즘을 담고 있는 모듈이 아니면 단위 테스트를 지양한다.
  • 내부 모듈간의 의존성을 Mocking 하는 것은 지양하고, 통합 테스트 위주로 작성한다.
  • Cypress와 같은 최신 E2E 도구는 더 나은 프런트엔드 테스트 환경을 제공해 준다.

개인적 느낀 점.

굉장히 유익한 시간이었다. 기존의 테스트에 대한 관심은 많았으나 막상 어떻게 사용하고 적용해야 하는 가에 의문을 말끔히 해결하는 자리가 되었다.

그리고 기존에는 부하 테스트에 사용하는 경우가 많았다면 storybook이외에 다양한 ui 테스트, cypress를 통한 E2E 테스트 등에 대해 알아 볼 수 있는 기회가 되었다.

하지만 실제로 실무에 적용하기 위해 ui테스트는 너무나도 비용이 많이 든다고 생각된다. 거의 똑같은 코드를 두번씩 작성하는 수준이라.. 실무에 적용하기에 의문이 든다.

특히 이런 부분

import prettyHTML from 'diffable-html';

// .. 초기화

it('생성시 버튼과 초기값을 렌더링한다.', () => {
expect(prettyHTML($container.html())).toBe(prettyHTML(`
<button type="button" class="btn btn-secondary btn-dec">-</button>
<span class="value">10</span>
<button type="button" class="btn btn-primary btn-inc">+</button>
`));
});

it('+ 버튼 클릭시 1 증가한다.', () => {
$container.find('.btn-inc').click();

expect(prettyHTML($container.html())).toBe(prettyHTML(`
<button type="button" class="btn btn-secondary btn-dec">-</button>
<span class="value">11</span>
<button type="button" class="btn btn-primary btn-inc">+</button>
`));
});

it('Max값인 경우 + 버튼이 disabled 상태가 되며 클릭해도 증가하지 않는다.', () => {
$container.find('.btn-inc').click();
$container.find('.btn-inc').click();
$container.find('.btn-inc').click();

expect(prettyHTML($container.html())).toBe(prettyHTML(`
<button type="button" class="btn btn-secondary btn-dec">-</button>
<span class="value">12</span>
<button type="button" disabled class="btn btn-primary btn-inc">+</button>
`));
});

이렇게 테스트 코드를 작성하나 여기에 적을 바에 실제로 ui를 그려서 테스트를 통해서 확인하는 것이 개발 비용이 훨씬 저렴하기에 ui 테스트를 일일히 다 하는 것은 무리라는 생각이 든다.

적용할 수 있는 부분을 찾아보면

  1. 스토리북을 이용한 ui컴포넌트 테스트컴포넌트 단위로 ui를 보며 테스트 하는 것인데 생각보다 많은 개발자들이 이 툴을 이용한다고 한다. 예전에 한번 써 보았으나 비용적인 측면에서 선택하지 않았었는데 고려해보면 좋을 것 같다.
  2. 데이터 통신에서의 테스트가장 무난하게 쓰이는 테스트이며 현재 티켓 product 에서 Java를 이용하여 사용하고 있다. JS도 이에 맞게 작성하면 좋을 것 같다.
  3. E2E 테스트이 부분에 대해서는 개발 완료한 뒤 구현하면 좋을 것 같은데 이 자료에서는 이에 대한 자료가 거의 없다. 그렇기에 시간이 상당히 걸릴 것 같다.

이 정도면 무난히 테스트 코드를 잘 적용했다. 정도라고 생각된다.

실습하는데 총 6시간 정도 걸린듯 하다. 하지만 그만큼 충분히 퀄리티 높고 의미있는 시간이었다. 하면서 퀘스트를 깨는 듯한 재미가 느껴져서 몰입해서 진행했다.

ps : 꼭 이 실습을 진행해보셨으면 좋겠고 모르는 부분은 말씀해주시면 함께 고민해보고 디버깅하면 좋겠습니다.

현재 적용할 수 있는 범위

  1. API Test
  2. UI Test
  3. E2E Test

--

--

dongwon kim
dongwon kim

Written by dongwon kim

I love to write about developmental writing.

No responses yet