실제로 통합 테스트를 수행하기 위한 Node.js 가이드

게시 됨: 2022-03-11

통합 테스트는 두려워할 대상이 아닙니다. 이는 애플리케이션을 완전히 테스트하는 데 필수적인 부분입니다.

테스트에 대해 이야기할 때 우리는 일반적으로 작은 덩어리의 코드를 격리하여 테스트하는 단위 테스트를 생각합니다. 그러나 응용 프로그램은 작은 코드 덩어리보다 크고 응용 프로그램의 거의 모든 부분이 독립적으로 작동하지 않습니다. 여기에서 통합 테스트가 그 중요성을 증명합니다. 통합 테스트는 단위 테스트가 부족한 부분을 찾아내고, 단위 테스트와 종단 간 테스트 간의 격차를 해소합니다.

통합 테스트를 작성해야 한다는 것을 알고 있는데 왜 하지 않습니까?
트위터

이 기사에서는 API 기반 애플리케이션의 예제를 사용하여 읽기 가능하고 구성 가능한 통합 테스트를 작성하는 방법을 배웁니다.

이 기사의 모든 코드 예제에 JavaScript/Node.js를 사용하지만 논의된 대부분의 아이디어는 모든 플랫폼의 통합 테스트에 쉽게 적용할 수 있습니다.

단위 테스트 대 통합 테스트: 둘 다 필요

단위 테스트는 하나의 특정 코드 단위에 초점을 맞춥니다. 종종 이것은 특정 방법 또는 더 큰 구성 요소의 기능입니다.

이러한 테스트는 모든 외부 종속성이 일반적으로 스텁되거나 조롱되는 격리되어 수행됩니다.

즉, 종속성이 사전 프로그래밍된 동작으로 대체되어 테스트 결과가 테스트 중인 장치의 정확성에 의해서만 결정되도록 합니다.

여기에서 단위 테스트에 대해 자세히 알아볼 수 있습니다.

단위 테스트는 좋은 디자인으로 고품질 코드를 유지하는 데 사용됩니다. 또한 모서리 케이스를 쉽게 덮을 수 있습니다.

그러나 단점은 단위 테스트가 구성 요소 간의 상호 작용을 다룰 수 없다는 것입니다. 여기에서 통합 테스트가 유용합니다.

통합 테스트

단위 테스트가 가장 작은 코드 단위를 격리하여 테스트하여 정의되는 경우 통합 테스트는 정반대입니다.

통합 테스트는 상호 작용에서 더 큰 여러 단위(구성 요소)를 테스트하는 데 사용되며 때로는 여러 시스템에 걸쳐 있을 수도 있습니다.

통합 테스트의 목적은 다음과 같은 다양한 구성 요소 간의 연결 및 종속성에서 버그를 찾는 것입니다.

  • 유효하지 않거나 잘못 정렬된 인수 전달
  • 손상된 데이터베이스 스키마
  • 잘못된 캐시 통합
  • 비즈니스 논리의 결함 또는 데이터 흐름의 오류(이제 더 넓은 관점에서 테스트가 수행되기 때문).

테스트 중인 구성 요소에 복잡한 논리가 없는 경우(예: 순환 복잡성이 최소화된 구성 요소) 통합 테스트는 단위 테스트보다 훨씬 더 중요합니다.

이 경우 단위 테스트는 주로 좋은 코드 디자인을 시행하는 데 사용됩니다.

단위 테스트는 기능이 제대로 작성되었는지 확인하는 데 도움이 되지만 통합 테스트는 시스템이 전체적으로 제대로 작동하는지 확인하는 데 도움이 됩니다. 따라서 단위 테스트와 통합 테스트는 각각 고유한 보완 목적을 수행하며 둘 다 포괄적인 테스트 접근 방식에 필수적입니다.

단위 테스트와 통합 테스트는 동전의 양면과 같습니다. 동전은 둘 다 없으면 유효하지 않습니다.

따라서 통합 및 단위 테스트를 모두 완료할 때까지 테스트가 완료되지 않습니다.

통합 테스트를 위한 제품군 설정

단위 테스트를 위한 테스트 스위트를 설정하는 것은 매우 간단하지만 통합 테스트를 위한 테스트 스위트를 설정하는 것은 종종 더 어렵습니다.

