На этой странице описаны рекомендации и инструменты для написания модульных тестов для функций, например тестов, которые будут частью системы непрерывной интеграции (CI). Чтобы упростить тестирование, Firebase предоставляет Firebase Test SDK для Cloud Functions. Он распространяется через npm как firebase-functions-test и является вспомогательным тестовым SDK для firebase-functions. Firebase Test SDK для Cloud Functions:
- Выполняет необходимые действия по подготовке и завершению тестов, например задает и отменяет переменные среды, необходимые для
firebase-functions. - Генерирует примеры данных и контекст событий, поэтому вам нужно указать только поля, относящиеся к вашему тесту.
Настройка тестирования
Установите firebase-functions-test и Jest, фреймворк для тестирования, выполнив следующие команды в папке функций:
npm install --save-dev firebase-functions-test
npm install --save-dev jest
Затем создайте папку test внутри папки functions, создайте в ней новый файл для тестового кода и назовите его, например, index.test.js.
Наконец, измените тег functions/package.json, добавив в него следующие строки:
"scripts": {
"test": "jest"
}
После того как вы напишете тесты, их можно будет запустить, выполнив команду npm test в каталоге функций.
Инициализация Firebase Test SDK для Cloud Functions
Использовать firebase-functions-test можно двумя способами:
- Онлайн-режим (рекомендуется). Напишите тесты, которые взаимодействуют с проектом Firebase, предназначенным для тестирования, чтобы записи в базу данных, создание пользователей и т. д. действительно происходили и ваш код мог проверять результаты. Это также означает, что другие SDK Google, используемые в ваших функциях, будут работать.
- Офлайн-режим. Пишите изолированные и автономные модульные тесты без побочных эффектов. Это означает, что все вызовы методов, взаимодействующих с продуктом Firebase (например, запись в базу данных или создание пользователя), должны быть заглушены. Если вы используете функции Cloud Firestore или Realtime Database, мы не рекомендуем работать в офлайн-режиме, поскольку это значительно усложняет код теста.
Инициализация SDK в онлайн-режиме (рекомендуется)
Если вы хотите написать тесты, взаимодействующие с тестовым проектом, вам нужно указать значения конфигурации Firebase, необходимые для инициализации приложения через firebase-admin, и путь к файлу ключа сервисного аккаунта.
Чтобы получить значения конфигурации Firebase:
В консоли Firebase перейдите на страницу Общие >
Настройки .Найдите карточку Ваши приложения и выберите нужное приложение.
Получите конфигурацию Firebase:
Для приложений Apple и Android выберите вариант скачивания файла конфигурации.
Для веб-приложений выберите Конфигурация, чтобы посмотреть значения конфигурации.
Чтобы создать файл ключа:
В консоли Google Cloud перейдите на панель "Сервисные аккаунты".
Выберите сервисный аккаунт по умолчанию App Engine и в меню параметров справа нажмите Создать ключ.
Когда появится запрос, выберите 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(), то перед импортом функций нужно создать заглушку. В тестовом файле укажите firebase-admin как admin, а затем замените initializeApp() на макет Jest:
// If initializeApp() is called in index.js, we mock it out before requiring index.js adminInitStub = jest.spyOn(admin, 'initializeApp').mockImplementation(() => {}); // Now we can require index.js and save the exports inside a namespace called myFunctions. myFunctions = require('../index');
Как тестировать фоновые функции (не HTTP)
Процесс тестирования функций, не связанных с HTTP, включает следующие шаги:
- Оберните функцию, которую хотите протестировать, методом
test.wrap. - Как создать тестовые данные
- Вызовите функцию-обертку с созданными вами тестовыми данными и любыми полями контекста события, которые вы хотите указать.
- Делать утверждения о поведении.
Сначала оберните функцию, которую хотите протестировать. Предположим, у вас есть функция в файле 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
принимает два параметра:
- data (обязательный параметр) – данные, которые нужно отправить в
makeUppercase. Он соответствует первому параметру, отправленному обработчику функции, которую вы написали.firebase-functions-testпредоставляет методы для создания пользовательских или демонстрационных данных. - 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, чтобы создать DocumentSnapshot Firestore. Первый аргумент – это данные, второй – полный путь к ссылке, а третий аргумент (необязательный) позволяет задать другие свойства снимка.
// 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)
В тестовом коде создайте простой объект, в котором будут работать оба пути кода, и используйте функции-заглушки Jest для заглушки методов.
// The following lines create 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, } } };
Утверждения
После инициализации SDK, создания оберток для функций и формирования данных вы можете вызывать обернутые функции с созданными данными и делать утверждения о поведении. Для этого можно использовать функцию expect из Jest.
Как делать утверждения в онлайн-режиме
Если вы инициализировали 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. return admin.database().ref('messages/11111/uppercase').once('value').then((createdSnap) => { // Assert that the value is the uppercased version of our input. expect(createdSnap.val()).toBe('INPUT'); }); });
Как делать утверждения в офлайн-режиме
Вы можете делать утверждения об ожидаемом возвращаемом значении функции:
const childParam = 'uppercase'; const setParam = 'INPUT'; // Spies/mocks 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 setStub = jest.fn().mockImplementation((val) => val === setParam ? true : undefined); const childStub = jest.fn().mockImplementation((path) => path === childParam ? { set: setStub } : undefined); // The following lines create 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, } } }; // Wrap the makeUppercase function. const wrapped = test.wrap(myFunctions.makeUppercase); // Since we've mocked 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 => { expect(makeUppercaseResult).toBe(true); });
Вы также можете использовать фиктивные функции Jest, чтобы убедиться, что определенные методы были вызваны с ожидаемыми параметрами.
Как тестировать функции HTTP
Чтобы протестировать функции HTTP onCall, используйте тот же подход, что и при тестировании фоновых функций.
Если вы тестируете функции onRequest HTTP, используйте firebase-functions-test в следующих случаях:
- Вы используете
functions.config() - Ваша функция взаимодействует с проектом Firebase или другими API Google, и вы хотите использовать для тестирования реальный проект Firebase и его учетные данные.
Функция onRequest HTTP принимает два параметра: объект запроса и объект ответа. Вот как можно протестировать функцию addMessage():
Заглушка
admin.database(), посколькуaddMessage()отправляет данные в Realtime Database. В офлайн-режиме вы заглушилиinitializeApp(), поэтому у Admin SDK нет приложения Firebase, которое можно использовать, и реальныйadmin.database()завершается с ошибкойThe default Firebase app does not exist:const pushStub = jest.fn().mockResolvedValue({ ref: 'new_ref' }); const refStub = jest.fn().mockReturnValue({ push: pushStub }); Object.defineProperty(admin, 'database', { configurable: true, writable: true, value: jest.fn().mockReturnValue({ ref: refStub }), });Переопределите функцию переадресации в объекте ответа, поскольку ее вызывает
addMessage().В функции переадресации используйте
expectиз Jest, чтобы проверить, с какими параметрами должна вызываться функция переадресации:
// 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) => { expect(code).toBe(303); expect(url).toBe('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 или Firestore DocumentSnapshot.
test.cleanup();
Посмотреть примеры и узнать больше
Полные примеры можно найти в репозитории Firebase на GitHub.
- Тестирование Realtime Database и функций HTTP в онлайн-режиме
- Тестирование Realtime Database и функций HTTP в офлайн-режиме
Подробнее об API
для firebase-functions-test…