1. Подготовка
Бессерверные инструменты, такие как Cloud Firestore и Cloud Functions, очень просты в использовании, но их может быть сложно тестировать. Firebase Local Emulator Suite позволяет запускать локальные версии этих сервисов на компьютере для разработки, чтобы вы могли быстро и безопасно создавать приложения.
Требования
- Простой редактор, например Visual Studio Code, Atom или Sublime Text.
- Node.js 10.0.0 или более поздней версии (чтобы установить Node.js, используйте nvm; чтобы проверить версию, выполните команду
node --version). - Java 7 или более поздней версии. Чтобы установить Java, воспользуйтесь этими инструкциями. Чтобы проверить версию, выполните команду
java -version.
Что вам предстоит сделать
В этой практической работе вы запустите и отладите простое приложение для онлайн-покупок, которое работает на базе нескольких сервисов Firebase:
- Cloud Firestore – глобально масштабируемая бессерверная база данных NoSQL с возможностями работы в реальном времени.
- Cloud Functions – бессерверный код, который выполняется в ответ на события или HTTP-запросы.
- Firebase Authentication – управляемый сервис аутентификации, который интегрируется с другими продуктами Firebase.
- Firebase Hosting – быстрый и безопасный хостинг для веб-приложений.
Вы подключите приложение к Emulator Suite, чтобы включить локальную разработку.

Вы также узнаете, как:
- Как подключить приложение к Emulator Suite и как подключены различные эмуляторы.
- Как работают правила безопасности Firebase и как протестировать правила безопасности Firestore с помощью локального эмулятора.
- Как написать функцию Firebase, которая запускается событиями Firestore, и как написать интеграционные тесты, которые выполняются в Emulator Suite.
2. Настроить
Как получить исходный код
В этой практической работе вы начнете с почти готовой версии примера The Fire Store, поэтому сначала вам нужно клонировать исходный код:
$ git clone https://github.com/firebase/emulators-codelab.git
Затем перейдите в каталог практической работы, в котором вы будете работать в дальнейшем:
$ cd emulators-codelab/codelab-initial-state
Теперь установите зависимости, чтобы запустить код. Если у вас медленное подключение к интернету, это может занять несколько минут.
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
Как получить интерфейс командной строки Firebase
Emulator Suite – это часть интерфейса командной строки Firebase, который можно установить на компьютер с помощью следующей команды:
$ npm install -g firebase-tools
Затем убедитесь, что у вас установлена последняя версия CLI. Для этой практической работы подойдет версия 9.0.0 или более поздняя, но в более поздних версиях исправлено больше ошибок.
$ firebase --version 9.6.0
Как подключиться к проекту Firebase
Создать проект Firebase
- Войдите в консоль Firebase, используя аккаунт Google.
- Нажмите кнопку, чтобы создать проект, и введите его название, например
Emulators Codelab.
- Нажмите Продолжить.
- Если появится запрос, прочитайте и примите Условия использования Firebase, а затем нажмите Продолжить.
- (Необязательно.) Включите помощь от ИИ в консоли Firebase (Gemini в Firebase).
- Для этой практической работы Google Аналитика не нужна, поэтому отключите ее.
- Нажмите Создать проект, дождитесь его инициализации и нажмите Продолжить.
Как подключить код к проекту Firebase
Теперь нужно подключить этот код к проекту Firebase. Сначала выполните следующую команду, чтобы войти в интерфейс командной строки Firebase:
$ firebase login
Затем выполните следующую команду, чтобы создать псевдоним проекта. Замените $YOUR_PROJECT_ID идентификатором своего проекта Firebase.
$ firebase use $YOUR_PROJECT_ID
Теперь вы можете запустить приложение.
3. Запуск эмуляторов
В этом разделе вы запустите приложение локально. Это значит, что пришло время запустить Emulator Suite.
Запустите эмуляторы
Чтобы запустить эмуляторы, выполните следующую команду в каталоге с исходными файлами практической работы:
$ firebase emulators:start --import=./seed
Вы должны увидеть примерно следующее:
$ firebase emulators:start --import=./seed i emulators: Starting emulators: auth, functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub i firestore: Importing data from /Users/samstern/Projects/emulators-codelab/codelab-initial-state/seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log i hosting: Serving hosting files from: public ✔ hosting: Local server: http://127.0.0.1:5000 i ui: Emulator UI logging to ui-debug.log i functions: Watching "/Users/samstern/Projects/emulators-codelab/codelab-initial-state/functions" for Cloud Functions... ✔ functions[calculateCart]: firestore function initialized. ┌─────────────────────────────────────────────────────────────┐ │ ✔ All emulators ready! It is now safe to connect your app. │ │ i View Emulator UI at http://127.0.0.1:4000 │ └─────────────────────────────────────────────────────────────┘ ┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘ Emulator Hub running at 127.0.0.1:4400 Other reserved ports: 4500 Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
Когда появится сообщение All emulators started (Все эмуляторы запущены), приложение будет готово к использованию.
Как подключить веб-приложение к эмуляторам
Судя по таблице в журналах, эмулятор Cloud Firestore прослушивает порт 8080, а эмулятор Authentication – порт 9099.
┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ 127.0.0.1:9099 │ http://127.0.0.1:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Functions │ 127.0.0.1:5001 │ http://127.0.0.1:4000/functions │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ 127.0.0.1:8080 │ http://127.0.0.1:4000/firestore │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Hosting │ 127.0.0.1:5000 │ n/a │ └────────────────┴────────────────┴─────────────────────────────────┘
Давайте подключим ваш интерфейсный код к эмулятору, а не к рабочей среде. Откройте файл public/js/homepage.js и найдите функцию onDocumentReady. Мы видим, что код обращается к стандартным экземплярам Firestore и Auth:
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
Давайте обновим объекты db и auth, чтобы они указывали на локальные эмуляторы:
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
// ADD THESE LINES
if (location.hostname === "127.0.0.1") {
console.log("127.0.0.1 detected!");
auth.useEmulator("http://127.0.0.1:9099");
db.useEmulator("127.0.0.1", 8080);
}
Теперь, когда приложение работает на вашем локальном компьютере (обслуживается эмулятором хостинга), клиент Firestore также указывает на локальный эмулятор, а не на рабочую базу данных.
Как открыть EmulatorUI
В браузере перейдите по адресу http://127.0.0.1:4000/. Вы должны увидеть интерфейс Emulator Suite.

