Локальная разработка с помощью пакета эмулятора Firebase

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 с возможностями работы в режиме реального времени.
  • Облачные функции : бессерверный серверный код, который запускается в ответ на события или HTTP-запросы.
  • Firebase Authentication : управляемая служба аутентификации, которая интегрируется с другими продуктами Firebase.
  • Хостинг Firebase : быстрый и безопасный хостинг для веб-приложений.

Вы подключите приложение к Emulator Suite, чтобы обеспечить локальную разработку.

2589e2f95b74fa88.png

Вы также узнаете, как:

  • Как подключить ваше приложение к 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 CLI (интерфейс командной строки), который можно установить на ваш компьютер с помощью следующей команды:

$ npm install -g firebase-tools

Затем убедитесь, что у вас установлена ​​последняя версия CLI. Эта лаборатория кода должна работать с версией 9.0.0 или выше, но более поздние версии содержат больше исправлений ошибок.

$ firebase --version
9.6.0

Подключитесь к своему проекту Firebase

Если у вас нет проекта Firebase, в консоли Firebase создайте новый проект Firebase. Запишите выбранный вами идентификатор проекта, он понадобится вам позже.

Теперь нам нужно подключить этот код к вашему проекту Firebase. Сначала выполните следующую команду, чтобы войти в интерфейс командной строки Firebase:

$ firebase login

Затем выполните следующую команду, чтобы создать псевдоним проекта. Замените $YOUR_PROJECT_ID идентификатором вашего проекта Firebase.

$ firebase use $YOUR_PROJECT_ID

Теперь вы готовы запустить приложение!

3. Запустите эмуляторы

В этом разделе вы запустите приложение локально. Это означает, что пришло время загрузить Emulator Suite.

Запустите эмуляторы

Из исходного каталога codelab выполните следующую команду, чтобы запустить эмуляторы:

$ 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.

Как только вы увидите сообщение «Все эмуляторы запущены» , приложение готово к использованию.

Подключите веб-приложение к эмуляторам

На основании таблицы в журналах мы видим, что эмулятор Cloud Firestore прослушивает порт 8080 , а эмулятор аутентификации — порт 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:

общественный/js/homepage.js

  const auth = firebaseApp.auth();
  const db = firebaseApp.firestore();

Давайте обновим объекты db и auth , чтобы они указывали на локальные эмуляторы:

общественный/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 также указывает на локальный эмулятор, а не на производственную базу данных.

Откройте интерфейс эмулятора.

В веб-браузере перейдите по адресу http://127.0.0.1:4000/ . Вы должны увидеть пользовательский интерфейс Emulator Suite.

Главный экран пользовательского интерфейса эмуляторов

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

4ef88d0148405d36.png

4. Запустите приложение

Откройте приложение

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

939f87946bac2ee4.png

Используйте приложение

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

a11bd59933a8e885.png

Давайте исправим этот баг! Поскольку все работает в эмуляторах, мы можем экспериментировать и не беспокоиться о том, что это повлияет на реальные данные.

5. Отладка приложения

Найдите ошибку

Хорошо, давайте посмотрим в консоли разработчика Chrome. Нажмите Control+Shift+J (Windows, Linux, Chrome OS) или Command+Option+J (Mac), чтобы увидеть ошибку на консоли:

74c45df55291dab1.png

Похоже, в методе addToCart произошла какая-то ошибка, давайте посмотрим на это. Где мы пытаемся получить доступ к чему-то под названием uid в этом методе и почему оно должно быть null ? Сейчас метод выглядит так в public/js/homepage.js :

общественный/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 . Давайте добавим проверку для этого:

общественный/js/homepage.js

  addToCart(id, itemData) {
    // ADD THESE LINES
    if (this.auth.currentUser === null) {
      this.showError("You must be signed in!");
      return;
    }

    // ...
  }

Протестируйте приложение

Теперь обновите страницу и нажмите «Добавить в корзину» . На этот раз вы должны получить более приятную ошибку:

c65f6c05588133f7.png

Но если вы нажмете «Войти» на верхней панели инструментов, а затем еще раз нажмите «Добавить в корзину» , вы увидите, что корзина обновилась.

Однако похоже, что цифры совсем не верны:

239f26f02f959eef.png

Не волнуйтесь, мы скоро исправим эту ошибку. Во-первых, давайте углубимся в то, что на самом деле произошло, когда вы добавили товар в корзину.

6. Триггеры локальных функций

Нажатие кнопки «Добавить в корзину» запускает цепочку событий, в которых участвуют несколько эмуляторов. В журналах Firebase CLI после добавления товара в корзину вы должны увидеть что-то вроде следующих сообщений:

i  functions: Beginning execution of "calculateCart"
i  functions: Finished "calculateCart" in ~1s

Было четыре ключевых события, которые привели к созданию этих журналов и наблюдаемому вами обновлению пользовательского интерфейса:

68c9323f2ad10f7a.png

1) Запись в Firestore — клиент

В коллекцию Firestore добавляется новый документ /carts/{cartId}/items/{itemId}/ . Вы можете увидеть этот код в функции addToCart внутри public/js/homepage.js :

общественный/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) Сработала функция облака

Облачная функция calculateCart прослушивает любые события записи (создание, обновление или удаление), которые происходят с элементами корзины, с помощью триггера onWrite , который вы можете увидеть в functions/index.js :

функции/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 — администратор

Функция calculateCart считывает все товары в корзине и складывает общее количество и цену, а затем обновляет документ «корзина» новыми итоговыми суммами (см. cartRef.update(...) выше).

4) Чтение Firestore — клиент

Веб-интерфейс подписан на получение обновлений об изменениях в корзине. Он получает обновление в режиме реального времени после того, как облачная функция записывает новые итоги и обновляет пользовательский интерфейс, как вы можете видеть в public/js/homepage.js :

общественный/js/homepage.js

this.cartUnsub = cartRef.onSnapshot(cart => {
   // The cart document was changed, update the UI
   // ...
});

Резюме

Хорошая работа! Вы только что установили полностью локальное приложение, которое использует три разных эмулятора Firebase для полностью локального тестирования.

db82eef1706c9058.gif

Но подождите, это еще не все! В следующем разделе вы узнаете:

  • Как писать модульные тесты, использующие эмуляторы 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/

Сначала перейдите в каталог функций (мы останемся здесь до конца кодовой лаборатории):

$ cd functions

Теперь запустите тесты mocha в каталоге функций и прокрутите вывод до начала:

# 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. Безопасный доступ к корзине

Первые две неудачи — это тесты «корзины покупок», которые проверяют, что:

  • Пользователи могут создавать и обновлять только свои собственные корзины.
  • Пользователи могут читать только свои корзины

функции/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 :

5680da418b420226.png

Перезапустите тесты и убедитесь, что первые два теста пройдены:

$ 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 , которое применяется ко всем запросам, изменяющим данные.

Обновите правило для документов в подколлекции элементов. 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 ) и добавьте товар в корзину. Это важный шаг для подтверждения того, что наши тесты и правила соответствуют функциональности, необходимой клиенту. (Помните, что в последний раз, когда мы тестировали, пользователи пользовательского интерфейса не могли добавлять товары в свою корзину!)

69ad26cee520bf24.png

Клиент автоматически перезагружает правила при сохранении файла firestore.rules . Итак, попробуйте добавить что-нибудь в корзину.

Резюме

Хорошая работа! Вы только что повысили безопасность своего приложения, а это важный шаг для его подготовки к производству! Если бы это было производственное приложение, мы могли бы добавить эти тесты в наш конвейер непрерывной интеграции. Это придаст нам уверенности в будущем, что данные нашей корзины покупок будут иметь эти средства контроля доступа, даже если другие изменят правила.

ba5440b193e75967.gif

Но подождите, это еще не все!

если вы продолжите, вы узнаете:

  • Как написать функцию, запускаемую событием Firestore
  • Как создавать тесты, которые работают на нескольких эмуляторах

15. Настройте тесты облачных функций.

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

Пакет Emulator Suite упрощает тестирование облачных функций, даже функций, использующих 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:

d6d0429b700d2b21.png

16. Прохождение функциональных тестов

Поскольку этот тест проверяет взаимодействие между Cloud Firestore и 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

Откройте новую вкладку терминала (оставьте эмуляторы включенными) и перейдите в каталог функций. Возможно, оно все еще открыто после проверки правил безопасности.

$ 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

Инициализируйте новую константу 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 равными нулю.

Затем добавьте логику в наш блок итерации. Сначала проверьте, есть ли у товара цена. Если для товара не указано количество, пусть по умолчанию оно равно 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/ ) и добавьте товар в корзину.

69ad26cee520bf24.png

Убедитесь, что в корзине указана правильная сумма. Фантастика!

Резюме

Вы прошли сложный тестовый пример между Cloud Functions for Firebase и Cloud Firestore. Вы написали облачную функцию, чтобы пройти тест. Вы также подтвердили, что новая функция работает в пользовательском интерфейсе! Вы сделали все это локально, запустив эмуляторы на своей машине.

Вы также создали веб-клиент, работающий с локальными эмуляторами, настроили правила безопасности для защиты данных и протестировали правила безопасности с использованием локальных эмуляторов.

c6a7aeb91fe97a64.gif