테스트


자바스크립트는 최근 몇 년간 비약적인 발전을 통해 사용 범위를 넓혀오고 있으며, 프론트엔드 환경에서 요구하는 애플리케이션의 수준도 나날이 복잡해지고 있다. 이와 더불어 자바스크립트의 테스트 환경도 짧은 기간 동안 많은 변화를 겪었는데, 특히 Node.js의 등장 이후 무수히 많은 도구가 쏟아져 나오며 빠른 속도로 발전해오고 있다.

프론트엔드 코드는 사용자에 따라 다양한 환경(브라우저, 기기, 운영체제 등)에서 실행되기 때문에 테스트할 때 많은 변수들을 고려해야 한다. 이 때문에 자바스크립트는 테스트를 위한 환경과 테스트 도구들을 얼마나 잘 이해하고 있는지가 중요한 요소이며, 프로젝트의 성격에 맞게 테스트의 범위나 테스트 대상을 결정하는 등의 전략을 세울 때에도 많은 노하우가 필요하다.

이 문서에서는 프론트엔드 자바스크립트 테스트에 사용되는 다양한 도구들과 사용법을 소개하고, 프로젝트 상황에 맞는 최적의 도구를 선택할 수 있도록 가이드를 제시한다.

(프로젝트 상황에 맞는 테스트 전략을 세우고 효율적인 테스트 코드를 작성하는 방법에 대해서는 별도의 가이드에서 상세하게 다룰 예정이다.)

목차

테스트의 의미와 종류

테스트란?

테스트라는 용어는 범용적으로 사용되기 때문에 분야에 따라 다양한 의미를 갖는다. 테스트를 소프트웨어의 관점에서 정의하자면 "애플리케이션이 요구 사항에 맞게 동작하는지를 검증하는 행위"라고 할 수 있다. 보통 개발과는 별도의 전문 영역으로 분류되며, 일정 규모 이상의 회사에서는 QA(Quality assuarance) 조직에 속한 전문 테스터들이 이 역할을 수행하는 것이 일반적이다.

하지만 2000년대 이후 애자일 방법론이 널리 퍼지기 시작하고 익스트림 프로그래밍(Extreme programming) 등에서 강조하는 테스트 주도 개발(Test Driven Development) 등이 대중화되면서, 테스트는 점점 더 개발 단계의 일부로 받아들여지고 있다. 여기서 말하는 테스트란 주로 코드로 작성된 자동화 테스트를 말한다. 개발자가 테스트 코드를 직접 작성하게 되면 더 적극적으로 리팩토링 등의 코드 개선을 할 수 있어 코드 품질이 향상되며, 개발 단계에서의 제품 품질이 향상되어 테스터와의 불필요한 커뮤니케이션 비용이 감소하게 된다.

(이 가이드에서는 테스트라는 용어를 "개발자가 작성하는 자동화 테스트"라는 의미로 사용하고 있으며, 이 범위를 벗어나는 내용은 다루지 않는다.)

테스트에는 다양한 종류가 있으며, 개발자의 관점에서는 보통 범위에 따른 구분을 많이 사용한다. 범위에 따라서는 크게 단위 테스트, 통합 테스트, E2E(End to End) 테스트로 구분할 수 있다. 흔히 개발자가 작성하는 테스트는 모두 단위 테스트라고 생각하는 경우가 많은데, 각 테스트 방식마다 고유한 장단점이 있기 때문에 이들을 잘 구분해서 상황에 따라 적절한 방식을 선택하는 것이 중요하다. 이를 위해 각각의 테스트 방식에 대해 좀 더 자세히 살펴보겠다.

단위 테스트

단위 테스트는 작은 단위(주로 모듈 단위)를 전체 애플리케이션에서 떼어 내어 분리된 환경에서 테스트하는 것을 말한다. 분리된 상태의 테스트이기 때문에 하나의 모듈이나 클래스에 대해 세밀한 부분까지 테스트할 수 있고 더 넓은 범위에서 테스트할 때보다 훨씬 빠르게 실행할 수 있다. 하지만 단위 테스트는 의존성이 있는 모듈을 제어하기 위해 필연적으로 모의 객체(Mocking)을 사용할 수밖에 없으며, 이 경우 각 모듈이 실제로 잘 연결되어 상호 작용하는지에 대해서는 검증하지 못한다. 또한 각 모듈의 사소한 API 변경에도 영향을 받기 때문에 작은 단위의 리팩토링에도 쉽게 깨지는 문제가 있다.

통합 테스트

통합 테스트는 단위 테스트보다 좀 더 넓은 범위의 테스트를 말하며 보통 두 개 이상의 모듈이 실제로 연결된 상태를 테스트한다. 여러 개의 모듈이 동시에 상호 작용하는 것을 테스트하기 때문에 단위 테스트에 비해 모의 객체의 사용이 적으며, 모듈 간의 연결에서 발생하는 에러를 검증할 수 있다. 또한 비교적 넓은 범위에서의 API 변경에만 영향을 받기 때문에 단위 테스트와 비교해 리팩토링을 할 때 쉽게 깨지지 않는 장점이 있다. 하지만 단일 모듈이 복잡한 알고리즘이나 분기문을 갖고 있을 때 단위 테스트에 비해 테스트가 번거롭고, 테스트 중복이 발생할 확률이 높다는 단점이 있다.

E2E 테스트

단위 테스트나 통합 테스트는 모두 내부 구조를 알고 있는 개발자의 관점에서 제품 일부분만을 선별해서 테스트하는 방식이다. E2E 테스트는 이와 다르게 실제 사용자의 관점에서 테스트를 진행하며, 그런 의미에서 기능(Functional) 테스트 혹은 UI(User Interface) 테스트라고 불리기도 한다. E2E 테스트는 사용자의 실행 환경과 거의 동일한 환경에서 테스트를 진행하기 때문에 실제 상황에서 발생할 수 있는 에러를 사전에 발견할 수 있다는 장점이 있다. 특히 브라우저를 외부에서 직접 제어할 수 있어 자바스크립트의 API만으로는 제어할 수 없는 행위(브라우저 크기 변경, 실제 키보드 입력 등)를 테스트할 수도 있다. 또한 테스트 코드가 실제 코드 내부 구조에 영향을 받지 않기 때문에 큰 범위의 리팩토링에도 깨지지 않으며, 이를 통해 개발자들이 좀 더 자신감 있게 코드를 개선할 수 있도록 도와준다.

반면 단위 테스트나 통합 테스트에 비해 테스트의 실행 속도가 느리기 때문에 개발 단계에서 빠른 피드백을 받기가 어려우며, 세부 모듈들이 갖는 다양한 상황들의 조합을 고려해야 하기 때문에 테스트를 작성하기가 쉽지 않다는 단점이 있다. 또한 큰 단위의 기능을 작은 기능으로 나누어 테스트할 수가 없기 때문에 필연적으로 테스트 사이에 중복이 발생할 수밖에 없다. 게다가 통제된 샌드박스 환경에서의 테스트가 아니기 때문에 테스트 실행 환경의 예상하지 못한 문제들(네트워크 오류, 프로세스 대기로 인한 타임아웃 등)로 인해 테스트가 가끔 실패하는 일이 발생하며, 이 때문에 테스트를 100% 신뢰할 수 없는 문제가 발생하기도 한다.

자바스크립트 테스트 도구

(이 장에서는 주로 단위/통합 테스트의 관점에서 테스트 도구들을 설명한다. E2E 테스트 도구의 경우 테스트를 바라보는 관점이 달라 이 장에서 다루는 분류와는 맞지 않는 경우도 있기 때문에 별도의 장에서 다루도록 하겠다.)

최근의 자바스크립트 테스트 도구들은 테스트를 위한 다양한 기능을 통합하여 제공하고 있으며, 각 도구가 지원하는 기능도 각기 다르다. 이들을 제대로 비교하고 선택하기 위해선 먼저 자바스크립트 테스트를 위해 어떤 기능들이 필요한지를 아는 것이 도움이 된다. 이들을 분류하는 정확한 기준은 없지만, 기본적으로는 테스트를 구동할 수 있는 환경을 제공하는 테스트 러너와 테스트 코드 작성을 위한 기반을 만들어주는 테스트 프레임워크로 나눌 수 있다. 이 외에도 테스트 코드를 좀 더 편리하게 작성할 수 있도록 도와주는 단언(assertion) 라이브러리, 테스트 더블 라이브러리 등을 사용하기도 한다.

