Jest 활용 경험: 테스팅에 대한 고민과 통찰

소프트웨어 개발 과정 중 꼭 필요한 것이 바로 '테스트'입니다. IMQA는 많은 테스트 프레임워크 중 왜 Jest를 선택했는지, 어떻게 활용하고 있는지 정리해 보았습니다.

작성일 2023년 08월 03일

오늘은 '테스팅'이라는 프로그래밍 세계의 필수적인 요소에 대한 이야기를 나누려 합니다. 그중에서도 JavaScript 환경에서 널리 사용하는 테스팅 프레임워크 'Jest'를 효과적으로 사용하는 방법에 대한 제 경험과 진행하는 과정에서 겪은 고민을 공유하려 합니다.

소프트웨어 테스트의 중요성

소프트웨어를 개발하면서 테스트는 선택이 아니라 필수입니다. 코드의 신뢰성을 확보하고, 예상치 못한 버그를 미리 찾아내기 위한 중요한 과정이죠. 이야기에 앞서, SW 테스트의 중요성을 알 수 있는 사례를 살펴보겠습니다.

T-money 시스템 오류로 인한 무료 개방 사례

2004년 7월 1일 서울특별시의 새로운 대중교통 시스템이 도입되었다. 시스템이 도입되기 전 시스템의 통합을 담당한 L사에서는, 제대로 테스트를 거치지 못한 상황에서 서비스를 개시하는 것은 무리이므로 도입 일정을 연기하는 것을 제안하였으나, 서울시는 일정을 연기하지 않았다. 이로 인해 새 대중교통 시스템 도입 첫날 시스템 오류로 인해 전체 대중교통 수단이 무료로 개방되었다.
교육행정정보시스템(나이스) 내신 석차 오류 사례

2011년 7월 국내에서는 S사가 정부 기관에 납품한 전산시스템에서 오류가 발생, 고등학생 2만 9,007명의 내신 석차와 등급이 잘못 산정되어 사회적으로 큰 파장을 일으켰다.컴퓨터는 소수점 32번째 자리까지만 인식을 하고 마지막 자리는 임의의 숫자를 적용하므로, 통상 프로그램 개발자는 소수점 16번째 자리까지만 값을 인식하도록 인위적으로 계산 방식을 보정해 왔다. 이 문제는 소수점의 보정 과정이 이루어지지 않아 발생하였다.

위 사례를 통해 충분히 테스트를 진행하지 않고, 소프트웨어를 출시했을 때 어떤 리스크가 생기는지 알 수 있습니다.

Jest 탄생과 설계의 목적

Jest는 2014년, Facebook이 개발하여 오픈 소스로 공개한 JavaScript 테스팅 프레임워크입니다.  Facebook은 Mocha, SuperTest, Chai 등 기존에 다양한 JavaScript 테스트 프레임워크가 있음에도 왜 Jest라는 테스트 프레임워크를 만들었을까요?

Mocha, SuperTest와 같은 JavaScript 테스트 프레임워크들이 유용하게 사용되고 있었지만, Facebook은 채팅 기능이 JavaScript로 재작성 되면서 자신들의 코드 베이스에 맞는 새로운 프레임워크가 필요했기 때문입니다.

Facebook의 대규모 코드 베이스는 기존의 테스트 도구로는 효율적으로 관리하기 어려웠을 뿐만 아니라, 여러 가지 도구를 조합하여 사용하면서 생기는 복잡성을 줄이고자 하는 필요성도 있었습니다.

