콘솔로 이동

Cloud Functions의 단위 테스트

이 페이지에서는 지속적 통합(CI)에 포함될 테스트와 같이 함수용 단위 테스트 작성에 대한 권장사항과 도구를 설명합니다. 쉽게 테스트할 수 있도록 Firebase에서 Cloud Functions용 Firebase Test SDK를 제공합니다. firebase-functions-test로 npm에 배포되며 firebase-functions에 대한 동반 테스트 SDK입니다. Cloud Functions용 Firebase Test SDK로 다음 작업을 할 수 있습니다.

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

테스트 설정

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

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

다음으로 함수 폴더 내에 test 폴더를 만들고 여기에 테스트 코드용으로 새 파일을 만든 후 index.test.js와 같은 이름을 지정합니다.

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

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

테스트를 작성한 후에는 함수 디렉토리 내에서 npm test를 실행하여 테스트를 실행할 수 있습니다.

Cloud Functions용 Firebase Test SDK 초기화

두 가지 방법으로 firebase-functions-test를 사용할 수 있습니다.

  1. 오프라인 모드: 부작용 없이 격리된 오프라인 단위 테스트를 작성합니다. 즉, Firebase 제품과 상호작용하는 모든 메소드 호출(예: 데이터베이스 쓰기 또는 사용자 생성)이 스텁 처리되어야 합니다.
  2. 온라인 모드: 데이터베이스 쓰기, 사용자 생성 등이 실제로 일어나고 테스트 코드로 결과를 검사할 수 있도록 테스트 전용 Firebase 프로젝트와 상호작용하는 테스트를 작성합니다. 즉, 함수에서 사용하는 다른 Google SDK도 작동하게 됩니다.

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

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

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

온라인 모드에서 SDK 초기화

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

Firebase 프로젝트의 구성 값을 가져오는 방법은 다음과 같습니다.

  1. Firebase Console로 이동합니다.
  2. 프로젝트를 선택한 다음 웹 앱에 Firebase 추가를 클릭하면 팝업에 구성 값이 표시됩니다.

키 파일을 만드는 방법은 다음과 같습니다.

  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');

구성 값 모의 처리

함수 코드에서 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. 동작에 대한 어설션을 만듭니다.

먼저 테스트할 함수를 래핑합니다. functions/index.jsmakeUppercase라는 테스트할 함수가 있다고 가정해 보겠습니다. 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 함수를 테스트하는 경우 이전 상태와 이후 상태용으로 하나씩 스냅샷 2개를 만들어야 합니다. 그런 다음 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를 초기화했으며 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();
      console.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와 같은 라이브러리를 사용할 수 있습니다.

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

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

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 Spies를 사용해도 특정 메소드가 호출되었다고 어설션을 만들고 예상되는 매개변수를 추가할 수 있습니다.

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

온라인 모드에서 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');
  });
});

HTTP 함수 테스트

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

  • functions.config()을 사용합니다.
  • 함수가 Firebase 프로젝트나 다른 Google API와 상호작용하며, 테스트에 실제 Firebase 프로젝트 및 사용자 인증 정보를 사용하고 싶습니다.

HTTP onRequest 함수는 요청 객체 및 응답 객체 등 2가지 매개변수를 취합니다. 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);

테스트 정리

테스트 코드의 맨 끝에서 정리 함수를 호출합니다. 그러면 초기화할 때 SDK에서 설정한 환경 변수가 설정 해제되며, SDK로 실시간 데이터베이스 DataSnapshot이나 Firestore DocumentSnapshot을 만든 경우에 생성되었을 수 있는 Firebase 앱이 삭제됩니다.

test.cleanup();

전체 예시를 검토하고 자세히 알아보기

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

자세한 내용은 firebase-functions-test에 대한 API 참조를 확인하세요.