예를 들어 통합 테스트의 구성 요소에는 데이터베이스, 파일 시스템, 이메일 공급자, 외부 지불 서비스 등과 같은 프로젝트 외부에 있는 종속성이 있을 수 있습니다.

경우에 따라 통합 테스트는 이러한 외부 서비스 및 구성 요소를 사용해야 하며 때로는 스텁될 수 있습니다.

필요할 때 몇 가지 문제가 발생할 수 있습니다.

  • 취약한 테스트 실행: 외부 서비스를 사용할 수 없거나 잘못된 응답을 반환하거나 잘못된 상태일 수 있습니다. 어떤 경우에는 이것이 위양성(false positive)이 될 수 있고 다른 경우에는 위음성(false negative)이 될 수 있습니다.
  • 느린 실행: 외부 서비스 준비 및 연결이 느릴 수 있습니다. 일반적으로 테스트는 CI의 일부로 외부 서버에서 실행됩니다.
  • 복잡한 테스트 설정: 외부 서비스는 테스트를 위해 원하는 상태여야 합니다. 예를 들어 데이터베이스에는 필수 테스트 데이터 등이 미리 로드되어 있어야 합니다.

통합 테스트를 작성할 때 따라야 할 지침

통합 테스트에는 단위 테스트와 같은 엄격한 규칙이 없습니다. 그럼에도 불구하고 통합 테스트를 작성할 때 따라야 할 몇 가지 일반적인 지침이 있습니다.

반복 가능한 테스트

테스트 순서 또는 종속성이 테스트 결과를 변경해서는 안 됩니다. 동일한 테스트를 여러 번 실행하면 항상 동일한 결과가 반환되어야 합니다. 테스트에서 인터넷을 사용하여 타사 서비스에 연결하는 경우 이 작업을 수행하기 어려울 수 있습니다. 그러나 이 문제는 stubbing 및 mocking을 통해 해결할 수 있습니다.

더 많이 제어할 수 있는 외부 종속성의 경우 통합 테스트 전후에 단계를 설정하면 테스트가 항상 동일한 상태에서 시작하여 실행되도록 하는 데 도움이 됩니다.

관련 작업 테스트

가능한 모든 경우를 테스트하려면 단위 테스트가 훨씬 더 나은 옵션입니다.

통합 테스트는 모듈 간의 연결에 더 중점을 두므로 모듈 간의 중요한 연결을 다루기 때문에 행복한 시나리오를 테스트하는 것이 일반적입니다.

이해 가능한 테스트 및 주장

테스트에 대한 한 가지 빠른 보기는 독자에게 테스트 대상, 환경 설정 방법, 스텁 대상, 테스트 실행 시기 및 주장된 내용을 알려야 합니다. 어설션은 단순해야 하며 더 나은 비교 및 ​​로깅을 위해 도우미를 사용해야 합니다.

쉬운 테스트 설정

테스트를 초기 상태로 만드는 것은 가능한 한 간단하고 이해하기 쉬워야 합니다.

타사 코드 테스트 피하기

타사 서비스를 테스트에 사용할 수 있지만 테스트할 필요는 없습니다. 그리고 당신이 그것들을 신뢰하지 않는다면, 당신은 아마 그것들을 사용하지 말아야 할 것입니다.

테스트 코드 없이 프로덕션 코드를 남겨두십시오.

프로덕션 코드는 깨끗하고 간단해야 합니다. 테스트 코드와 프로덕션 코드를 혼합하면 연결할 수 없는 두 도메인이 함께 결합됩니다.

관련 로깅

실패한 테스트는 좋은 로깅 없이는 그다지 가치가 없습니다.

테스트를 통과하면 추가 로깅이 필요하지 않습니다. 그러나 실패하면 광범위한 로깅이 중요합니다.

로깅에는 모든 데이터베이스 쿼리, API 요청 및 응답은 물론 주장되는 내용에 대한 전체 비교가 포함되어야 합니다. 이것은 디버깅을 상당히 용이하게 할 수 있습니다.

좋은 테스트는 깨끗하고 이해하기 쉬워 보입니다.