이러한 배경에서 Facebook은 다음과 같은 설계 목적을 통해 Jest를 개발하였습니다.

  1. 단순함
    Jest는 설정이 거의 필요 없는 테스트 프레임워크를 목표로 하였습니다. 이로 인해 개발자는 복잡한 설정 절차 없이도 빠르게 테스트를 작성하고 실행할 수 있습니다. 이는 테스트 작성의 진입 장벽을 낮추고, 더 많은 테스트 코드 작성을 유도하여 코드 품질 향상에 기여합니다.
  2. 성능
    Jest는 매우 빠르게 테스트를 수행합니다. 이는 큰 프로젝트에서도 빠른 피드백을 받을 수 있게 해주므로, 효율적인 개발이 가능하게 합니다.
  3. 풍부한 기능
    Jest는 모의 함수, 타이머, 비동기 테스트 등과 같이 다양한 테스팅 기능을 제공합니다. 이들 기능은 단위 테스트와 통합 테스트를 간편하게 작성하는 데 도움을 줍니다.
  4. 배려
    Jest는 개발자가 최대한 적은 노력으로 테스트를 수행할 수 있도록 설계되었습니다. 예를 들어, Jest는 실패한 테스트를 먼저 실행하고, 관련 없는 테스트는 실행하지 않음으로써 테스트 수행 시간을 최소화합니다.
  5. 개방성과 커뮤니티
    Jest는 오픈 소스 프로젝트로, 커뮤니티의 기여를 적극적으로 받아들입니다. 이는 Jest가 끊임없이 발전하고, 다양한 요구 사항을 충족할 수 있게 해줍니다.

Jest의 장점

Jest에는 여러 가지 장점이 있지만, 그중 제가 Jest를 사용할 때 느낀 장점은 다음과 같습니다.

  1. 범용성 - JavaScript 기반 프로젝트에 쉽게 적용 가능
    IMQA 솔루션은 대부분 Node.js으로 구성되어 있습니다. Jest는 JavaScript가 기반의 프로젝트 (Babel, TypeScript, Node, React, Angular, Vue)에서 사용할 수 있습니다. 또한 아래 명령어를 이용하여 간편하게 설치할 수 있습니다.
//Jest를 설치하기 위한 command 명령어
npm install jest

// package.json를 설정하여 Jest를 테스트 도구로 설정
{
  "scripts": {
    "test": "jest"
  }
}

2. 의존성 줄이기 - 직관적인 테스트 결과, 별도의 외부 툴 없이 결과 확인 가능
Jest는 자체적으로 테스트 결과를 보여주는 모듈이 내장되어 있어 별도로 설치하지 않는 장점이 있습니다.

위의 이미지는 Sum 함수와 관련 테스트 코드 및 테스트 코드 결과를 보여줍니다. 해당 테스트를 실행하였을 때 기대했던 값은 70이나,  테스트 결과는 80으로 나와 실패했습니다. 이처럼 Jest는 테스트 결과를 직관적으로 표출해 성공/실패 여부를 쉽게 알 수 있습니다.

3. 테스트 시간 감소 - 테스트 병렬 실행, 총 테스트 시간 절약

두 개의 테스트 코드가 있다고 가정해 봅시다. 두 개의 테스트 코드를 간단하게 설명해 드리자면, 다음과 같습니다.

  • 타이머 시작 → sleep 5초 부여 → 타이머 종료 function

두 개의 코드는 별개의 테스트 코드여서 만약 병렬로 실행할 수 없다면, 각각 5초가 걸려 10초 이상의 테스트 소요 시간이 발생합니다. 그러나 Jest는 해당 명령어를 통해 각 테스트를 병렬로 실행할 수 있습니다.

npm test --maxWorkers=2

여기서 maxWorkers Option은 병렬로 실행할 최대 개수 나타냅니다. 해당 코드를 실제 테스트한 결과는 다음과 같습니다.

이처럼 Jest는 각 테스트 파일을 독립된 프로세스에서 실행하며, 이를 통해 테스트 시간을 단축할 수 있습니다.

Jest 사용 고민

이제 본격적으로 Jest를 사용하면서 제가 느꼈던 내용에 대해 설명드리고자 합니다.

관리 방법

프로젝트를 진행하면서 가장 큰 고민 중 하나는 '테스트 코드를 어떻게 작성하고 관리하는가' 였습니다.

IMQA 팀에서는 효율적인 코드 관리와 신뢰성 있는 서비스 제공을 위해 테스트 코드 작성을 매우 중요하게 생각합니다. 하지만 프로젝트가 커질수록, 기능이 복잡해질수록 테스트 코드를 작성하고 관리하는 것은 쉽지 않은 일이었습니다.