테스트 러너

테스트 러너는 테스트 파일을 읽어들여 작성한 코드를 실행하고, 그 결과를 특정한 형식으로 출력해준다. 테스트의 수행 결과는 리포터(Reporter)를 지정해서 원하는 형태로 출력할 수 있다. 부가적으로 테스트 코드나 소스 코드가 변경된 경우 영향을 받는 테스트를 자동으로 재실행해주는 왓쳐(Watcher) 등의 기능도 제공한다.

이전에는 자바스크립트를 실행할 수 있는 환경이 브라우저에 한정되어 있었기 때문에, 작성된 테스트 코드를 직접 브라우저에서 실행한 후 웹페이지나 브라우저 콘솔을 통해서만 결과를 확인할 수 있었다. 하지만 Node.js의 등장으로 브라우저 없이도 자바스크립트 코드를 손쉽게 실행할 수 있게 되었으며, 덕분에 테스트 러너와 같은 도구를 사용해 이러한 과정을 자동화할 수 있게 되었다.

테스트 러너는 크게 Karma와 같이 브라우저에서 직접 코드를 실행하는 러너와, Jest와 같이 Node.js 환경에서 코드를 실행하는 러너로 나눌 수 있다. 이 중 Node.js 기반의 테스트 러너들은 굳이 러너의 실행 환경과 코드의 실행 환경을 구분할 필요가 없기 때문에 대부분 테스트 프레임워크와 통합된 형태로 제공된다.

테스트 프레임워크

사용자가 테스트 코드를 작성할 수 있는 기반을 제공해주는 자바스크립트 도구이다. 프레임워크가 제공하는 함수들을 사용해서 테스트 코드를 작성하면, 프레임워크가 테스트 코드를 자동으로 실행한 후 성공 및 실패에 대한 결과를 반환해준다. 대표적인 테스트 프레임워크로는 Mocha, Jasmine, AVA 등이 있으며, 최근에는 Jest가 빠른 속도로 점유율을 높여가고 있다.

아래는 Jasmine으로 작성된 테스트 코드의 예이다. describe, beforeEach, it, expect 등의 함수는 모두 Jasmine이 제공하는 전역 함수이며, 좀 더 상세한 내용은 이 글의 후반부에서 설명하고 있다.

