Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기

Cloud Functions의 단위 테스트

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

이 페이지에서는 CI(연속 통합) 시스템의 일부가 될 테스트와 같이 함수에 대한 단위 테스트를 작성하기 위한 모범 사례 및 도구를 설명합니다. 더 쉽게 테스트할 수 있도록 Firebase는 Cloud Functions용 ​​Firebase Test SDK를 제공합니다. npm에서 firebase-functions-test 로 배포되며 firebase- firebase-functions 의 동반 테스트 SDK입니다. Cloud Functions용 ​​Firebase 테스트 SDK:

  • firebase-functions 에 필요한 환경 변수 설정 및 설정 해제와 같이 테스트를 위한 적절한 설정 및 해제를 처리합니다.
  • 샘플 데이터 및 이벤트 컨텍스트를 생성하므로 테스트와 관련된 필드만 지정하면 됩니다.

테스트 설정

functions 폴더에서 다음 명령어를 실행하여 firebase-functions-test 및 테스트 프레임워크인 Mocha 를 모두 설치합니다.

npm install --save-dev firebase-functions-test
npm install --save-dev mocha

그런 다음 functions 폴더 안에 test 폴더를 만들고 그 안에 테스트 코드를 위한 새 파일을 만들고 index.test.js 와 같은 이름을 지정합니다.

마지막으로 functions/package.json 을 수정하여 다음을 추가합니다.

"scripts": {
  "test": "mocha --reporter spec"
}

테스트를 작성했으면 함수 디렉터리 내에서 npm test 를 실행하여 테스트를 실행할 수 있습니다.

Cloud Functions용 ​​Firebase 테스트 SDK 초기화

firebase-functions-test 를 사용하는 방법에는 두 가지가 있습니다.

  1. 온라인 모드(권장): 데이터베이스 쓰기, 사용자 생성 등이 실제로 발생하고 테스트 코드가 결과를 검사할 수 있도록 테스트 전용 Firebase 프로젝트와 상호 작용하는 테스트를 작성합니다. 이것은 또한 함수에 사용되는 다른 Google SDK도 작동함을 의미합니다.
  2. 오프라인 모드: 부작용 없이 사일로 및 오프라인 단위 테스트를 작성합니다. 즉, Firebase 제품과 상호작용하는 모든 메소드 호출(예: 데이터베이스에 쓰기 또는 사용자 생성)은 스텁해야 합니다. Cloud Firestore 또는 실시간 데이터베이스 기능이 있는 경우 일반적으로 오프라인 모드를 사용하지 않는 것이 좋습니다. 테스트 코드의 복잡성이 크게 증가하기 때문입니다.

온라인 모드에서 SDK 초기화(권장)

테스트 프로젝트와 상호작용하는 테스트를 작성하려면 firebase-admin 을 통해 앱을 초기화하는 데 필요한 프로젝트 구성 값과 서비스 계정 키 파일의 경로를 제공해야 합니다.

Firebase 프로젝트의 구성 값을 가져오려면 다음 안내를 따르세요.

  1. Firebase 콘솔 에서 프로젝트 설정을 엽니다.
  2. 내 앱에서 원하는 앱을 선택합니다.
  3. 오른쪽 창에서 Apple 및 Android 앱용 구성 파일을 다운로드하는 옵션을 선택합니다.

    웹 앱의 경우 구성 을 선택하여 구성 값을 표시합니다.

키 파일을 생성하려면:

  1. Google Cloud Console의 서비스 계정 창 을 엽니다.
  2. App Engine 기본 서비스 계정을 선택하고 오른쪽의 옵션 메뉴를 사용하여 키 만들기 를 선택합니다.
  3. 프롬프트가 표시되면 키 유형으로 JSON을 선택하고 작성을 클릭 하십시오 .

키 파일을 저장한 후 SDK를 초기화합니다.

// At the top of test/index.test.js
const test = require('firebase-functions-test')({
  databaseURL: 'https://my-project.firebaseio.com',
  storageBucket: 'my-project.appspot.com',
  projectId: 'my-project',
}, 'path/to/serviceAccountKey.json');

오프라인 모드에서 SDK 초기화

완전히 오프라인 테스트를 작성하려면 매개변수 없이 SDK를 초기화할 수 있습니다.

// At the top of test/index.test.js
const test = require('firebase-functions-test')();

모의 구성 값