여기의 지침을 따르는 간단한 테스트는 다음과 같을 수 있습니다.

 const co = require('co'); const test = require('blue-tape'); const factory = require('factory'); const superTest = require('../utils/super_test'); const testEnvironment = require('../utils/test_environment_preparer'); const path = '/v1/admin/recipes'; test(`API GET ${path}`, co.wrap(function* (t) { yield testEnvironment.prepare(); const recipe1 = yield factory.create('recipe'); const recipe2 = yield factory.create('recipe'); const serverResponse = yield superTest.get(path); t.deepEqual(serverResponse.body, [recipe1, recipe2]); }));

위의 코드는 저장된 레시피의 배열을 응답으로 반환할 것으로 예상하는 API( GET /v1/admin/recipes )를 테스트하고 있습니다.

테스트가 간단하지만 많은 유틸리티에 의존한다는 것을 알 수 있습니다. 이는 모든 우수한 통합 테스트 제품군에 공통적입니다.

도우미 구성 요소를 사용하면 이해 가능한 통합 테스트를 쉽게 작성할 수 있습니다.

통합 테스트에 필요한 구성 요소를 검토해 보겠습니다.

도우미 구성 요소

포괄적인 테스트 제품군에는 흐름 제어, 테스트 프레임워크, 데이터베이스 처리기 및 백엔드 API에 연결하는 방법을 비롯한 몇 가지 기본 구성 요소가 있습니다.

흐름 제어

JavaScript 테스트에서 가장 큰 문제 중 하나는 비동기식 흐름입니다.

콜백은 코드를 혼란에 빠뜨릴 수 있으며 약속만으로는 충분하지 않습니다. 이때 흐름 도우미가 유용합니다.

async/await가 완전히 지원되기를 기다리는 동안 유사한 동작을 가진 라이브러리를 사용할 수 있습니다. 목표는 비동기 흐름을 가질 가능성이 있는 읽기 쉽고 표현력이 높으며 강력한 코드를 작성하는 것입니다.

Co를 사용하면 코드를 비차단 상태로 유지하면서 좋은 방식으로 코드를 작성할 수 있습니다. 이것은 공동 생성기 함수를 정의한 다음 결과를 생성함으로써 수행됩니다.

또 다른 솔루션은 Bluebird를 사용하는 것입니다. Bluebird는 배열, 오류, 시간 등의 처리와 같은 매우 유용한 기능을 가진 Promise 라이브러리입니다.

Co 및 Bluebird 코루틴은 ES7의 async/await와 유사하게 작동합니다(계속하기 전에 해결 대기). 유일한 차이점은 오류 처리에 유용한 항상 약속을 반환한다는 점입니다.

테스트 프레임워크

테스트 프레임워크를 선택하는 것은 개인의 취향에 달려 있습니다. 내가 선호하는 프레임워크는 사용하기 쉽고 부작용이 없으며 쉽게 읽을 수 있고 파이프가 있는 출력물입니다.

JavaScript에는 다양한 테스트 프레임워크가 있습니다. 이 예에서는 테이프를 사용하고 있습니다. 내 생각에 테이프는 이러한 요구 사항을 충족할 뿐만 아니라 Mocha 또는 Jasmin과 같은 다른 테스트 프레임워크보다 깨끗하고 간단합니다.

테이프는 TAP(Test Anything Protocol)를 기반으로 합니다.

TAP에는 대부분의 프로그래밍 언어에 대한 변형이 있습니다.

테이프는 테스트를 입력으로 받아 실행한 다음 결과를 TAP로 출력합니다. 그런 다음 TAP 결과를 테스트 리포터로 파이프하거나 원시 형식으로 콘솔에 출력할 수 있습니다. 테이프는 명령줄에서 실행됩니다.

테이프에는 전체 테스트 스위트를 실행하기 전에 로드할 모듈을 정의하고, 작고 간단한 어설션 라이브러리를 제공하고, 테스트에서 호출해야 하는 어설션 수를 정의하는 것과 같은 몇 가지 멋진 기능이 있습니다. 모듈을 사용하여 미리 로드하면 테스트 환경 준비를 단순화하고 불필요한 코드를 제거할 수 있습니다.

공장 도서관

팩토리 라이브러리를 사용하면 테스트용 데이터를 생성하는 훨씬 더 유연한 방법으로 정적 픽스처 파일을 교체할 수 있습니다. 이러한 라이브러리를 사용하면 지저분하고 복잡한 코드를 작성하지 않고도 모델을 정의하고 해당 모델에 대한 엔터티를 만들 수 있습니다.

