Модульное тестирование облачных функций

На этой странице описаны лучшие практики и инструменты для написания модульных тестов для ваших функций, например, тестов, которые будут частью системы непрерывной интеграции (CI). Для упрощения тестирования Firebase предоставляет Firebase Test SDK для Cloud Functions . Он распространяется в npm как firebase-functions-test и является дополнительным SDK для тестирования к firebase-functions . Firebase Test SDK для Cloud Functions :

  • Обеспечивает надлежащую настройку и завершение работы ваших тестов, например, установку и удаление переменных окружения, необходимых для firebase-functions .
  • Генерирует примеры данных и контекст событий, так что вам нужно будет указать только те поля, которые имеют отношение к вашему тесту.

Настройка теста

Установите firebase-functions-test и Mocha , фреймворк для тестирования, выполнив следующие команды в папке functions:

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

Далее создайте папку test внутри папки functions, создайте в ней новый файл с тестовым кодом и назовите его, например, index.test.js .

Наконец, внесите следующие изменения в файл functions/package.json :

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

После написания тестов вы можете запустить их, выполнив npm test в каталоге `functions`.

Инициализация Firebase Test SDK для Cloud Functions

Существует два способа использования firebase-functions-test :

  1. Онлайн-режим (рекомендуется): Напишите тесты, которые взаимодействуют с проектом Firebase, предназначенным для тестирования, чтобы происходили операции записи в базу данных, создания пользователей и т. д., а ваш тестовый код мог проверять результаты. Это также означает, что другие SDK Google, используемые в ваших функциях, тоже будут работать.
  2. Автономный режим: Пишите изолированные и автономные модульные тесты без побочных эффектов. Это означает, что любые вызовы методов, взаимодействующих с продуктом Firebase (например, запись в базу данных или создание пользователя), должны быть заглушены. Использование автономного режима, как правило, не рекомендуется, если у вас есть функции Cloud Firestore или Realtime Database , поскольку это значительно увеличивает сложность вашего тестового кода.

Инициализируйте SDK в онлайн-режиме (рекомендуется).

Если вы хотите написать тесты, взаимодействующие с тестовым проектом, вам необходимо указать значения конфигурации проекта, необходимые для инициализации приложения через firebase-admin , а также путь к файлу ключа учетной записи службы.

Чтобы получить значения конфигурации вашего проекта Firebase:

  1. Откройте настройки проекта в консоли Firebase .
  2. В разделе «Ваши приложения» выберите нужное приложение.
  3. В правой панели выберите опцию загрузки файла конфигурации для приложений Apple и Android.

    Для веб-приложений выберите «Конфигурация» , чтобы отобразить значения параметров конфигурации.

Для создания файла ключа:

  1. Откройте панель «Учетные записи служб» в консоли Google Cloud .
  2. Выберите учетную запись службы App Engine по умолчанию и в меню параметров справа выберите пункт «Создать ключ» .
  3. При появлении запроса выберите JSON в качестве типа ключа и нажмите «Создать» .

После сохранения файла ключа инициализируйте SDK:

// At the top of test/index.test.js
// Make sure to use values from your actual Firebase configuration
const test = require('firebase-functions-test')({
  databaseURL: 'https://PROJECT_ID.firebaseio.com',
  storageBucket: 'PROJECT_ID.firebasestorage.app',
  projectId: 'PROJECT_ID',
}, '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/v1');
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.js под названием makeUppercase , которую вы хотите протестировать. Напишите следующий код в файле 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. Данные (обязательно): данные, которые будут отправлены функции makeUppercase . Это напрямую соответствует первому параметру, отправленному в обработчик функции, который вы написали. firebase-functions-test предоставляет методы для создания пользовательских данных или примеров данных.
  2. eventContextOptions (необязательно): поля контекста события, которые вы хотите указать. Контекст события — это второй параметр, передаваемый в написанный вами обработчик функции. Если вы не укажете параметр eventContextOptions при вызове wrapped , контекст события все равно будет сгенерирован с осмысленными полями. Вы можете переопределить некоторые из сгенерированных полей, указав их здесь. Обратите внимание, что вам нужно указать только те поля, которые вы хотите переопределить. Любые поля, которые вы не переопределили, будут сгенерированы.
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 или Realtime Database , вам следует использовать простой объект с заглушками вместо создания фактического 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 .

Формулирование утверждений в онлайн-режиме

Если вы инициализировали Firebase Test SDK для Cloud Functions в онлайн-режиме , вы можете убедиться, что необходимые действия (например, запись в базу данных) были выполнены, используя SDK firebase-admin .

Приведённый ниже пример подтверждает, что значение '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 wrapped(snap).then(makeUppercaseResult => {
  return assert.equal(makeUppercaseResult, true);
});

Также можно использовать шпионы Sinon для проверки того, что определенные методы были вызваны, и с ожидаемыми параметрами.

Тестирование HTTP-функций

Для тестирования функций HTTP onCall используйте тот же подход, что и для тестирования фоновых функций .

Если вы тестируете функции HTTP onRequest, вам следует использовать firebase-functions-test если:

  • Вы используете functions.config()
  • Ваша функция взаимодействует с проектом Firebase или другими API Google, и вы хотели бы использовать реальный проект 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);

Очистка тестов

В самом конце тестового кода вызовите функцию очистки. Это отменяет переменные среды, установленные SDK при инициализации, и удаляет приложения Firebase, которые могли быть созданы, если вы использовали SDK для создания DataSnapshot базы данных в реальном времени или DocumentSnapshot Firestore.

test.cleanup();

Ознакомьтесь с полными примерами и узнайте больше.

Полные примеры можно посмотреть в репозитории Firebase на GitHub.

Для получения более подробной информации обратитесь к справочнику API для firebase-functions-test .