함수 코드에서 functions.config() 를 사용하면 구성 값을 조롱할 수 있습니다. 예를 들어 functions/index.js 에 다음 코드가 포함된 경우:

const functions = require('firebase-functions');
const key = functions.config().stripe.key;

그런 다음 다음과 같이 테스트 파일 내부의 값을 조롱할 수 있습니다.

// Mock functions config values
test.mockConfig({ stripe: { key: '23wr42ewr34' }});

함수 가져오기

함수를 가져오려면 require 를 사용하여 기본 함수 파일을 모듈로 가져옵니다. firebase-functions-test 를 초기화하고 구성 값을 조롱한 후에만 이 작업을 수행해야 합니다.

// after firebase-functions-test has been initialized
const myFunctions = require('../index.js'); // relative path to functions code

오프라인 모드 에서 firebase-functions-test 를 초기화했고 함수 코드에 admin.initializeApp() 이 있는 경우 함수를 가져오기 전에 스텁해야 합니다.

// If index.js calls admin.initializeApp at the top of the file,
// we need to stub it out before requiring index.js. This is because the
// functions will be executed as a part of the require process.
// Here we stub admin.initializeApp to be a dummy function that doesn't do anything.
adminInitStub = sinon.stub(admin, 'initializeApp');
// Now we can require index.js and save the exports inside a namespace called myFunctions.
myFunctions = require('../index');

백그라운드(비 HTTP) 기능 테스트

비 HTTP 기능을 테스트하는 프로세스에는 다음 단계가 포함됩니다.

  1. 테스트하려는 함수를 test.wrap 메서드로 래핑합니다.
  2. 테스트 데이터 구성
  3. 구성한 테스트 데이터와 지정하려는 이벤트 컨텍스트 필드를 사용하여 래핑된 함수를 호출합니다.
  4. 행동에 대해 주장하십시오.

먼저 테스트하려는 함수를 래핑합니다. 테스트 makeUppercase 라는 functions/index.js 에 함수가 있다고 가정해 보겠습니다. functions/test/index.test.js 에 다음을 작성합니다.

// "Wrap" the makeUpperCase function from index.js
const myFunctions = require('../index.js');
const wrapped = test.wrap(myFunctions.makeUppercase);

wrapped 는 호출될 때 makeUppercase 를 호출하는 함수입니다. wrapped 는 2개의 매개변수를 사용합니다.

  1. data (필수): makeUppercase 로 보낼 데이터. 이는 작성한 함수 핸들러로 전송된 첫 번째 매개변수에 직접적으로 해당합니다. firebase-functions-test 는 맞춤 데이터 또는 예시 데이터를 구성하기 위한 메서드를 제공합니다.
  2. eventContextOptions (선택 사항): 지정하려는 이벤트 컨텍스트의 필드입니다. 이벤트 컨텍스트는 작성한 함수 핸들러로 전송되는 두 번째 매개변수입니다. wrapped 를 호출할 때 eventContextOptions 매개변수를 포함하지 않으면 여전히 감지 가능한 필드로 이벤트 컨텍스트가 생성됩니다. 여기에서 지정하여 생성된 필드 중 일부를 재정의할 수 있습니다. 재정의하려는 필드만 포함하면 됩니다. 재정의하지 않은 모든 필드가 생성됩니다.
const data = … // See next section for constructing test data

// Invoke the wrapped function without specifying the event context.
wrapped(data);

// Invoke the function, and specify params
wrapped(data, {
  params: {
    pushId: '234234'
  }
});

// Invoke the function, and specify auth and auth Type (for real time database functions only)
wrapped(data, {
  auth: {
    uid: 'jckS2Q0'
  },
  authType: 'USER'
});

// Invoke the function, and specify all the fields that can be specified
wrapped(data, {
  eventId: 'abc',
  timestamp: '2018-03-23T17:27:17.099Z',
  params: {
    pushId: '234234'
  },
  auth: {
    uid: 'jckS2Q0' // only for real time database functions
  },
  authType: 'USER' // only for real time database functions
});

테스트 데이터 구성

래핑된 함수의 첫 번째 매개변수는 기본 함수를 호출할 테스트 데이터입니다. 테스트 데이터를 구성하는 방법에는 여러 가지가 있습니다.

맞춤 데이터 사용