describe('calculations', () => {
  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) 라이브러리

테스트 코드는 주로 테스트를 위한 초기화와 단언으로 이루어지며, 단언은 개별 테스트가 통과하기 위한 조건을 명확하게 기술하기 위해 사용된다. 보통은 테스트 프레임워크에서 다양한 방식의 단언 API를 기본 제공하고 있으며, Mocha의 경우에만 Chai와 같은 별도의 단언 라이브러리를 사용하도록 권장하고 있다.

초기의 단언 라이브러리들은 JUnit과 유사한 방식의 API를 많이 따랐지만, 최근에 가장 많이 사용되는 Chai, Jasmine 등에서는 좀 더 자연어에 가까운 BDD(Behavior-driven development 방식의 API가 사용된다. 또한 대부분의 단언 라이브러리들은 사용자들이 필요에 따라 자신만의 단언을 추가해서 사용할 수 있는 플러그인 확장 기능을 제공한다.

아래 예제는 Jasmine으로 작성된 테스트 코드이다. expect() 와 함께 toBeNull(), toEqual(), toHaveBeenCalled() 등의 다양한 단언들이 사용되는 것을 볼 수 있다.

expect(obj).not.toBeNull();
expect(obj).toEqual({
  name: 'Kim',
  age: 30
});
expect(result).toBe(true);
expect(result).toBeTruthy();
expect(spy).toHaveBeenCalled();

테스트 더블(test double) 라이브러리

테스트 더블이란 실제 객체 대신 테스트를 위해 동작하는 객체를 말하며, 주로 분리된(isloated) 단위 테스트를 위해 외부 의존성을 임의로 주입하기 위해서 사용한다. 필요에 따라 스파이(spy), 스텁(stub), 목(mock) 등의 다양한 테스트 더블을 사용할 수 있으며, 이들을 쉽게 만들 수 있도록 도와주는 라이브러리를 테스트 더블 라이브러리라고 한다. 단언과 마찬가지로 테스트 더블을 위한 함수들도 테스트 프레임워크에서 기본 제공되는 경우가 대부분이며, Mocha의 경우에만 Sinon.JS 등의 별도 라이브러리를 사용하도록 권장하고 있다.

테스트 더블은 일반적으로 자바스크립트 객체 혹은 함수를 직접 변경하거나 생성하는 형태로 사용되며, Jest에서는 모듈 단위로 사용할 수 있는 기능도 제공한다. 또한 Jasmine의 Clock이나 Sinon.JS의 Lolex와 같은 도구를 사용하면 자바스크립트의 타이머 API도 직접 제어하며 테스트할 수 있다. 이 외에도 Axios와 같은 유명 라이브러리의 경우 별도로 구현된 Mock 라이브러리를 쉽게 찾을 수 있는 경우가 많으므로, 먼저 해당 관련된 라이브러리가 있는지 검색해 보길 권장한다.

다음은 Jasmine에서 제공하는 spyOn 함수를 사용하여 테스트 더블을 만드는 예제이다. 스파이를 사용해 객체의 특정 메서드가 호출된 적이 있는지, 호출될 때 어떤 인자가 넘어왔는지를 검증하는 것을 볼 수 있다.

const person = {
  name: 'Kim',
  getName() {
    return this.name;
  },
  setName(name) {
    this.name = name;
  }
}

it('test spy', () => {
  spyOn(person, 'setName');
  spyOn(person, 'getName').and.callThrough();
  
  person.setName('Lee');
  const name = person.getName();
  
  expect(person.setName).toHaveBeenCalledWith('Lee');
  expect(person.getName).toHaveBeenCalled();
  expect(name).toBe('Kim');
});

테스트 실행 환경

테스트 러너 항목에서 언급했듯이, 자바스크립트의 테스트는 브라우저 환경과 Node.js 환경 모두에서 실행할 수 있다. 하지만 두 환경 모두 뚜렷한 장단점이 있기 때문에 상황에 맞게 적절한 테스트 러너를 선택해야 한다. 이 단락에서는 두 환경의 장단점을 알아보고, 각 실행 환경에서 권장하는 테스트 방식을 간략하게 설명하겠다.

브라우저

실제 브라우저를 실행해서 테스트 코드를 실행하는 방식을 의미하며, E2E 테스트 도구들을 제외한다면 현재로서는 Karma를 사용하는 것이 유일한 방법이라 할 수 있다. Karma는 테스트 러너의 역할만 하기 때문에 별도의 테스트 프레임워크가 추가로 필요하며, 보통 Jasmine을 사용하기를 권장한다.

커맨드 라인에서 Karma를 실행하면 먼저 자체 웹서버를 구동한 후 테스트 실행을 위한 HTML 페이지를 만들고, 작성된 테스트 코드 및 소스 코드 전부를 해당 페이지에 로드한다. 이후 브라우저를 직접 실행해서 해당 웹페이지에 접속하면 로드된 코드가 실행되고 테스트의 실행 결과는 브라우저 콘솔에 출력된다. Karma는 이 정보를 받아와 지정된 리포터를 사용해 결과를 정리한 후 커맨드 라인에 보여준다.

이 방식의 가장 큰 장점은 실제 브라우저 환경에서 테스트하기 때문에 브라우저의 모든 기능(네트워크 IO, 렌더링 엔진 등)을 활용해서 테스트할 수 있다는 점이다. 또한 Selenium 등의 도구를 사용하면 동일한 테스트 코드를 다양한 환경(운영체제, 브라우저) 테스트를 실행할 수 있기 때문에, 브라우저 호환성 및 기기 환경에 대한 테스트도 진행할 수 있다.

하지만 브라우저의 프로세스가 Node.js의 프로세스보다 무겁기 때문에 테스트의 초기 구동 속도가 더 느리다는 단점이 있다. 또한 브라우저라는 별도의 애플리케이션을 추가로 실행해야 하기 때문에 실행을 위한 브라우저 런처(launcher) 등을 추가로 설치해 주어야 하는 번거로움이 있으며, 크로스 브라우징 테스트 등을 위해서 별도의 환경을 구축하고 유지보수 하는 비용도 결코 무시할 수 없다.

이러한 단점을 극복하기 위해서 보통 개발 단계에서는 헤드리스 브라우저를 사용해서 빠른 피드백을 얻을 수 있도록 하고, 개발 완료 혹은 배포 시에만 CI 서버와 통합하여 크로스 브라우징 테스트를 하는 방식을 권장한다. 또한 Browser Stack이나 Sauce Lab 등의 외부 서비스를 사용하면 크로스 브라우징을 위한 환경을 직접 구축할 필요 없이 Karma와 손쉽게 연동하여 사용할 수 있다.

Node.js

Node.js 환경에서 테스트 코드를 실행하는 방식을 의미하며, 최근 가장 많이 쓰이는 도구는 Mocha와 Jest이다. 위에서 언급한 것처럼 테스트 러너와 테스트 프레임워크가 통합되어 있어 설치 및 실행이 비교적 간단하다. 이 방식의 가장 큰 장점은 역시 속도인데, Node.js의 프로세스가 브라우저의 프로세스에 비해 훨씬 가볍기 때문에 실행 속도가 빠르다. 또한 브라우저에서는 아직 모듈 단위의 테스트를 실행하기가 어려워 webpack 등의 번들러를 사용해야 하는 제약이 있는 반면 Node.js 환경에서는 개별 프로세스에서 원하는 모듈만 가져와서(import) 테스트할 수 있기 때문에 훨씬 간단하고 안전한 방식으로 테스트를 할 수 있다.

반면 이 방식의 중요한 단점은 브라우저의 모든 API를 제대로 활용할 수 없다는 것이다. Node.js에는 브라우저가 제공하는 DOM(Document Object Model)이나 BOM(Browser Object Model) 등의 API가 없기 때문이다. 이 문제를 해결하기 위해 jsdom과 같은 라이브러리를 사용해서 브라우저 환경을 가상으로 구현하는 방식을 사용하고 있지만, 실제 브라우저의 동작을 100% 구현하지는 못하기 때문에 많은 제약이 있다. 예를 들어 렌더링 엔진을 갖고 있지 않기 때문에 UI 요소의 레이아웃에 대한 테스트를 할 수 없고, 내비게이션 관련 동작도 사용할 수 없다. 그뿐만 아니라 브라우저에서 실행할 수가 없기 때문에 크로스 브라우징에 대한 테스트도 할 수 없다.

브라우저 vs Node.js

위의 설명을 보고도 아직 어떤 환경을 선택해야 할지 고민된다면 다음의 가이드를 따르기를 권장한다.

  1. 크로스 브라우징 테스트가 "반드시" 필요한 경우 브라우저 환경을 사용한다.
  2. 브라우저의 실제 동작(렌더링, 네트워크 IO, 내비게이션 등)에 대한 테스트가 필요한 경우 브라우저 환경을 사용한다.
  3. 그 외의 경우 Node.js 환경을 사용한다.

1번에서 크로스 브라우징 테스트가 "반드시" 필요한 경우라고 강조한 이유는 최근에 크로스 브라우징 테스트의 필요성이 많이 감소했기 때문이다. 우선 최신 브라우저들은 표준 명세의 구현에 있어 과거에 비해 브라우저 간의 차이가 거의 없어졌다. 또한 최신 자바스크립트 문법에 대한 호환성 지원은 Babel 등의 트랜스파일러 도구가 대신해주고, DOM을 직접 조작하는 일도 React(https://reactjs.org), Vue(https://vuejs.org) 등의 프레임워크가 대신해 주는 경우가 많다. 프로젝트가 지원해야 하는 브라우저 범위나 사용하는 도구 등에 따라 크로스 브라우징 테스트가 정말로 필요한 지 다시 한 번 검토해 볼 필요가 있을 것이다.

([QA(Quality assurance)](https://en.wikipedia.org/wiki/Qualityassurance)의 관점에서는 당연히 애플리케이션이 모든 환경에서 문제없이 실행되는 것을 보장해야 한다. 이 가이드에서는 QA의 관점이 아닌 개발자의 관점에서 작성하는 자동화 테스트에 대해서 설명하고 있으며, 이 경우 크로스 브라우징 테스트를 작성하고 유지 보수하는 비용과 이를 통해 얻을 수 있는 효과를 잘 따져보는 것이 중요하다.)_

현재 널리 많이 쓰이고 있으며 이 가이드에서도 권장하고 있는 도구들은 다음과 같다.

  • 브라우저 : Karma + Jasmine
  • Node.js : Jest

이어지는 장에서는 각 도구들의 특징과 사용법을 좀 더 자세하게 설명하겠다.

Jasmine

Jasmine은 BDD 스타일의 단언 API를 사용하는 통합 테스트 프레임워크이며, Node.js와 브라우저 환경 모두에서 사용 가능하다. Mocha의 경우 단언 라이브러리는 Chai, 테스트 더블은 Sinon을 사용해야 하는 반면, Jasmine은 모든 기능을 통합해서 제공하기 때문에 라이브러리를 추가로 설치하고 설정할 필요 없이 쉽게 사용할 수 있다.

테스트 명세 작성

Jasmine의 모든 테스트 명세는 it() 함수를 사용해서 작성해야 한다. it() 함수의 첫 번째 인자로 명세의 제목을, 두 번째 인자로 명세를 실행할 함수를 넘겨주면 된다. 명세를 실행할 함수 내에서 검증을 위한 단언은 expect() 문을 사용하면 된다. 설명을 위해 두 개의 숫자를 더하는 간단한 함수를 만들어보자.

function sum(a, b) {
  return a + b;
}

이 함수를 테스트하기 위한 명세는 다음과 같이 작성하면 된다.

it('sum() 함수는 두 인자의 합을 반환한다.', () => {
  expect(sum(3, 5)).toBe(8);
});

테스트 명세 그룹화

테스트 대상이나 목적에 따라 관련된 테스트를 그룹으로 묶어주면 테스트 명세가 늘어날 경우에도 쉽게 관리할 수 있으며, 테스트 결과도 그룹별로 정리해서 보여줄 수 있다. Jasmine에서는 describe()를 사용해서 그룹화를 할 수 있으며, 중첩해서 사용하면 두 단계 이상의 하위 그룹을 구성할 수도 있다.

describe('사칙 연산', () => {
    describe('sum()', () => {
        it('인자가 하나인 경우 에러를 반환한다.', () => {
          // ...
        });
        
        it('인자가 두 개인 경우 두 인자의 합을 반환한다.', () => {
          expect(sum(3, 5)).toBe(8);
        };
    });
    
    describe('multiply()', () => {
      // ...
    });
});

테스트 초기화

테스트를 작성할 때 반복적인 초기화 작업이 필요한 경우가 있다. 이러한 경우 beforeEach()afterEach()을 사용하면 각 테스트 명세가 실행되기 전에 필요한 로직과 실행된 후에 필요한 로직을 분리해서 정의할 수 있다.

let uploader;

beforeEach(() => {
  uploader = new Uploader({
    url: 'http://test.url'
  });
});

afterEach(() => {
  uploader.destroy();
});

describe('Uploader', () => {
    it('파일 업로드 요청', () => {
      // ...
    });

    it('업로드 대기중인 파일 목록 받아오기', () => {
      // ...
    };
});

스파이(spy)

스파이는 자바스크립트에서 모의 객체를 사용할 때 가장 유용하게 사용되는 테스트 더블 중의 하나이다. 스파이는 단순히 객체를 대신하는 역할을 할 뿐만 아니라 실제 함수가 몇 번 호출되었는지, 어떤 인자를 넘겨주었는지 등의 정보를 모두 저장하고 있기 때문에 이러한 정보를 검증에 활용할 수 있다. Jasmine이 제공하는 createSpy()spyOn() 함수를 사용하면 간단하게 스파이를 만들 수 있으며, toHaveBeenCalledWith() 등의 단언을 사용해서 다양한 방식으로 결과를 검증할 수 있다.

let counter;

beforeEach(() => {
  counter = new Counter();
  spyOn(counter, 'inc');
  
  counter.inc(10);
  counter.inc(20);
});

it('inc() 호출 여부 확인', () => {
  expect(counter.inc).toHaveBeenCalled();
});

it('inc() 호출 횟수 확인', () => {
  expect(counter.inc).toHaveBeenCalledTimes(2);
});

it('inc() 호출 인자 확인', () => {
  expect(counter.inc).toHaveBeenCalledWith(10);
  expect(counter.inc).toHaveBeenCalledWith(20);
});

타이머 제어

Jasmine은 자바스크립트 타이머 API를 임의로 변경하여 시간을 직접 제어할 수 있도록 해 준다. 단, 타이머를 제어할 때 전역 객체에 있는 setTimeout 함수나 Date 등을 내부적으로 변경하기 때문에, 타이머가 필요한 테스트가 종료되면 꼭 제어를 해제해야 한다.

beforeEach(() => {
    jasmine.clock().install(); // 타이머 제어 시작
});

afterEach(() => {
    jasmine.clock().uninstall(); // 타이머 제어 해제
});

it('setTimeout() 함수는 주어진 밀리세컨 만큼 시간이 지난 후에 콜백 함수를 실행한다', () => {
  const callback = jasmine.createSpy('callback');
 
  setTimeout(callback, 100);
  
  expect(callback).not.toHaveBeenCalled(); // 아직 callback이 실행되지 않음
  jasmine.clock().tick(100); // 시간을 100ms 뒤로 변경
  expect(callback).toHaveBeenCalled(); // callback이 실행됨
});

비동기 테스트

콜백 함수 사용

테스트를 하려는 코드가 비동기로 수행되는 경우, 테스트 명세의 실행을 종료하기 전에 모든 비동기 코드가 끝날 때까지 기다려야 한다. 이 경우 it의 내용을 담는 콜백 함수에서 별도의 파라미터(주로 done이라는 이름을 사용)를 사용할 수 있다. 해당 콜백 함수에 done 파라미터를 선언하게 되면 done 함수가 실행될 때까지 테스트 명세가 종료되지 않고 대기하게 된다.

it('fetchData: API 호출 후 비동기로 콜백 함수를 실행한다.', (done) => {
  api.fetchData((response => {
    expect(response).toEqual({
      success: true
    });
    done(); // 이 함수가 실행될때까지 테스트를 종료하지 않고 대기
  });
});

프라미스 사용

Jasmine은 it의 내용을 담는 콜백 함수가 프라미스를 반환하는 경우 해당 프라미스가 해결될 때까지 자동으로 대기해준다. 위의 예제에서 fetchData 함수가 콜백을 사용하는 대신 프라미스를 반환한다면 테스트 명세를 다음과 같이 작성할 수 있다. 이 경우 fetchData()의 결괏값인 프라미스를 it 함수의 반환값으로 넘겼기 때문에, 해당 프라미스가 해결될 때까지 테스트가 종료되지 않고 대기하게 된다.

it('fetchData: 프라미스를 반환한다', () => {
  return api.fetchData().then(response => {
    expect(response).toEqual({
      success: true
    });
  });
});

async/await 사용

ES2017 명세에 추가된 async/await를 지원하는 환경의 경우 다음과 같이 it의 콜백 함수로 async 함수를 직접 넘길 수 있다. async 함수는 종료된 때 자동으로 프라미스를 리턴하기 때문에 별도의 반환문이 없이도 위의 프라미스 예제와 동일한 결과를 가질 수 있으며, 프라미스보다 더 직관적인 테스트 명세를 작성할 수 있다.

it('fetchData: 프라미스를 반환한다.', async () => {
  const response = await api.fetchData();
  expect(response).toEqual({
    success: true
  });
});

Karma 테스트 러너

앞서 Jasmine으로 작성한 테스트 코드를 브라우저 환경에서 실행하기 위해서는 별도의 페이지를 생성하고, 소스 코드 및 테스트 코드 등을 모두 로드하는 등의 작업이 추가로 필요하다. 또한 테스트 결과를 확인하기 위해서는 UI를 추가하거나 브라우저 개발자 도구의 콘솔 창을 사용할 수밖에 없다. Karma는 브라우저 환경에서 테스트를 할 때 이러한 일련의 작업을 대신해 주는 도구로서, 아래와 같은 기능을 제공한다.

  • 로컬 웹서버를 구동한 후 테스트에 필요한 소스 코드 및 리소스를 모두 로드하는 HTML 페이지를 생성한다.
  • 지정된 브라우저 프로세스를 자동으로 실행한 후 앞서 생성한 웹페이지의 URL에 접속한다.
  • 브라우저에서 실행된 결과를 받아와서 지정된 리포터를 사용해 다양한 형식으로 출력한다.

설치 및 실행

다음은 Karma를 실제로 설치해서 설정한 후 실행하기까지의 간단한 과정을 소개한다.

Karma 설치

npm을 사용하면 간단하게 바이너리를 설치할 수 있다.

$ npm install --save-dev karma

Jasmine 플러그인 설치

Karma에서 Jasmine을 사용하기 위해서는 설정 파일을 통해 Jasmine의 소스 코드를 직접 페이지에 로드해야 한다. 플러그인을 사용하면 이러한 과정이 필요 없이 손쉽게 Jasmine을 사용할 수 있다. (Jasmine은 이미 설치되어 있다고 가정한다.)

$ npm install --save-dev karma-jasmine

크롬 런처 설치

브라우저를 자동으로 실행하려면 각 브라우저에 맞는 런처를 별도로 설치해야 한다. 예를 들어 크롬 브라우저를 실행하기 위해서는 karma-chrome-launcher를 설치해야 한다. 크롬 외에도 다양한 브라우저 런처를 제공하며, 지원 브라우저의 목록은 공식 홈페이지에서 확인할 수 있다.

$ npm install --save-dev karma-chrome-launcher

설정 파일

Karma는 프로젝트의 최상위 폴더에 있는 karma.config.js 파일로 설정 파일을 관리한다. 아래는 최소한의 내용만을 포함하는 간단한 설정 파일의 예시이며, 옵션에 대한 좀 더 상세한 설명은 공식 홈페이지에서 확인할 수 있다.

module.exports = (config) => {
  config.set({
    frameworks: ['jasmine'],   // Jasmine 테스트 프레임워크 사용
    files: [
      'src/**/*.js',      // 소스 파일 경로
      'test/**/*.spec.js'  // 테스트 파일 경로
    ],
    reporters: ['dots'],    // 리포터 지정 (점 형태로 결과 출력)
    browsers: ['Chrome'],  // 크롬 브라우저를 자동 실행을 위한 런처 지정
    singleRun: true         // 테스트 1회 실행 후 Karma 종료
  });
};

테스트 실행

먼저 package.json에 다음과 같이 npm 스크립트를 등록한다.

{
  // ...
  "scripts": {
    "test": "karma start"
  }
}

이제, 커맨드 라인에서 아래와 같이 편리하게 실행할 수 있다.

$ npm test

karma가 실행되면 먼저 브라우저가 열리고 테스트가 수행된 다음 아래와 같이 콘솔에 결과가 출력된다. 테스트가 완료되면 브라우저는 자동으로 닫힌다.

Screen Shot 2018-10-23 at 12 55 52 PM

테스트 커버리지 측정

작성한 테스트 코드의 커버리지는 Istanbul 라이브러리를 사용해 측정할 수 있다. Istanbul은 소스 코드를 분석해서 모든 줄마다 실행 횟수를 측정할 수 있는 코드를 삽입하는 방식으로 커버리지를 측정한다. 코드 실행 후에는 실행 결과를 HTML, LCOV, Cobertura 등의 다양한 포맷으로 출력해 주며, CI 서버에 연동해서 사용할 수도 있다.

Istanbul을 커맨드 라인에서 직접 실행할 수도 있지만, 일반적으로는 테스트 러너가 플러그인 형태로 제공하는 것을 사용한다. Karma의 경우 karma-coverage 플러그인을 사용하면 istanbul을 사용한 커버리지 측정 결과를 손쉽게 확인할 수 있다.

karma-coverage 플러그인 설치

$ npm install --save-dev karma-coverage

설정 파일 수정

설치가 완료되면, 설정 파일을 수정해야 한다. 위의 설정 파일 예제에서 reporters'coverage'를 추가하고, preprocessors 항목에도 커버리지 측정을 원하는 소스 파일에 대해 'coverage'를 추가해 준다.

module.exports = (config) => {
  config.set({
    frameworks: ['jasmine'],
    files: [
      'src/**/*.js',
      'test/**/*.spec.js'
    ],
    reporters: ['dots', 'coverage'], // 커버리지 리포터 추가
    coverageReporter: {
      type: 'html', // 커버리지 출력 형식 지정
      dir: 'coverage' // 커버리지 결과가 저장될 폴더를 지정
    },
    browsers: ['Chrome'], 
    singleRun: true,
    preprocessors: {   
      'src/**/*.js': ['coverage'] // 전체 소스 코드에 대해 커버리지 측정을 위한 전처리 지정
    }    
  });
};

실행 및 결과 확인

이제 다시 Karma를 실행해보면 다음과 같이 coverage라는 폴더 내부에, 실행된 브라우저 및 운영체제의 버전이 명시된 Chrome 69.0.3497 (Mac OX X 10.14.0) 폴더가 생성된 것을 확인할 수 있다. (만약 다수의 브라우저 런처를 사용했다면, 각 브라우저 별로 고유한 폴더가 생성된다.)

Screen Shot 2018-10-23 at 12 57 53 PM

src 폴더 안에 있는 index.html 파일을 브라우저에서 열어 보면 다음과 같이 커버리지 측정 결과가 파일별로 정리되어 있는 것을 확인할 수 있다.

Screen Shot 2018-10-23 at 12 58 56 PM

파일명을 클릭하면 개별 파일의 커버리지 측정 결과도 확인할 수 있다. 다음은 calc.js 파일에 대한 줄 단위의 커버리지 결과이다. 좌측 줄번호 옆의 1x 표시는 해당 줄이 전체 테스트가 진행되는 동안 한 번 실행되었다는 의미이다.

Screen Shot 2018-10-23 at 12 58 43 PM

크로스 브라우징 테스트

지금까지는 주로 개발자의 로컬 PC에서 이루어지는 테스트에 대해서 알아보았다. Karma의 브라우저 런처 플러그인을 사용하면 개발자의 로컬 PC에서도 다양한 브라우저에 대한 테스트를 동시에 실행할 수 있다. 하지만, 더 많은 기기를 지원해야 하는 프로젝트에서는 개발자 PC에서만 테스트를 하기가 불가능한 경우도 있다. 예를 들어 인터넷 익스플로러는 윈도우가 설치된 하나의 PC에 하나의 버전만 설치할 수 있기 때문에, 여러 버전의 인터넷 익스플로러를 테스트해야 하는 경우 여러 개의 PC 혹은 가상 머신을 사용할 수 밖에 없다.

이런 경우 karma와 Selenium WebDriver를 연결하여 사용하면 원격 PC를 사용해 테스트를 실행하고, 결과를 한 곳에 모아 출력할 수 있다. 간단히 설명하면 위의 예제에서 사용한 Chrome 런처 대신를 karma-webdriver-launcher로 변경해 주기만 하면 된다. 그러면 다음 그림과 같이 Hub의 역할을 하는 기기를 통해 연결된 원격 PC가 로컬 Karma 서버에 접근하도록 만들어 테스트를 실행하게 된다.

31878040-9e9691a2-b813-11e7-8413-e54dfafc9781

Selenium WebDriver에 대한 소개는 이 가이드 후반부의 "E2E 테스트 도구" 장에서 다루고 있지만, 직접 설치하고 테스트 환경을 구축하는 내용까지 다루지는 않는다. 설치와 관련된 내용은 멀티 브라우저 테스트 환경 구축하기를 참고하기 바란다.

Jest

Jest는 페이스북에서 만든 오픈소스 테스트 프레임워크이며, 최근 프론트엔드 개발에서 가장 활발하게 사용되는 테스트 도구이다. 꽤 오랜 기간 동안 개발되어 왔음에도 불구하고 한동안 관심을 받지 못하다가, 최근에 안정성 및 성능이 눈에 띄게 좋아지면서 많은 인기를 끌고 있다. Karma와는 다르게 Node.js 환경에서 실행되며, 내부적으로 Jasmine 스타일의 단언 API를 사용하기 때문에 기존에 Jasmine을 사용하고 있던 사용자들도 쉽게 적응할 수 있다.

이 가이드에서 Jest의 상세한 사용법은 다루지 않지만, Jest만이 갖고 있는 장점과 유용한 기능들을 간단히 소개하도록 하겠다.

쉬운 설치 및 실행

Jest의 가장 큰 장점은 쉬운 설치 및 사용 방법이라 할 수 있다. Jest는 테스트 러너의 기능뿐 아니라, 단언, 테스트 더블, 코드 커버리지 등 테스트에 필요한 모든 기능을 지원하기 때문에 별다른 추가 설치가 필요 없다. 또한 특별한 설정 없이 디폴트 설정 만으로도 실행할 수 있기 때문에, 처음 사용하는 사람도 손쉽게 테스트를 작성하고 실행해 볼 수 있다.

아주 간단한 예제를 통해 얼마나 쉽게 실행할 수 있는지를 알아보자. 우선 npm 명령을 사용해 간단히 설치할 수 있다.

$ npm install --save-dev jest

실행을 돕기 위해 package.jsontest 스크립트를 등록한다.

{
  //...
  "test": "jest"
}

만약 테스트 파일이 *.spec.js 형식을 따른다면 추가 설정이 필요 없이 바로 실행할 수 있다.

$ npm test

그러면 다음과 같이 결과가 터미널에 출력되는 것을 볼 수 있다.

Screen Shot 2018-10-23 at 3 18 03 PM

물론 숙련된 사용자들을 위한 다양한 설정 옵션도 제공한다. 별도의 설정 파일인 jest.conf.js 파일을 생성해서 설정 옵션들을 설정할 수 있으며, package.json 파일 내부에도 jest 프라퍼티를 사용해서 옵션을 설정할 수 있다. 예를 들어 테스트 파일의 경로를 지정하고 싶다면 package.json 파일 내부에 다음과 같이 testMatch 옵션을 설정하면 된다.

{
  "name": "my-project",
  "jest": {
    "testMatch": ["<rootDir>/test/**/*.spec.js"]
  }
}

각 옵션에 대한 좀 더 상세한 설명은 공식 홈페이지에서 확인할 수 있다.

쉬운 커버리지 측정

Jest의 커버리지 측정 역시 Karma와 마찬가지로 istanbul을 사용한다. 하지만, Jest에서는 이 기능 또한 통합된 형태로 제공되기 때문에 사용자는 별도의 설치나 추가 설정 없이 바로 사용할 수 있다. 위의 예제에서 커버리지를 확인하기 위해서는 커맨드 라인에서 실행할 때 --coverage 옵션만 추가하면 된다.

npm 스크립트를 실행할 때 옵션을 추가하기 위해선 -- 를 사용하면 된다. 다음과 같이 실행해보자.

$ npm test -- --coverage

실행이 완료되면 다음과 같이 커맨드 라인에 커버리지 측정 결과가 출력되고, 프로젝트 폴더에는 coverage 폴더가 자동으로 생성된다.

Screen Shot 2018-10-23 at 3 25 57 PM Screen Shot 2018-10-23 at 3 30 20 PM

lcov-report 폴더의 index.html 파일을 브라우저에서 열어 보면 위의 Karma 예제에서와 동일한 화면을 확인할 수 있다.

jsdom 내장

앞서 언급했듯이 Node.js 환경에서는 브라우저에서 제공하는 DOM이나 window 객체의 API를 사용할 수 없다. 그래서 프론트엔드 코드를 테스트하기 위해서는 이러한 API를 가상으로 구현한 환경이 추가로 필요한데, 그중 가장 완성도가 높고 널리 쓰이는 라이브러리가 바로 jsdom이다. 하지만 jsdom은 라이브러리 형태로 제공되기 때문에 실제 사용하기 위해서는 테스트를 실행할 때마다 초기화 관련 코드를 실행해 주어야 하며, 이는 브라우저 환경에서 직접 실행하는 것에 비해 번거로울 수밖에 없다.

Jest에서는 jsdom을 내장하여 테스트 실행할 때마다 필요한 환경을 자동으로 설정해서 제공하기 때문에, 별다른 추가 작업 없이 마치 브라우저 환경인 것 처럼 테스트를 작성할 수 있다.

스냅샷 테스트

스냅샷 테스트는 Jest의 상징과 같은 기능이라고 볼 수 있다. 간단히 말해, 스냅샷 테스트는 객체 내부의 상태를 그대로 파일로 저장해 놓고, 다음 테스트에서 객체의 현재 상태가 이전에 저장된 상태와 다른지를 비교하는 테스트를 말한다. 일종의 회귀 테스트라고 볼 수 있는데, 해당 객체 내부의 상태가 복잡한 경우 모든 상태에 대한 기댓값을 일일이 다 코드로 작성할 필요 없이 눈으로만 확인할 수 있어 테스트 작성이 쉬워진다는 장점이 있다.

스냅샷 테스트는 주로 리액트의 가상 DOM 구조를 비교하기 위해 사용되는데, 테스트를 작성할 때 다음과 같이 toMatchSnapshot() 함수만을 사용하면 된다.

import React from 'react';
import Link from './Link';
import renderer from 'react-test-renderer';

it('렌더링 확인', () => {
  const tree = renderer
    .create(<Link page="http://ui.toast.com">TOAST UI</Link>)
    .toJSON();
  expect(tree).toMatchSnapshot();
});

테스트를 실행하고 나면 자동으로 스냅샷 파일이 만들어지는데, 이 파일의 내용을 살펴보면 다음과 같이 해당 컴포넌트가 렌더링한 DOM의 구조가 그대로 저장된 것을 볼 수 있다.

exports[`렌더링 확인 1`] = `
<a
  className="normal"
  href="http://ui.toast.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  TOAST UI
</a>
`;

이렇게 스냅샷 파일이 생성되고 나면, 이후에 컴포넌트가 반환하는 DOM 구조가 변경되었을 때 테스트가 실패하게 된다. 사용자는 달라진 내용을 확인하고 의도된 수정이면 스냅샷을 갱신하고, 아닌 경우 문제를 해결하면 된다.

이러한 방식은 리액트가 아니더라도 복잡한 데이터 구조에 대한 테스트를 작성할 때는 언제든 활용할 수 있다. 다만, 스냅샷 테스트를 작성하는 경우 테스트 코드에서 테스트의 의도가 명확하게 드러나지 않는다는 단점이 있다. 특히 원하는 기댓값에 대한 고민 없이 의미 없는 테스트를 만들거나 테스트가 실패했을 때 세부 내용을 면밀히 살펴보지 않고 스냅샷을 갱신하는 등의 잘못된 테스트 습관을 만들 수도 있으므로 꼭 필요한 경우에만 주의해서 사용해야 한다.

테스트 파일 필터링

Jest는 테스트할 대상 파일을 구체적으로 지정할 수 있는 기능을 제공한다. 우선 Jest는 기본적으로 Git과 같은 버전 관리 도구와 연동하여 마지막 커밋 이후에 변경 사항이 있는 파일만을 테스트 대상에 포함한다. 이를 통해 이미 검증된 파일에 대해서 불필요한 테스트를 매번 실행하는 것을 방지할 수 있다.

커맨드 라인 인터페이스를 사용하면 파일을 더 구체적으로 선별할 수 있다. 한 번 실행한 이후에는 추가로 명령할 수 없는 보통의 러너들과는 달리, Jest는 인터랙티브한 커맨드 라인 인터페이스를 제공해서 실행 이후에도 키 입력을 통해 테스트 대상 파일들을 변경할 수 있는 기능을 지원한다. 이를 통해 현재 진행 중인 테스트를 취소하거나, 특정 파일명과 매치되는 테스트만 필터링해서 실행하도록 명령할 수 있다. 또한 스냅샷 테스트가 실패한 경우 결과를 확인한 후에 바로 스냅샷을 갱신하도록 만들 수도 있다.

다음 스크린샷 영상은 a 키를 통해 전체 파일을 테스트하도록 명령한 다음 q 키를 눌러서 취소하고, p 키를 눌러서 원하는 패턴의 파일만을 테스트하는 일련의 과정을 보여준다.

Screen Recording 2018-10-23 at 3 36 57 PM 2018-10-23 15_41_40

샌드박스 병렬 테스트

Node.js 환경에서 테스트를 하는 것의 가장 큰 장점은 바로 "속도"이다. Node.js의 프로세스는 브라우저의 프로세스보다 훨씬 가볍기 때문에 초기 구동 속도가 빠를 수밖에 없다. Jest는 이러한 특징을 이용해서 각각의 테스트 파일을 독립된 프로세스에서 실행한다. 이 경우 각각의 테스트가 사용하는 전역 객체나 모듈의 상태가 서로에게 영향을 미치지 않고 마치 샌드박스 내부에 있는 것처럼 실행되기 때문에 훨씬 안전하게 테스트를 할 수 있다.

단, 테스트를 순차적으로 실행하면서 개별 테스트마다 새로운 자식 프로세스를 생성하게 되면 단일 프로세스에서 실행하는 것보다 느려지게 된다. 이 문제를 해결하기 위해 Jest는 다수의 프로세스를 병렬로 실행하는 방식을 사용해 속도를 향상시키며, 내부적으로 CPU 코어의 수 등을 고려해 동시에 실행되는 프로세스의 개수를 적절하게 조절하여 최적화된 속도를 유지할 수 있도록 한다. 또한 앞서 설명한 "테스트 파일 필터링" 기능을 사용하면 불필요한 테스트 실행을 방지해서 속도를 향상시킬 수 있다.

Jest는 이러한 방식으로 테스트 실행 속도를 높은 수준으로 유지하면서도 훨씬 더 안전한 테스트 환경을 제공해 준다.

E2E 테스트 도구

지금까지 살펴본 Karma, Jest 등의 도구들은 모두 단위 테스트나 통합 테스트를 위한 도구라고 볼 수 있다. 사실 E2E 테스트는 작성이 번거롭고 실행 속도가 느리며 통제된 환경에서 테스트를 할 수 없다는 단점 때문에 개발자들이 개발 과정에 사용하기에는 어려움이 많았다. 하지만 프론트엔드 개발의 경우 UI/UX 관련 기능을 실제 사용자 환경과 분리된 상태에서 테스트하기에는 한계가 있었기 때문에, 사용자의 관점에서 테스트를 할 수 있는 E2E 테스트에 대한 필요성이 꾸준히 요구되어 왔다.

이러한 흐름에서 최근 CypressTestCafe 등의 새로운 도구들이 등장했는데, 이들은 기존 E2E 도구들이 갖던 단점을 최소화하여 E2E 테스트의 장점을 최대한 활용할 수 있도록 도와준다. 이 장에서는 기존에 가장 널리 사용되던 도구인 Selenium Webdriver와 최근 가장 인기 있는 도구인 Cypress를 비교해 보며, E2E 도구가 어떻게 발전하고 있는지를 살펴보도록 하겠다.

Selenium WebDriver

Selenium WebDriver는 흔히 Selenium, 혹은 WebDriver라고도 부르는데, 정확하게는 Selenium 2.0부터 새롭게 도입된 WebDriver API와 Selenium을 묶어서 부르는 이름이다. 이전 버전인 Selenium 1.0(Selenium RC)에서는 자바스크립트를 브라우저 내부에 삽입하는 방식을 사용한 반면, 2.0부터는 브라우저를 외부에서 제어하는 방식을 사용하기 때문에 더 구체적인 행위까지 제어할 수 있게 되었다. 또한 Selenium Grid를 사용해서 동일한 테스트를 다양한 기기에서 동시에 실행할 있는 기능도 제공한다.

WebDriver는 통일된 API를 기반으로 브라우저를 제어하기 위해 만든 HTTP 기반의 프로토콜이며, 독자적인 명세로 시작했지만 현재는 W3C에서 관리되고 있는 표준이 되었다. WebDriver는 기본적으로 브라우저가 서버 역할을 하고 제어를 요청하는 기기(개발자 PC 혹은 CI 서버)가 클라이언트의 역할을 하는 서버-클라이언트 구조라고 할 수 있으며, 브라우저용 드라이버와 개발자용 클라이언트를 설치해서 사용하게 된다.

예를 들어 브라우저가 클라이언트의 응답을 처리하기 위해서는 별도의 드라이버를 설치해야 하는데, 홈페이지에서 크롬, 인터넷 익스플로러, 파이어폭스, 사파리 등 다양한 브라우저를 지원하는 다양한 드라이버를 다운로드할 수 있다. 또한 WebDriver는 HTTP를 사용한 JSON 기반의 프로토콜이기 때문에 테스트 코드를 작성할 때 언어의 제약을 받지 않으며, 현재 자바스크립트뿐만 아니라 Java, C#, Ruby, Python 등의 언어를 지원하는 클라인언트를 다운로드할 수 있다.

특히 자바스크립트에는 WebDriver API를 사용한 Node.js 기반의 테스트 프레임워크가 이미 다양하게 존재하는데, 여기서 그중 몇 가지를 소개하겠다.

  • Protractor : Angular 프로젝트를 위한 테스트 프레임워크
  • WebdriverJS : Selenium Webdriver의 정식 Node.js 구현체이며, 낮은 수준(Low-level)의 API를 제공한다.
  • NightWatch : Mocha 기반의 테스트 러너와 직관적인 API, CI 서버 통합 등의 다양한 기능을 지원한다.
  • WebdriverIO : 테스트 러너, 정적 웹 서버, CI 서버 통합, REPL 인터페이스 등 다양한 기능을 지원하며 커뮤니티가 가장 잘 활성화되어 있다.

WebDriver는 현존하는 E2E 테스트 도구 중 가장 널리 쓰이는 도구라고 할 수 있다. 개발자와 전문 테스터 모두가 사용할 수 있으며 테스트뿐만 아니라 브라우저를 사용한 다양한 자동화 작업에 사용된다. 하지만 WebDriver는 앞서 설명한 E2E 테스트의 단점을 그대로 갖고 있기 때문에 테스트를 작성하거나 유지 보수하는데 많은 비용이 들며, 이로 인해 개발자들이 개발 단계에서 사용하는 테스트 도구로써는 사실상 널리 활용되지 못하고 있다.

Cypress

Cypress는 TestCafe와 함께 최근 가장 각광받고 있는 E2E 도구이며, WebDriver와는 다르게 실제 애플리케이션과 테스트 코드를 동일한 브라우저에서 실행하는 방식을 취하고 있다. 이 방식은 HTTP 등을 사용한 프로세스 사이의 통신이 필요 없이 동일한 프로세스 내부에서 테스트를 실행하기 때문에 테스트를 훨씬 빠르고 안정적으로 실행할 수 있다. 또한 브라우저 기반의 GUI를 사용하여 테스트의 실행 상태를 확인하고 디버깅할 수 있는 다양한 편의 기능을 제공한다.

예를 들면, 실행된 모든 테스트 명령과 각 명령이 실행될 때의 UI 상태를 스냅샷 형태로 모두 저장하고 있어, 특정 시점의 UI 상태를 눈으로 확인할 수 있다. 또한 전체 테스트 진행 과정을 동영상으로 저장하거나 테스트가 실패했을 때 자동으로 스크린샷을 남길 수 있어 테스트가 실패했을 때 원인을 파악하기가 매우 쉽다. 게다가 브라우저에서 실행되기 때문에 필요한 경우 크롬 개발자 도구를 사용해 디버깅을 할 수도 있다.

하지만 브라우저 내부에서 실행되는 방식에는 단점 또한 존재하며, Cypress의 공식 문서에 잘 정리되어 있다. 예를 들면, 브라우저의 새 탭 혹은 새 창을 열 수 없으며, 동일 출처(Same-origin) 정책을 벗어나는 페이지로는 이동을 할 수가 없다. 또한 브라우저가 실행할 수 있는 언어는 자바스크립트가 유일하기 때문에 다른 언어로 테스트를 작성할 수 없는 제약이 있다.

하지만 이러한 단점들은 범용 E2E 도구로써 사용할 때의 단점일 뿐 개발 단계에서 자바스크립트 코드를 테스트할 때는 크게 문제가 되지 않는다. Cypress는 Selenium WebDriver와는 전혀 다른 목적을 갖는 도구이며, 정확히 프론트엔드 개발자들이 개발 단계에서 사용하기에 최적화된 도구라고 할 수 있다. 특히 기존 E2E 도구의 가장 큰 단점이던 테스트 작성 비용과 테스트 실행 시간을 대폭 줄여주었기 때문에, 개발자들은 단위 테스트나 통합 테스트를 작성할 때와 비슷한 비용으로 E2E 테스트를 작성할 수 있게 되었다. E2E 테스트가 주는 많은 이점들을 생각해 볼 때, Cypress와 같은 도구가 발전할수록 개발자들은 점점 더 많은 E2E 테스트를 작성하게 될 것이다.

Cypress 설치 및 실행

이제 실제로 Cypress를 설치하고, 간단한 TodoMVC 애플리케이션을 테스트하는 코드를 작성해보자.

설치 및 설정

Cypress는 npm 명령으로 간단하게 설치할 수 있다.

$ npm install cypress --save-dev

Cypress는 별다른 설정 없이 바로 실행할 수 있지만, npm 스크립트를 등록하면 더 편하게 실행할 수 있다. package.json 파일에 아래의 스크립트를 입력하자.

{
  "scripts": {
    "cypress:open": "cypress open",
  }
}

인터랙티브 모드 실행

아래 명령을 커맨드 라인에 입력하면 Cypress가 실행된다.

$ npm run cypress:open

처음 Cypress를 실행하면 프로젝트 폴더에 cypress라는 폴더가 생성된다. 해당 폴더에는 처음 사용하는 사용자들을 위한 다양한 샘플 파일들이 포함되어 있다. 특히 integration/examples 폴더 내부에는 다양한 상황에서 어떤 식으로 테스트를 작성해야 하는지를 알려주는 테스트 명세가 포함되어 있어, 처음 Cypress의 사용 방법을 익히는 데에 큰 도움이 된다.

Screen Shot 2018-12-03 at 3 58 39 PM

Cypress가 실행되면 가장 먼저 위의 그림과 같이 Electron으로 만들어진 GUI 애플리케이션을 볼 수 있다. Tests 탭에 현재 작성된 테스트 명세 파일들이 표시되는데, 아직 테스트를 작성하지는 않았지만 integration/examples 폴더 내부에 포함된 샘플 파일들을 볼 수 있다. 이 중 actions.spec.js를 선택해서 실제로 테스트를 실행해보자.

Screen Shot 2018-12-03 at 4 14 30 PM

테스트를 선택하면 위의 그림과 같이 테스트 러너가 실행된 후에 개별 테스트가 순차적으로 실행되는 것을 눈으로 확인할 수 있다. 테스트 러너의 각 화면에 대한 간략한 설명은 다음과 같으며, 좀 더 상세한 설명은 공식 홈페이지에서 확인할 수 있다.

(1) 테스트 상태 메뉴 - 현재 성공 및 실패한 테스트의 개수와 소요된 시간 등을 표시하는 영역이다. (2) URL 프리뷰 영역 - 현재 실행 중인 애플리케이션의 URL이 표시되는 영역이다. (3) 명령(command) 로그 영역 - 테스트 명세 내에서 실행된 모든 명령의 로그가 표시되는 영역이다. - 마우스를 올리거나 클릭하면 해당 명령이 실행될 시점의 애플리케이션 상태를 확인할 수 있다. (4) 앱 프리뷰 영역 - 실제 실행 중인 애플리케이션 화면이 표시되는 영역이다.

위의 그림에서 명령 로그 영역에 보라색으로 표시된 부분(A)이 현재 클릭된 명령이며, 이 경우 앱 프리뷰 영역에서 해당 명령에 영향을 받는 DOM 엘리먼트를 하이라이팅 처리(B) 해 준다. 또한 앱 프리뷰 영역의 하단에 있는 레이어(C)를 통해 명령이 실행되기 직전과 직후의 모습을 비교해볼 수 있다.

백그라운드 실행

Cypress를 open 명령을 통해 실행하게 되면 위와 같이 GUI 화면과 함께 인터랙티브 모드로 실행된다. 이 기능은 개발할 때에는 유용하지만 CI 서버와 같은 환경에서는 불필요한데, 이 경우 run 명령을 통해 실행하면 GUI 없이 백그라운드로 실행할 수 있다. package.json 파일에 다음과 같이 스크립트를 추가한 다음 npm 명령어를 실행해보자.

{
  "cypress:run": "cypress run",
}
$ npm run cypress:run

아무런 옵션 없이 run 명령을 실행하면 모든 테스트를 실행하기 때문에 시간이 좀 오래 걸릴 것이다. 테스트가 진행되면서 다음과 같이 개별 테스트에 대한 결과가 화면에 표시되는 것을 볼 수 있다.

Screen Shot 2018-12-03 at 5 08 56 PM

마지막 테스트까지 실행되고 나면 다음과 같이 전체 테스트에 대한 결과를 정리해서 보여준다.

Screen Shot 2018-12-03 at 5 08 26 PM

테스트의 전체 진행 상황은 동영상으로 자동으로 저장되며, cypress/videos/examples 폴더에 가면 테스트 파일별로 별도의 mp4 형식의 동영상 파일이 생성된 것을 확인할 수 있다.

Cypress 테스트 코드 작성

이제 Todo MVC 애플리케이션을 사용해 아주 간단한 테스트를 작성하면서 실제로 Cypress의 API를 경험해 보도록 하자. Todo MVC의 예제는 홈페이지를 통해 쉽게 구할 수 있으며 어떤 프레임워크를 사용하든 상관없기 때문에 애플리케이션 설치 과정은 따로 다루지 않겠다. 이 가이드에서는 http://localhost:8888에 Todo MVC 애플리케이션이 실행되어 있다고 가정한다.

테스트 파일 생성 및 실행

가장 먼저 해야 할 일은 테스트 파일을 추가하는 일이다. cypress/integration 폴더에 todo.spec.js 파일을 추가한 후에 다음과 같이 입력하자.

describe('Todo MVC', () => {
  beforeEach(() => {
    cy.visit('http://localhost:8888');
  });
});

Cypress는 내부적으로 Mocha의 API를 사용하는데, describe, beforeEach 등의 함수명이 앞서 살펴본 Jasmine의 API와 거의 유사한 것을 볼 수 있다. 유일한 다른 점은 전역 객체인 cy인데, Cypress에서는 모든 명령을 이 cy 객체를 통해서 실행한다고 생각하면 된다. 위의 코드에서 cy.visit은 인자로 넘겨진 URL로 접속해서 페이지를 로드하기 위한 명령이다.

테스트 코드 작성하기

이제 커맨드 라인에 npm run cypress:open을 입력해 Cypress를 실행한 다음 테스트 러너에서 todo.spec.js를 클릭해서 해당 테스트 파일을 실행해보자. 아직은 테스트를 작성하지 않았기 때문에 명령 로그 영역에 "No tests found in file"이라는 메시지가 표시될 것이다. 이제 실제로 테스트를 작성해보자.

describe('Todo MVC', () => {
  beforeEach(() => {
    cy.visit('http://localhost:8888');
  });
  
  it('Todo 입력하기', () => {
    // 클래스가 new-todo인 엘리먼트에 "Cypress 실습"을 입력한 후 엔터키 입력
    cy.get('.new-todo').type('Cypress 실습{enter}'); 

    // 클래스가 new-todo인 엘리먼트의 value가 빈 문자열이어야 한다.
    cy.get('.new-todo').should('have.value', '');
    
    // 클래스가 todo-list인 엘리먼트의 첫번째 자식 li는 "Cypress 실습" 이라는 텍스트를 포함한다.
    cy.get('.todo-list li').eq(0).should('contain', 'Cypress 실습');
    
    // 클래스가 todo-count인 엘리먼트는 "1 item left" 라는 텍스트를 포함한다.
    cy.get('.todo-count').should('contain', '1 item left');
  });
});

위의 코드에서 볼 수 있듯이 cy 객체에서 실행되는 대부분의 함수들은 cy 객체를 다시 반환하기 때문에 체이닝(Chaining) 형태로 호출할 수 있다. 여기서 cy 객체의 모든 API에 대해 소개할 수는 없기 때문에 좀 더 자세한 내용은 공식 API 문서를 참고하길 바라며, 대신 이해를 돕기 위해 각각의 코드 위에 한글로 주석을 달아놓았다. 하지만 get, type, should 등의 API가 아주 직관적이고 단순하게 설계되어 있기 때문에 굳이 설명이 없어도 테스트가 검증하려고 하는 것들이 무엇인지 한눈에 알 수 있을 것이다.

위의 코드를 저장하면 Cypress가 코드 변경을 감지한 후 테스트 러너가 재실행되고, 실제 테스트가 진행되는 것을 볼 수 있다. 테스트가 완료된 후에는 아래와 같은 화면을 볼 수 있을 것이다. 좌측 명령 로그 영역에는 위에서 작성한 모든 명령들이 기록되어 있으며, 각 명령들에 마우스를 올리거나 클릭하면 해당 명령이 실행될 때의 화면 상태를 모두 확인할 수 있다.

Screen Shot 2018-12-03 at 9 08 26 PM

이상으로 Cypress를 사용한 간단한 테스트까지 작성해보았다. Cypress는 이 외에도 E2E 테스트 작성을 도와주는 수많은 기능을 제공하고 있다. Cypress의 공식 가이드에는 튜토리얼 및 테스트 작성 가이드, API 등 모든 문서가 아주 친절하고 자세하게 작성되어 있으므로, 더 깊은 내용을 알고 싶다면 꼭 홈페이지에 있는 문서를 하나하나 꼼꼼하게 읽어보길 바란다.

맺음말

지금까지 Jasmine, Karma, Jest, Selenium WebDriver, Cypress등 프론트엔드 환경에서 사용되는 다양한 테스트 도구들에 대해 살펴보았다. 이토록 다양한 테스트 도구가 만들어진 이유는 자바스크립트가 그만큼 많은 영역에서 사용된다는 의미이며, 동시에 자바스크립트의 발전과 더불어 개발 도구들도 끊임없이 발전하고 있다는 반증일 것이다. 이 문서를 통해 각각의 도구들이 갖는 장단점을 잘 이해하고, 프로젝트의 성격에 맞는 도구를 잘 활용해서 제품의 품질과 코드의 품질을 모두 높일 수 있게 되길 바란다.


이 문서의 내용과 연관된 FE개발랩 사내 교육은 아래와 같다. 추가로 교육을 수강할 것을 권장 한다.

  • 테스트
  • 웹팩을 이용한 자바스크립트 실전 개발

이 문서는 NHN Cloud의 FE개발랩에서 작성하고 관리하는 공식 웹 프론트 개발 가이드이다. 가이드 적용 관련 문의나 문서의 오류, 개선 제안은 공식 문의 채널(dl_javascript@nhn.com)을 통해 할 수 있다.


Last Modified
2019. 03. 29
FE Development LabBack to list