이러한 상황에서 기존에 사용 중인 Jest를 어떻게 활용해야 할지, 어떻게 테스트 코드를 작성하고 관리할지, 우리에게 맞는 활용법에 대해 고민하기 시작하였습니다.

우리 상황에 맞는 테스트 기법 / 기준 정하기

테스트는 여러 가지 기법과 기준이 있습니다. 그중에서 우리는 어떠한 기준으로 테스트 코드를 구성해야 할지 고민했습니다.

  1. 테스트 코드의 작성
    우리는 어떤 규칙을 가지고 테스트 코드를 작성해야 하는가?
  2. 테스트 커버리지 활용
    - 테스트 커버리지는 어떻게 만드는가?
    - 우리의 테스트 커버리지 목표는 무엇인가?
    - 테스트 커버리지는 어떻게 활용해야 할까?
  3. 테스트 코드 구조화
    테스트 코드를 어떻게 구조화(정의)할 것인가?

Jest  활용하기

우리의 상황에 맞는 테스트 기법과 기준으로 고민하던 사항은 Jest 도입을 통해 어느 정도 답을 찾을 수 있었습니다.

1. 테스트 코드 작성

테스트 코드 작성에 앞서 이해를 돕기 위해 IMQA에 대해서 간단하게 설명드리겠습니다. IMQA는 SDK(AOS/IOS)에서 전달받은 덤프 데이터를 수집, 가공 처리를 한 데이터를 통해 앱에 대한 성능 모니터링 및 장애를 감지하는 솔루션입니다.

앱에서는 사용자 행동분석, CPU 사용량, 화면 응답시간 등 여러 데이터를 추출합니다. 이 중 스택 덤프는 앱의 병목 구간을 측정하는 목적으로 추출합니다.

추출한 스택 덤프에는 실행 중인 스레드의 스택 트레이스(stack trace), 즉 실행 중인 함수의 목록과 그 순서, 각 함수에 대한 변수와 파라미터, 그리고 프로그램 카운터와 스택 포인터와 같은 저수준의 상세 정보가 포함됩니다. 이 정보를 사용하여 개발자는 실패한 위치와 원인을 파악하고 버그를 수정할 수 있습니다.

그러나 이렇게 중요한 스택임에도 불구하고 중복된 데이터, 낮은 자원을 사용한 데이터 등 불필요한 데이터들도 포함되어 있습니다. 따라서 IMQA에는 스택 덤프를 필터링 처리합니다.

이때, 스택 덤프에서 중복되거나 불필요한 데이터에 대한 필터링 기능을 어떻게 테스트 코드를 구현할지 난감하였습니다. 여러 고민 끝에 데이터가 필터링되는 과정에 대한 시나리오를 작성하였고, 이를 테스트 코드에 그대로 반영하였습니다.

실제 데이터 필터링 작업은 정해진 순서와 규칙으로 인해 복잡합니다. 대신 비즈니스 로직 없이 쉽게 이해하기 위해 데이터 필터링 과정을 간략하게 작성하였습니다.

// ... 중략 ...
const body = {
	"project_id" : 2,
	"response_time" : 30000
}

async function getProjectInfo(body){
	if (body.project_id === 2){
		return true;
	} else {
		return false;
	} 
}

async function isDataLimitExceeded(body){
	if (body.response_time >= 30000){
		return true;
	} else {
		return false;
	}
}

// ... 중략 ...

async function dumpFiltering(body) {

	// 1. 프로젝트 정보 조회
	const project_exist = await getProjectInfo(body);
	if (project_exist === false){
		return ;
	}
	     
	// 2. 데이터 필터링 조건 체크
	if (await isDataLimitExceeded(body.project_id)) {
		return ;
	}

}

await dumpFiltering(body);
// ... 중략 ...

여기서 나올 수 있는 시나리오는 다음과 같습니다.

  1. 프로젝트 정보 조회
    a. 해당 조건에 만족하는 데이터 없음 - 1번
    b. 해당 조건에 만족하는 데이터 있음  - 다음 시나리오 이동
  2. 데이터 필터링 조건 체크
    a. 설정 정보에 해당 프로젝트가 필터링 검사 대상에 포함되지 않는 경우 - 2번
    b. 설정 정보에 해당 프로젝트가 필터링 검사 대상에 포함된 경우 - 3번