firebase-functions-test 에는 함수를 테스트하는 데 필요한 데이터를 구성하는 여러 함수가 있습니다. 예를 들어 test.firestore.makeDocumentSnapshot 을 사용하여 Firestore DocumentSnapshot 을 생성합니다. 첫 번째 인수는 데이터이고 두 번째 인수는 전체 참조 경로이며 지정할 수 있는 스냅샷의 다른 속성에 대한 선택적 세 번째 인수 가 있습니다.

// Make snapshot
const snap = test.firestore.makeDocumentSnapshot({foo: 'bar'}, 'document/path');
// Call wrapped function with the snapshot
const wrapped = test.wrap(myFunctions.myFirestoreDeleteFunction);
wrapped(snap);

onUpdate 또는 onWrite 함수를 테스트하는 경우 두 개의 스냅샷을 생성해야 합니다. 하나는 이전 상태용이고 다른 하나는 이후 상태용입니다. 그런 다음 makeChange 메서드를 사용하여 이러한 스냅샷으로 Change 개체를 만들 수 있습니다.

// Make snapshot for state of database beforehand
const beforeSnap = test.firestore.makeDocumentSnapshot({foo: 'bar'}, 'document/path');
// Make snapshot for state of database after the change
const afterSnap = test.firestore.makeDocumentSnapshot({foo: 'faz'}, 'document/path');
const change = test.makeChange(beforeSnap, afterSnap);
// Call wrapped function with the Change object
const wrapped = test.wrap(myFunctions.myFirestoreUpdateFunction);
wrapped(change);

다른 모든 데이터 유형에 대한 유사한 함수는 API 참조 를 참조하십시오.

예제 데이터 사용

테스트에 사용되는 데이터를 맞춤설정할 필요가 없는 경우 firebase-functions-test 는 각 함수 유형에 대한 예제 데이터를 생성하는 메서드를 제공합니다.

// For Firestore onCreate or onDelete functions
const snap = test.firestore.exampleDocumentSnapshot();
// For Firestore onUpdate or onWrite functions
const change = test.firestore.exampleDocumentSnapshotChange();

모든 함수 유형에 대한 예제 데이터를 가져오는 방법은 API 참조 를 참조하세요.

스텁 데이터 사용(오프라인 모드용)

오프라인 모드에서 SDK를 초기화하고 Cloud Firestore 또는 실시간 데이터베이스 기능을 테스트하는 경우 실제 DocumentSnapshot 또는 DataSnapshot 을 생성하는 대신 스텁이 있는 일반 객체를 사용해야 합니다.

다음 함수에 대한 단위 테스트를 작성한다고 가정해 보겠습니다.

// Listens for new messages added to /messages/:pushId/original and creates an
// uppercase version of the message to /messages/:pushId/uppercase
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snapshot, context) => {
      // Grab the current value of what was written to the Realtime Database.
      const original = snapshot.val();
      functions.logger.log('Uppercasing', context.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing asynchronous tasks inside a Functions such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
      return snapshot.ref.parent.child('uppercase').set(uppercase);
    });

함수 내에서 snap 이 두 번 사용됩니다.

  • snap.val()
  • snap.ref.parent.child('uppercase').set(uppercase)

테스트 코드에서 이러한 코드 경로가 모두 작동하는 일반 개체를 만들고 Sinon 을 사용하여 메서드를 스텁합니다.

// The following lines creates a fake snapshot, 'snap', which returns 'input' when snap.val() is called,
// and returns true when snap.ref.parent.child('uppercase').set('INPUT') is called.
const snap = {
  val: () => 'input',
  ref: {
    parent: {
      child: childStub,
    }
  }
};
childStub.withArgs(childParam).returns({ set: setStub });
setStub.withArgs(setParam).returns(true);

어설션 만들기

SDK를 초기화하고 함수를 래핑하고 데이터를 생성한 후 생성된 데이터로 래핑된 함수를 호출하고 동작에 대한 어설션을 만들 수 있습니다. 이러한 어설션을 만들기 위해 Chai 와 같은 라이브러리를 사용할 수 있습니다.

온라인 모드에서 어설션 만들기

온라인 모드 에서 Cloud Functions용 ​​Firebase Test SDK를 초기화한 경우 firebase-admin SDK를 사용하여 원하는 작업(예: 데이터베이스 쓰기)이 수행되었음을 어설션할 수 있습니다.