JavaScript에는 이를 위한 factory_girl이 있습니다. 이는 원래 Ruby on Rails용으로 개발된 비슷한 이름의 gem에서 영감을 받은 라이브러리입니다.

 const factory = require('factory-girl').factory; const User = require('../models/user'); factory.define('user', User, { username: 'Bob', number_of_recipes: 50 }); const user = factory.build('user');

시작하려면 factory_girl에 새 모델을 정의해야 합니다.

이름, 프로젝트의 모델 및 새 인스턴스가 생성되는 개체로 지정됩니다.

또는 새 인스턴스가 생성되는 개체를 정의하는 대신 개체 또는 약속을 반환하는 함수를 제공할 수 있습니다.

모델의 새 인스턴스를 만들 때 다음을 수행할 수 있습니다.

  • 새로 생성된 인스턴스의 값을 재정의합니다.
  • 빌드 기능 옵션에 추가 값 전달

예를 들어 보겠습니다.

 const factory = require('factory-girl').factory; const User = require('../models/user'); factory.define('user', User, (buildOptions) => { return { name: 'Mike', surname: 'Dow', email: buildOptions.email || '[email protected]' } }); const user1 = factory.build('user'); // {"name": "Mike", "surname": "Dow", "email": "[email protected]"} const user2 = factory.build('user', {name: 'John'}, {email: '[email protected]'}); // {"name": "John", "surname": "Dow", "email": "[email protected]"}

API에 연결

완전한 HTTP 서버를 시작하고 실제 HTTP 요청을 하고, 특히 여러 테스트를 수행할 때 몇 초 후에 해제하는 것은 완전히 비효율적이며 통합 테스트가 필요한 것보다 훨씬 더 오래 걸릴 수 있습니다.

SuperTest는 새로운 활성 서버를 생성하지 않고 API를 호출하기 위한 JavaScript 라이브러리입니다. TCP 요청을 생성하기 위한 라이브러리인 SuperAgent를 기반으로 합니다. 이 라이브러리를 사용하면 새 TCP 연결을 만들 필요가 없습니다. API는 거의 즉시 호출됩니다.

Promise를 지원하는 SuperTest는 supertest-as-promised입니다. 이러한 요청이 약속을 반환하면 여러 중첩된 콜백 함수를 피할 수 있으므로 흐름을 훨씬 쉽게 처리할 수 있습니다.

 const express = require('express') const request = require('supertest-as-promised'); const app = express(); request(app).get("/recipes").then(res => assert(....));

SuperTest는 Express.js 프레임워크용으로 만들어졌지만 약간만 변경하면 다른 프레임워크에서도 사용할 수 있습니다.

기타 유틸리티

어떤 경우에는 코드에서 일부 종속성을 조롱하거나 스파이를 사용하여 함수 주변의 논리를 테스트하거나 특정 위치에서 스텁을 사용할 필요가 있습니다. 여기에서 이러한 유틸리티 패키지 중 일부가 유용합니다.

SinonJS는 테스트를 위한 스파이, 스텁 및 모의를 지원하는 훌륭한 라이브러리입니다. 또한 굽힘 시간, 테스트 샌드박스 및 확장된 어설션과 같은 다른 유용한 테스트 기능과 가짜 서버 및 요청을 지원합니다.

어떤 경우에는 코드에서 일부 종속성을 조롱할 필요가 있습니다. 조롱하려는 서비스에 대한 참조는 시스템의 다른 부분에서 사용됩니다.

이 문제를 해결하기 위해 의존성 주입을 사용할 수 있으며 옵션이 아닌 경우 Mockery와 같은 모의 서비스를 사용할 수 있습니다.

Mockery는 외부 종속성이 있는 코드를 조롱하는 데 도움이 됩니다. 제대로 사용하려면 테스트나 코드를 로드하기 전에 Mockery를 호출해야 합니다.

 const mockery = require('mockery'); mockery.enable({ warnOnReplace: false, warnOnUnregistered: false }); const mockingStripe = require('lib/services/internal/stripe'); mockery.registerMock('lib/services/internal/stripe', mockingStripe);