Нажмите, чтобы посмотреть интерфейс эмулятора Firestore. Коллекция items уже содержит данные, импортированные с флагом --import.

4. Запустите приложение.
Открыть приложение
В веб-браузере перейдите по адресу http://127.0.0.1:5000, и вы увидите, что Fire Store работает локально на вашем компьютере!

Использовать приложение
Выберите товар на главной странице и нажмите Добавить в корзину. К сожалению, вы столкнетесь со следующей ошибкой:

Давайте исправим эту ошибку! Поскольку все работает в эмуляторах, мы можем экспериментировать и не беспокоиться о том, что это повлияет на реальные данные.
5. Отладка приложения
Найдите ошибку
Давайте посмотрим в консоли разработчика Chrome. Нажмите Control+Shift+J (Windows, Linux, Chrome OS) или Command+Option+J (Mac), чтобы посмотреть ошибку в консоли.

Похоже, что в методе addToCart есть ошибка. Давайте посмотрим. Где в этом методе мы пытаемся получить доступ к uid и почему это может быть null? Сейчас метод в файле public/js/homepage.js выглядит так:
public/js/homepage.js
addToCart(id, itemData) {
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
Ага! Мы не вошли в приложение. Согласно документации по Firebase Authentication, если мы не вошли в приложение, auth.currentUser – это null. Добавим проверку:
public/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
Тестирование приложения
Теперь обновите страницу и нажмите Добавить в корзину. На этот раз вы должны увидеть более понятную ошибку:

Но если вы нажмете Войти на верхней панели инструментов, а затем снова нажмете Добавить в корзину, то увидите, что корзина обновилась.
Однако цифры кажутся неверными:

Не волнуйтесь, скоро мы исправим эту ошибку. Сначала давайте разберемся, что произошло, когда вы добавили товар в корзину.
6. Триггеры локальных функций
Нажатие кнопки Добавить в корзину запускает цепочку событий, в которых задействовано несколько эмуляторов. После добавления товара в корзину в журналах интерфейса командной строки Firebase должны появиться сообщения, похожие на следующие:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
Эти записи в журнале и обновление интерфейса были вызваны четырьмя ключевыми событиями:

1) Firestore Write – Client
В коллекцию Firestore /carts/{cartId}/items/{itemId}/ добавляется новый документ. Этот код можно увидеть в функции addToCart в файле public/js/homepage.js:
public/js/homepage.js
addToCart(id, itemData) {
// ...
console.log("addToCart", id, JSON.stringify(itemData));
return this.db
.collection("carts")
.doc(this.auth.currentUser.uid)
.collection("items")
.doc(id)
.set(itemData);
}
2. Активация функции Cloud Functions
Функция Cloud Functions calculateCart отслеживает любые события записи (создание, обновление или удаление), которые происходят с товарами в корзине, с помощью триггера onWrite, который можно увидеть в functions/index.js:
functions/index.js
exports.calculateCart = functions.firestore
.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
}
);
3) Firestore Write – Admin
Функция calculateCart считывает все товары в корзине, суммирует их количество и цену, а затем обновляет документ "корзина" с новыми итоговыми значениями (см. cartRef.update(...) выше).
4. Чтение Firestore – клиент
Веб-интерфейс подписан на получение обновлений об изменениях в корзине. Она обновляется в реальном времени после того, как функция Cloud Functions записывает новые итоги и обновляет пользовательский интерфейс, как показано в public/js/homepage.js:
public/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
Recap
Отлично! Вы только что настроили полностью локальное приложение, которое использует три разных эмулятора Firebase для полностью локального тестирования.