해당 기능에 대해서 발생할 수 있는 시나리오는 총 3개입니다. 테스트 코드는 각 시나리오에 맞게 구성합니다.

let exist;
let exist_not_filter;
let not_exist;

async function getProjectInfo(body) {
  if (body.project_id === 2) {
    return true;
  }
  return false;
}

async function isDataLimitExceeded(body) {
  if (body.response_time >= 30000) {
    return true;
  }
  return false;
}

beforeAll(() => {
  exist = {
    project_id: 2,
    response_time: 30000,
  };
  exist_not_filter = {
    project_id: 2,
    response_time: 10000,
  };

  not_exist = {
    project_id: 3,
    response_time: 20000,
  };
});

describe('프로젝트 덤프 체크 테스트', () => {
  test('프로젝트 조회시 정보 없음', async () => {
    const project_info = await getProjectInfo(not_exist);
    expect(project_info).toBeFalsy();
  });

  test('덤프 데이터 필터링 조건에 해당 되지 않을때 필터링 미동작 확인', async () => {
    const result = await isDataLimitExceeded(exist_not_filter);
    expect(result).toBeFalsy();
  });

  test('데이터 필터링 조건에 해당 될때 필터링 기능 확인', async () => {
    const result = await isDataLimitExceeded(exist);
    expect(result).toBeTruthy();
  });
});

이때 하나의 기능 단위를 describe, 세부적인 단위를 test로 정의합니다. 자세한 내용은 뒤에 테스트 코드 구조화에서 설명해 드리겠습니다.

2. 테스트 커버리지 활용

우선 테스트 커버리지가 무엇인지, 부터 설명하고자 합니다. '테스트 커버리지' 혹은 '코드 커버리지'라고 불리며 소프트웨어의 테스트를 논할 때 얼마나 테스트가 충분한지를 나타내는 지표 중 하나입니다.

말 그대로 코드가 얼마나 커버되었는 지를 나타내며, 소프트웨어 테스트를 진행했을 때 코드 자체가 얼마나 실행되었는지 알 수 있습니다.

테스트 커버리지를 만들기 위해서 별도의 모듈을 설치하는 여러 테스팅 프레임워크와 달리, Jest는 테스트 커버리지가 내장되어 있어 별도의 모듈을 설치할 필요가 없습니다. 우선 테스트 커버리지를 만들려면 다음과 같이 세팅해 주어야 합니다.

"scripts" : {
	"coverage": "jest --coverage"
}

세팅이 완료되면 해당 명령어를 실행합니다.

// 일부의 테스트 코드만을 실행하는 경우 뒤에 폴더명 혹은 파일명을 붙여준다.
npm run coverage (파일명/폴더명)

해당 명령어를 실행하면 아래와 같은 커버리지가 생성됩니다.

생성된 커버리지는 다음과 같이 활용할 수 있습니다.

1. 커버리지 결과 점수 기반으로 목표를 정할 수 있습니다.

이상적인 커버리지 점수가 따로 정해져 있진 않지만, 보통의 가이드라인은 60%면 용인되는 수준, 75%는 칭찬할 만한 수준, 90%는 모범적인 수준으로 봅니다. 하지만 이는 전사적으로 강제하는 기준은 아니며 비즈니스 요구에 맞춰 얼마를 달성할 것인지 정해야 합니다.

2. 테스트 코드 실행이 누락되었는지 체크할 수 있습니다.

테스트 코드를 전체적으로 실행하다 보면 특정 function이 미 실행되는 등 예상치 못한 예외 사항을 보다 쉽게 파악할 수 있습니다.

3. 테스트 코드 구조화

테스트 코드를 작성하는 데 있어 구조화를 어떻게 해야 할지 고민이 많았습니다. 그러나 Jest에서 권장하는 테스트 코드 구조가 있어 쉽게 테스트 코드를 구조화할 수 있었습니다.