이 새로운 참조(이 예에서는 mockingStripe )를 사용하면 나중에 테스트에서 서비스를 모의하는 것이 더 쉽습니다.

 const stubStripeTransfer = sinon.stub(mockingStripe, 'transferAmount'); stubStripeTransfer.returns(Promise.resolve(null));

Sinon 라이브러리의 도움으로 쉽게 조롱할 수 있습니다. 여기서 유일한 문제는 이 스텁이 다른 테스트로 전파된다는 것입니다. 이를 샌드박스 처리하려면 sinon 샌드박스를 사용할 수 있습니다. 이를 통해 이후 테스트에서 시스템을 초기 상태로 되돌릴 수 있습니다.

 const sandbox = require('sinon').sandbox.create(); const stubStripeTransfer = sandbox.sinon.stub(mockingStripe, 'transferAmount'); stubStripeTransfer.returns(Promise.resolve(null)); // after the test, or better when starting a new test sandbox.restore();

다음과 같은 기능을 위한 다른 구성 요소가 필요합니다.

  • 데이터베이스 비우기(하나의 계층 구조 사전 빌드 쿼리로 수행 가능)
  • 작동 상태로 설정(sequelize-fixtures)
  • 타사 서비스에 대한 TCP 요청 모의(nock)
  • 더 풍부한 어설션 사용(chai)
  • 타사의 저장된 응답(간단한 수정)

간단하지 않은 테스트

추상화와 확장성은 효과적인 통합 테스트 스위트를 구축하기 위한 핵심 요소입니다. 테스트의 핵심에서 초점을 제거하는 모든 것(데이터 준비, 작업 및 주장)은 그룹화되고 유틸리티 기능으로 추상화되어야 합니다.

여기에는 옳고 그른 경로가 없지만 모든 것이 프로젝트와 요구 사항에 따라 달라지므로 일부 주요 품질은 우수한 통합 테스트 제품군에 여전히 공통적입니다.

다음 코드는 레시피를 생성하고 부작용으로 이메일을 보내는 API를 테스트하는 방법을 보여줍니다.

실제로 이메일을 보내지 않고 이메일이 전송되었는지 테스트할 수 있도록 외부 이메일 공급자를 스텁합니다. 이 테스트는 API가 적절한 상태 코드로 응답했는지도 확인합니다.

 const co = require('co'); const factory = require('factory'); const superTest = require('../utils/super_test'); const basicEnv = require('../utils/basic_test_enivornment'); const path = '/v1/admin/recipes'; basicEnv.test(`API POST ${path}`, co.wrap(function* (t, assert, sandbox) { const chef = yield factory.create('chef'); const body = { chef_id: chef.id, recipe_name: 'cake', Ingredients: ['carrot', 'chocolate', 'biscuit'] }; const stub = sandbox.stub(mockery.emailProvider, 'sendNewEmail').returnsPromise(null); const serverResponse = yield superTest.get(path, body); assert.spies(stub).called(1); assert.statusCode(serverResponse, 201); }));

위의 테스트는 매번 깨끗한 환경에서 시작하므로 반복 가능합니다.

설정과 관련된 모든 것이 basicEnv.test 함수 내에 통합되는 간단한 설정 프로세스가 있습니다.

하나의 작업(단일 API)만 테스트합니다. 그리고 간단한 assert 문을 통해 테스트의 기대치를 명확하게 나타냅니다. 또한 테스트에는 stubbing/mocking에 의한 타사 코드가 포함되지 않습니다.

통합 테스트 작성 시작

새 코드를 프로덕션에 푸시할 때 개발자(및 다른 모든 프로젝트 참가자)는 새 기능이 작동하고 이전 기능이 중단되지 않는지 확인하기를 원합니다.

이것은 테스트 없이 달성하기가 매우 어려우며 제대로 수행되지 않으면 좌절, 프로젝트 피로, 결국 프로젝트 실패로 이어질 수 있습니다.

단위 테스트와 결합된 통합 테스트는 첫 번째 방어선입니다.

둘 중 하나만 사용하는 것은 충분하지 않으며 발견되지 않은 오류를 위한 많은 공간을 남깁니다. 항상 두 가지를 모두 활용하면 새로운 커밋이 견고해지고 모든 프로젝트 참가자에게 자신감을 주고 신뢰를 불러일으킵니다.