На этой странице описаны рекомендации и инструменты для написания модульных тестов для ваших функций, например тестов, которые будут частью системы непрерывной интеграции (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, выполнив следующие команды в папке functions:
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 перейдите на страницу
Settings (Настройки) > General (Общие).Прокрутите экран до карточки Ваши приложения и выберите нужное.
Получите конфигурацию Firebase:
Для приложений Apple и Android выберите вариант скачивания файла конфигурации.
Для веб-приложений выберите Конфигурация, чтобы посмотреть значения конфигурации.
Чтобы создать файл ключа, выполните следующие действия:
В консоли Google Cloud перейдите на панель Service Accounts (Сервисные аккаунты).
Выберите сервисный аккаунт по умолчанию App Engine и в меню параметров справа нажмите Создать ключ.
В появившемся окне выберите JSON в качестве типа ключа и нажмите Create (Создать).
После сохранения файла ключа инициализируйте 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. - Создайте тестовые данные
- Вызовите упакованную функцию с тестовыми данными, которые вы создали, и любыми полями контекста события, которые вы хотите указать.
- Делать утверждения о поведении.
Сначала оберните функцию, которую хотите протестировать. Допустим, у вас есть функция 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
принимает два параметра:
- 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 в онлайн-режиме, то можете подтвердить, что нужные действия (например, запись в базу данных) были выполнены, с помощью 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. 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.