해당 부분을 설명하기 위해 테스트 코드 작성에서 설명해 드렸던 덤프 데이터 필터링 기능을 다시 인용해 보겠습니다.

// ... 중략 ...
beforeAll(() => {
  exist = {
    project_id: 2,
    response_time: 30000,
  };
  exist_not_filter = {
    project_id: 2,
    response_time: 10000,
  };

  not_exist = {
    project_id: 3,
    response_time: 20000,
  };
});

describe('프로젝트 덤프 체크 테스트', () => {
  test('프로젝트 조회시 정보 없음', async () => {
    const project_info = await getProjectInfo(not_exist);
    expect(project_info).toBeFalsy();
  });

  test('덤프 데이터 필터링 조건에 해당 되지 않을때 필터링 미동작 확인', async () => {
    const result = await isDataLimitExceeded(exist_not_filter);
    expect(result).toBeFalsy();
  });

  test('데이터 필터링 조건에 해당 될때 필터링 기능 확인', async () => {
    const result = await isDataLimitExceeded(exist);
    expect(result).toBeTruthy();
  });
});

위와 같이 테스트 코드를 작성했다고 가정했을 때 테스팅 코드는 아래와 같이 정의 내릴 수 있습니다.

  1. describe
    기능 단위의 테스트를 진행할 때 관련 테스트를 그룹화 합니다.
  2. beforeAll
    테스트 코드 실행 전/후 모든 테스트 공유에 필요한 초기 설정 시 사용됩니다.
  3. test
    기능 단위의 테스트를 진행할 때 개별적인 테스트를 정의합니다.

기본적인 규칙을 기반으로 테스트 코드를 작성하여 테스트 코드를 쉽게 구조화할 수 있으며,  단위 테스트 코드를 보다 쉽게 이해할 수 있었습니다.

Jest 사용 효과

Jest를 기반으로 테스팅 코드를 작성하게 되면서 제가 느꼈던 사용 효과는 다음과 같습니다.

  1. 코드의 견고성 향상
    Jest를 사용하면 여러 시나리오에 대해 코드를 작성하여 테스트를 진행하여 예상치 못한 에러를 사전에 찾아낼 수 있습니다. 이는 전체적인 코드의 견고성을 높이며, 실제 사용자가 사용할 때 발생할 수 있는 문제를 미리 방지하는 데 도움이 됩니다.
  2. 코드 리뷰의 품질 향상
    Jest를 사용하여 작성된 테스트 케이스는 코드 리뷰에서도 중요한 역할을 합니다. 테스트 케이스를 통해 코드의 의도를 명확하게 알 수 있으므로, 리뷰어가 보다 효과적인 피드백을 제공할 수 있습니다.
  3. 코드 파악이 쉬워짐
    테스트 코드 자체가 곧 기능 명세가 되므로 코드만으로도 어떻게 동작 하는지 파악할 수 있습니다.

맺음

Jest를 사용해 보고 난 후, 저는 테스트 작성이 얼마나 쉬워졌는지, 코드 퀄리티가 어떻게 향상되었는지 느낄 수 있었습니다. Jest는 테스트 작성을 쉽게 해주며, 우리의 코드가 예상대로 작동하고 있는지 확인하는 데 많은 도움이 되었습니다. 이러한 경험을 바탕으로, 저는 다른 개발자들에게도 Jest를 사용해 보기를 적극 권장합니다.

단, 무조건 Jest를 맹신하고 사용하는 것은 아닙니다. Jest 또한 메모리 누수 이슈, complication cache (Node 16 이상) 발생으로 인한 속도 저하 등 몇 가지 이슈들이 발생하고 있어 완벽하지 않습니다. 그러므로 각자의 개발 환경을 기반으로 여러 가지 테스팅 프레임워크의 장∙단점을 비교하며 신중하게 테스팅 프레임워크를 선택하셨으면 좋겠습니다.


<참고 자료>

Share on

Tags

IMQA 뉴스레터 구독하기

국내외 다양한 기술 소식을 선별하여 매월 전달해드립니다. IMQA 뉴스레터를 통해 기술 이야기를 함께해보세요.

구독하기