И это ещё не всё В следующем разделе вы узнаете:
- Как писать модульные тесты, использующие эмуляторы Firebase.
- Как использовать эмуляторы Firebase для отладки правил безопасности.
7. Создайте правила безопасности, адаптированные для вашего приложения
Наше веб-приложение считывает и записывает данные, но до сих пор мы вообще не беспокоились о безопасности. В Cloud Firestore используется система правил безопасности, которая определяет, у кого есть доступ к чтению и записи данных. Emulator Suite – отличный инструмент для создания прототипов таких правил.
В редакторе откройте файл emulators-codelab/codelab-initial-state/firestore.rules. Наши правила состоят из трех основных разделов:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// TODO: Change these! Anyone can read or write.
allow read, write: if true;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
Сейчас любой пользователь может читать и записывать данные в нашу базу данных! Мы хотим убедиться, что выполняются только допустимые операции и что мы не раскрываем конфиденциальную информацию.
В этой практической работе мы будем следовать принципу минимальных привилегий. Сначала мы заблокируем все документы, а затем будем постепенно добавлять доступ, пока у всех пользователей не будет всех необходимых прав, но не более того. Обновим первые два правила, чтобы запретить доступ, установив условие false:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// User's cart metadata
match /carts/{cartID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// Items inside the user's cart
match /carts/{cartID}/items/{itemID} {
// UPDATE THIS LINE
allow read, write: if false;
}
// All items available in the store. Users can read
// items but never write them.
match /items/{itemID} {
allow read: if true;
}
}
}
8. Запустите эмуляторы и тесты
Запустите эмуляторы
В командной строке убедитесь, что вы находитесь в каталоге emulators-codelab/codelab-initial-state/. Возможно, у вас все ещё запущены эмуляторы, которые вы использовали на предыдущих шагах. Если нет, запустите эмуляторы снова:
$ firebase emulators:start --import=./seed
После запуска эмуляторов вы можете запускать на них тесты локально.
Как запустить тесты
В командной строке на новой вкладке терминала из каталога emulators-codelab/codelab-initial-state/
Сначала перейдите в каталог functions (мы останемся здесь до конца практической работы):
$ cd functions
Теперь запустите тесты mocha в каталоге functions и прокрутите вывод до самого верха:
# Run the tests
$ npm test
> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha
shopping carts
1) can be created and updated by the cart owner
2) can be read only by the cart owner
shopping cart items
3) can be read only by the cart owner
4) can be added only by the cart owner
adding an item to the cart recalculates the cart total.
- should sum the cost of their items
0 passing (364ms)
1 pending
4 failing
Сейчас у нас четыре ошибки. По мере создания файла правил вы можете отслеживать прогресс, наблюдая за тем, как все больше тестов проходят успешно.
9. Безопасный доступ к корзине
Первые две ошибки связаны с тестированием корзины. Они означают, что:
- Пользователи могут создавать и обновлять только свои корзины.
- Пользователи могут просматривать только свои корзины.
functions/test.js
it('can be created and updated by the cart owner', async () => {
// Alice can create her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Bob can't create Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
}));
// Alice can update her own cart with a new total
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").update({
total: 1
}));
// Bob can't update Alice's cart with a new total
await firebase.assertFails(bobDb.doc("carts/alicesCart").update({
total: 1
}));
});
it("can be read only by the cart owner", async () => {
// Setup: Create Alice's cart as admin
await admin.doc("carts/alicesCart").set({
ownerUID: "alice",
total: 0
});
// Alice can read her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart").get());
// Bob can't read Alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart").get());
});
Давайте сделаем так, чтобы эти тесты прошли. В редакторе откройте файл правил безопасности firestore.rules и обновите инструкции в match /carts/{cartID}:
firestore.rules
rules_version = '2';
service cloud.firestore {
// UPDATE THESE LINES
match /carts/{cartID} {
allow create: if request.auth.uid == request.resource.data.ownerUID;
allow read, update, delete: if request.auth.uid == resource.data.ownerUID;
}
// ...
}
}
Теперь эти правила разрешают доступ на чтение и запись только владельцу корзины.
Чтобы проверить входящие данные и аутентификацию пользователя, мы используем два объекта, которые доступны в контексте каждого правила:
- Объект
requestсодержит данные и метаданные о выполняемой операции. - Если в проекте Firebase используется Firebase Authentication, объект
request.authописывает пользователя, который отправляет запрос.
10. Проверить доступ к корзине
Emulator Suite автоматически обновляет правила при каждом сохранении файла firestore.rules. Вы можете убедиться, что эмулятор обновил правила, посмотрев на вкладку, на которой запущен эмулятор, и найдя сообщение Rules updated:

Повторно запустите тесты и убедитесь, что первые два теста теперь пройдены:
$ npm test
> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha
shopping carts
✓ can be created and updated by the cart owner (195ms)
✓ can be read only by the cart owner (136ms)
shopping cart items
1) can be read only by the cart owner
2) can be added only by the cart owner
adding an item to the cart recalculates the cart total.
- should sum the cost of their items
2 passing (482ms)
1 pending
2 failing
Отлично! Теперь у вас есть доступ к корзинам. Перейдем к следующему тесту.
11. Проверьте процесс добавления в корзину в интерфейсе
Сейчас владельцы корзин могут читать и записывать данные в корзину, но не могут читать или записывать данные отдельных товаров в корзине. Это связано с тем, что, хотя у владельцев есть доступ к документу корзины, у них нет доступа к подколлекции товаров корзины.
Это неисправное состояние для пользователей.
Вернитесь в веб-интерфейс, который работает на http://127.0.0.1:5000,, и попробуйте добавить что-нибудь в корзину. В консоли отладки появится ошибка Permission Denied, потому что мы ещё не предоставили пользователям доступ к созданным документам в подколлекции items.
12. Разрешить доступ к товарам в корзине
Эти два теста подтверждают, что пользователи могут добавлять товары только в свою корзину и просматривать только ее содержимое:
it("can be read only by the cart owner", async () => {
// Alice can read items in her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/milk").get());
// Bob can't read items in alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/milk").get())
});
it("can be added only by the cart owner", async () => {
// Alice can add an item to her own cart
await firebase.assertSucceeds(aliceDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
// Bob can't add an item to alice's cart
await firebase.assertFails(bobDb.doc("carts/alicesCart/items/lemon").set({
name: "lemon",
price: 0.99
}));
});
Поэтому мы можем написать правило, которое разрешает доступ, если текущий пользователь имеет тот же UID, что и ownerUID в документе корзины. Поскольку для create, update, delete не нужно указывать разные правила, вы можете использовать правило write, которое применяется ко всем запросам, изменяющим данные.
Обновите правило для документов в подколлекции items. Переменная get в условии считывает значение из Firestore – в данном случае это ownerUID в документе корзины.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ...
// UPDATE THESE LINES
match /carts/{cartID}/items/{itemID} {
allow read, write: if get(/databases/$(database)/documents/carts/$(cartID)).data.ownerUID == request.auth.uid;
}
// ...
}
}
13. Проверить доступ к товарам в корзине
Теперь можно перезапустить проверку. Прокрутите вывод вверх и убедитесь, что больше тестов пройдено:
$ npm test
> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha
shopping carts
✓ can be created and updated by the cart owner (195ms)
✓ can be read only by the cart owner (136ms)
shopping cart items
✓ can be read only by the cart owner (111ms)
✓ can be added only by the cart owner
adding an item to the cart recalculates the cart total.
- should sum the cost of their items
4 passing (401ms)
1 pending
Отлично! Теперь все наши тесты пройдены. У нас есть один ожидающий тест, но мы вернемся к нему через несколько шагов.
14. Проверьте процесс добавления в корзину ещё раз.
Вернитесь в веб-интерфейс (http://127.0.0.1:5000) и добавьте товар в корзину. Это важный шаг, который позволяет убедиться, что наши тесты и правила соответствуют функциональным требованиям клиента. (Помните, что в прошлый раз, когда мы тестировали интерфейс, пользователи не могли добавлять товары в корзину!)

Клиент автоматически перезагружает правила при сохранении firestore.rules. Попробуйте добавить что-нибудь в корзину.
Recap
Отлично! Вы повысили безопасность приложения. Это важный шаг на пути к публикации рабочей версии. Если бы это было рабочее приложение, мы могли бы добавить эти тесты в наш конвейер непрерывной интеграции. Это позволит нам быть уверенными, что у данных корзины будут эти средства контроля доступа, даже если другие пользователи изменят правила.

Но это ещё не всё!
Если вы продолжите, то узнаете:
- Как написать функцию, активируемую событием Firestore
- Как создавать тесты, которые работают на нескольких эмуляторах
15. Как настроить тесты Cloud Functions
До сих пор мы фокусировались на фронтенде нашего веб-приложения и правилах безопасности Firestore. Но это приложение также использует Cloud Functions, чтобы поддерживать актуальность корзины пользователя, поэтому мы хотим протестировать и этот код.
Emulator Suite позволяет легко тестировать функции Cloud Functions, даже те, которые используют Cloud Firestore и другие сервисы.
В редакторе откройте файл emulators-codelab/codelab-initial-state/functions/test.js и прокрутите его до последнего теста. Сейчас у него статус "Ожидает":
// REMOVE .skip FROM THIS LINE
describe.skip("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
Чтобы включить тест, удалите .skip, чтобы получилось следующее:
describe("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
Затем найдите переменную REAL_FIREBASE_PROJECT_ID в верхней части файла и замените ее на идентификатор вашего проекта Firebase:
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
Если вы забыли идентификатор проекта Firebase, его можно найти в настройках проекта в консоли Firebase:

16. Тестирование функций
Поскольку этот тест проверяет взаимодействие между Cloud Firestore и Cloud Functions, он требует больше настроек, чем тесты в предыдущих практических работах. Давайте разберём этот тест и поймём, что от него ожидается.
Как создать корзину
Функции Cloud Functions выполняются в доверенной серверной среде и могут использовать аутентификацию сервисного аккаунта, применяемую Admin SDK . Сначала инициализируйте приложение, используя initializeAdminApp вместо initializeApp. Затем создайте объект DocumentReference для корзины, в которую будут добавляться товары, и инициализируйте корзину:
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
...
});
Как запустить функцию
Затем добавьте документы в подколлекцию items документа корзины, чтобы активировать функцию. Добавьте два элемента, чтобы убедиться, что вы тестируете добавление, которое происходит в функции.
it("should sum the cost of their items", async () => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
await aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
await aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
await aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
...
});
});
Определите ожидания от тестирования
Используйте onSnapshot(), чтобы зарегистрировать прослушиватель для любых изменений в документе корзины. onSnapshot() возвращает функцию, которую можно вызвать, чтобы отменить регистрацию прослушивателя.
Для этого теста добавьте два товара, которые вместе стоят 9,98 доллара США. Затем проверьте, есть ли в корзине ожидаемые itemCount и totalPrice. Если да, то функция выполнила свою задачу.
it("should sum the cost of their items", (done) => {
const db = firebase
.initializeAdminApp({ projectId: REAL_FIREBASE_PROJECT_ID })
.firestore();
// Setup: Initialize cart
const aliceCartRef = db.doc("carts/alice")
aliceCartRef.set({ ownerUID: "alice", totalPrice: 0 });
// Trigger calculateCart by adding items to the cart
const aliceItemsRef = aliceCartRef.collection("items");
aliceItemsRef.doc("doc1").set({name: "nectarine", price: 2.99});
aliceItemsRef.doc("doc2").set({ name: "grapefruit", price: 6.99 });
// Listen for every update to the cart. Every time an item is added to
// the cart's subcollection of items, the function updates `totalPrice`
// and `itemCount` attributes on the cart.
// Returns a function that can be called to unsubscribe the listener.
await new Promise((resolve) => {
const unsubscribe = aliceCartRef.onSnapshot(snap => {
// If the function worked, these will be cart's final attributes.
const expectedCount = 2;
const expectedTotal = 9.98;
// When the `itemCount`and `totalPrice` match the expectations for the
// two items added, the promise resolves, and the test passes.
if (snap.data().itemCount === expectedCount && snap.data().totalPrice == expectedTotal) {
// Call the function returned by `onSnapshot` to unsubscribe from updates
unsubscribe();
resolve();
};
});
});
});
});
17. Как запустить тесты
Возможно, у вас все ещё запущены эмуляторы, которые вы использовали для предыдущих тестов. Если нет, запустите эмуляторы. В командной строке выполните следующую команду:
$ firebase emulators:start --import=./seed
Откройте новую вкладку терминала (не закрывая эмуляторы) и перейдите в каталог functions. Возможно, она у вас уже открыта после тестирования правил безопасности.
$ cd functions
Теперь запустите модульные тесты. Вы должны увидеть 5 тестов:
$ npm test
> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha
shopping cart creation
✓ can be created by the cart owner (82ms)
shopping cart reads, updates, and deletes
✓ cart can be read by the cart owner (42ms)
shopping cart items
✓ items can be read by the cart owner (40ms)
✓ items can be added by the cart owner
adding an item to the cart recalculates the cart total.
1) should sum the cost of their items
4 passing (2s)
1 failing
Если вы посмотрите на конкретную ошибку, то увидите, что она связана с тайм-аутом. Это связано с тем, что тест ожидает правильного обновления функции, но этого не происходит. Теперь мы готовы написать функцию, которая будет соответствовать тесту.
18. Напишите функцию
Чтобы исправить этот тест, обновите функцию в functions/index.js. Хотя часть этой функции написана, она не завершена. Вот как сейчас выглядит функция:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 125.98;
let itemCount = 8;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
Функция правильно задает ссылку на корзину, но вместо того, чтобы рассчитать значения totalPrice и itemCount, она заменяет их на заданные в коде.
Получение и итерация по
items subcollection
Инициализируйте новую константу itemsSnap, которая будет подколлекцией items. Затем переберите все документы в коллекции.
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
let totalPrice = 125.98;
let itemCount = 8;
const cartRef = db.collection("carts").doc(context.params.cartId);
// ADD LINES FROM HERE
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
})
// TO HERE
return cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
Рассчитайте totalPrice и itemCount
Сначала присвоим переменным totalPrice и itemCount нулевые значения.
Затем добавьте логику в блок итерации. Сначала проверьте, указана ли цена товара. Если количество не указано, по умолчанию используется значение 1. Затем добавьте количество к текущей сумме itemCount. Наконец, добавьте цену товара, умноженную на количество, к текущей сумме totalPrice:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
try {
// CHANGE THESE LINES
let totalPrice = 0;
let itemCount = 0;
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
// ADD LINES FROM HERE
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = itemData.quantity ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
// TO HERE
})
await cartRef.update({
totalPrice,
itemCount
});
} catch(err) {
}
});
Вы также можете добавить ведение журнала, чтобы упростить отладку состояний успеха и ошибки:
// Recalculates the total cost of a cart; triggered when there's a change
// to any items in a cart.
exports.calculateCart = functions
.firestore.document("carts/{cartId}/items/{itemId}")
.onWrite(async (change, context) => {
console.log(`onWrite: ${change.after.ref.path}`);
if (!change.after.exists) {
// Ignore deletes
return;
}
let totalPrice = 0;
let itemCount = 0;
try {
const cartRef = db.collection("carts").doc(context.params.cartId);
const itemsSnap = await cartRef.collection("items").get();
itemsSnap.docs.forEach(item => {
const itemData = item.data();
if (itemData.price) {
// If not specified, the quantity is 1
const quantity = (itemData.quantity) ? itemData.quantity : 1;
itemCount += quantity;
totalPrice += (itemData.price * quantity);
}
});
await cartRef.update({
totalPrice,
itemCount
});
// OPTIONAL LOGGING HERE
console.log("Cart total successfully recalculated: ", totalPrice);
} catch(err) {
// OPTIONAL LOGGING HERE
console.warn("update error", err);
}
});
19. Повторить тесты
В командной строке убедитесь, что эмуляторы все ещё работают, и повторно запустите тесты. Перезапускать эмуляторы не нужно, поскольку они автоматически подхватывают изменения в функциях. Вы должны увидеть, что все тесты пройдены:
$ npm test
> functions@ test .../emulators-codelab/codelab-initial-state/functions
> mocha
shopping cart creation
✓ can be created by the cart owner (306ms)
shopping cart reads, updates, and deletes
✓ cart can be read by the cart owner (59ms)
shopping cart items
✓ items can be read by the cart owner
✓ items can be added by the cart owner
adding an item to the cart recalculates the cart total.
✓ should sum the cost of their items (800ms)
5 passing (1s)
Отлично!
20. Попробуйте использовать интерфейс витрины
Для финального теста вернитесь в веб-приложение (http://127.0.0.1:5000/) и добавьте товар в корзину.

Убедитесь, что в корзине указана правильная итоговая сумма. Прекрасно!
Recap
Вы рассмотрели сложный тестовый пример между Cloud Functions для Firebase и Cloud Firestore. Вы написали функцию Cloud Functions, чтобы пройти тест. Вы также подтвердили, что новые функции работают в пользовательском интерфейсе. Вы сделали все это локально, запустив эмуляторы на своем компьютере.
Вы также создали веб-клиент, который работает с локальными эмуляторами, настроили правила безопасности для защиты данных и протестировали правила безопасности с помощью локальных эмуляторов.