아래 예는 'INPUT'이 테스트 프로젝트의 데이터베이스에 기록되었음을 확인합니다.

// Create a DataSnapshot with the value 'input' and the reference path 'messages/11111/original'.
const snap = test.database.makeDataSnapshot('input', 'messages/11111/original');

// Wrap the makeUppercase function
const wrapped = test.wrap(myFunctions.makeUppercase);
// Call the wrapped function with the snapshot you constructed.
return wrapped(snap).then(() => {
  // Read the value of the data at messages/11111/uppercase. Because `admin.initializeApp()` is
  // called in functions/index.js, there's already a Firebase app initialized. Otherwise, add
  // `admin.initializeApp()` before this line.
  return admin.database().ref('messages/11111/uppercase').once('value').then((createdSnap) => {
    // Assert that the value is the uppercased version of our input.
    assert.equal(createdSnap.val(), 'INPUT');
  });
});

오프라인 모드에서 어설션 만들기

함수의 예상 반환 값에 대한 어설션을 만들 수 있습니다.

const childParam = 'uppercase';
const setParam = 'INPUT';
// Stubs are objects that fake and/or record function calls.
// These are excellent for verifying that functions have been called and to validate the
// parameters passed to those functions.
const childStub = sinon.stub();
const setStub = sinon.stub();
// The following lines creates a fake snapshot, 'snap', which returns 'input' when snap.val() is called,
// and returns true when snap.ref.parent.child('uppercase').set('INPUT') is called.
const snap = {
  val: () => 'input',
  ref: {
    parent: {
      child: childStub,
    }
  }
};
childStub.withArgs(childParam).returns({ set: setStub });
setStub.withArgs(setParam).returns(true);
// Wrap the makeUppercase function.
const wrapped = test.wrap(myFunctions.makeUppercase);
// Since we've stubbed snap.ref.parent.child(childParam).set(setParam) to return true if it was
// called with the parameters we expect, we assert that it indeed returned true.
return assert.equal(wrapped(snap), true);

또한 Sinon 스파이 를 사용하여 특정 메소드가 호출되었음을 어설션하고 예상하는 매개변수를 사용할 수 있습니다.

HTTP 기능 테스트

HTTP onCall 기능을 테스트하려면 백그라운드 기능 테스트 와 동일한 접근 방식을 사용하십시오.

HTTP onRequest 함수를 테스트하는 경우 다음과 같은 경우 firebase-functions-test 를 사용해야 합니다.

  • functions.config() 를 사용합니다.
  • 귀하의 함수는 Firebase 프로젝트 또는 다른 Google API와 상호 작용하며 테스트를 위해 실제 Firebase 프로젝트 및 해당 자격 증명을 사용하려고 합니다.

HTTP onRequest 함수는 요청 객체와 응답 객체라는 두 가지 매개변수를 사용합니다. 다음은 addMessage() 예제 함수를 테스트하는 방법입니다.

  • sendMessage() 가 호출하므로 응답 객체의 리디렉션 함수를 재정의합니다.
  • 리디렉션 함수 내에서 chai.assert 를 사용하여 리디렉션 함수를 호출해야 하는 매개 변수에 대한 어설션을 만드는 데 도움이 됩니다.
// A fake request object, with req.query.text set to 'input'
const req = { query: {text: 'input'} };
// A fake response object, with a stubbed redirect function which asserts that it is called
// with parameters 303, 'new_ref'.
const res = {
  redirect: (code, url) => {
    assert.equal(code, 303);
    assert.equal(url, 'new_ref');
    done();
  }
};

// Invoke addMessage with our fake request and response objects. This will cause the
// assertions in the response object to be evaluated.
myFunctions.addMessage(req, res);

테스트 정리

테스트 코드 맨 끝에서 cleanup 함수를 호출합니다. 이렇게 하면 SDK가 초기화될 때 설정한 환경 변수가 설정 해제되고 SDK를 사용하여 실시간 데이터베이스 DataSnapshot 또는 Firestore DocumentSnapshot 을 만든 경우 생성되었을 수 있는 Firebase 앱이 삭제됩니다.

test.cleanup();

전체 예제를 검토하고 자세히 알아보십시오.

Firebase GitHub 저장소에서 전체 예제를 검토할 수 있습니다.

자세히 알아보려면 firebase-functions-test 에 대한 API 참조 문서 를 확인하세요.