1. Başlamadan önce
Cloud Firestore ve Cloud Functions gibi sunucusuz arka uç araçlarının kullanımı çok kolay olsa da test edilmesi zor olabilir. Firebase Local Emulator Suite, bu hizmetlerin yerel sürümlerini geliştirme makinenizde çalıştırmanıza olanak tanır. Böylece uygulamanızı hızlı ve güvenli bir şekilde geliştirebilirsiniz.
Ön koşullar
- Visual Studio Code, Atom veya Sublime Text gibi basit bir düzenleyici
- Node.js 10.0.0 veya daha yeni bir sürüm (Node.js'yi yüklemek için nvm'yi kullanın, sürümünüzü kontrol etmek için
node --version
komutunu çalıştırın) - Java 7 veya daha yeni bir sürüm (Java'yı yüklemek için bu talimatları kullanın, sürümünüzü kontrol etmek için
java -version
komutunu çalıştırın)
Yapacaklarınız
Bu codelab'de, birden fazla Firebase hizmeti tarafından desteklenen basit bir online alışveriş uygulamasını çalıştırıp hatalarını ayıklayacaksınız:
- Cloud Firestore: Küresel olarak ölçeklenebilir, sunucusuz ve NoSQL veritabanı olup gerçek zamanlı özelliklere sahiptir.
- Cloud Functions: Etkinliklere veya HTTP isteklerine yanıt olarak çalışan sunucusuz bir arka uç kodudur.
- Firebase Authentication: Diğer Firebase ürünleriyle entegre olan, yönetilen bir kimlik doğrulama hizmetidir.
- Firebase Hosting: Web uygulamaları için hızlı ve güvenli barındırma.
Yerel geliştirme özelliğini etkinleştirmek için uygulamayı Emulator Suite'e bağlayacaksınız.
Ayrıca şunları da öğreneceksiniz:
- Uygulamanızı Emulator Suite'e bağlama ve çeşitli emülatörlerin nasıl bağlandığı.
- Firebase Güvenlik Kuralları'nın işleyiş şekli ve Firestore Güvenlik Kuralları'nın yerel bir emülatöre karşı nasıl test edileceği.
- Firestore etkinlikleri tarafından tetiklenen bir Firebase işlevinin nasıl yazılacağı ve Emulator Suite'e karşı çalışan entegrasyon testlerinin nasıl yazılacağı.
2. Kur
Kaynak kodu alma
Bu codelab'de, The Fire Store örneğinin neredeyse tamamlanmış bir sürümüyle başlayacaksınız. Bu nedenle, yapmanız gereken ilk şey kaynak kodu klonlamak:
$ git clone https://github.com/firebase/emulators-codelab.git
Ardından, bu codelab'in geri kalanında çalışacağınız codelab dizinine geçin:
$ cd emulators-codelab/codelab-initial-state
Şimdi, kodu çalıştırabilmek için bağımlılıkları yükleyin. Yavaş bir internet bağlantısı kullanıyorsanız bu işlem bir veya iki dakika sürebilir:
# Move into the functions directory
$ cd functions
# Install dependencies
$ npm install
# Move back into the previous directory
$ cd ../
Firebase CLI'yı edinme
Emulator Suite, Firebase CLI'nin (komut satırı arayüzü) bir parçasıdır ve aşağıdaki komutla makinenize yüklenebilir:
$ npm install -g firebase-tools
Ardından, CLI'nın en son sürümüne sahip olduğunuzu onaylayın. Bu codelab, 9.0.0 veya sonraki sürümlerde çalışır ancak daha yeni sürümlerde daha fazla hata düzeltmesi bulunur.
$ firebase --version 9.6.0
Firebase projenize bağlanma
Firebase projesi oluşturma
- Google Hesabınızı kullanarak Firebase konsolunda oturum açın.
- Yeni bir proje oluşturmak için düğmeyi tıklayın ve ardından bir proje adı girin (örneğin,
Emulators Codelab
).
- Devam'ı tıklayın.
- İstenirse Firebase şartlarını inceleyip kabul edin ve Devam'ı tıklayın.
- (İsteğe bağlı) Firebase konsolunda yapay zeka yardımını etkinleştirin ("Firebase'de Gemini" olarak adlandırılır).
- Bu codelab için Google Analytics'e ihtiyacınız yoktur. Bu nedenle, Google Analytics seçeneğini devre dışı bırakın.
- Proje oluştur'u tıklayın, projenizin hazırlanmasını bekleyin ve ardından Devam'ı tıklayın.
Kodunuzu Firebase projenize bağlama
Şimdi bu kodu Firebase projenize bağlamanız gerekiyor. Önce Firebase CLI'de oturum açmak için aşağıdaki komutu çalıştırın:
$ firebase login
Ardından, proje takma adı oluşturmak için aşağıdaki komutu çalıştırın. $YOUR_PROJECT_ID
yerine Firebase projenizin kimliğini yazın.
$ firebase use $YOUR_PROJECT_ID
Artık uygulamayı çalıştırmaya hazırsınız.
3. Emülatörleri çalıştırma
Bu bölümde uygulamayı yerel olarak çalıştıracaksınız. Bu, Emulator Suite'i başlatma zamanı geldiği anlamına gelir.
Emülatörleri başlatma
Emülatörleri başlatmak için codelab kaynak dizininden aşağıdaki komutu çalıştırın:
$ firebase emulators:start --import=./seed
Aşağıdakine benzer bir çıkış görmeniz gerekir:
$ 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 (Tüm emülatörler başlatıldı) mesajını gördüğünüzde uygulama kullanıma hazırdır.
Web uygulamasını emülatörlere bağlama
Günlüklerdeki tabloya göre Cloud Firestore emülatörünün 8080
bağlantı noktasında, Authentication emülatörünün ise 9099
bağlantı noktasında dinleme yaptığını görüyoruz.
┌────────────────┬────────────────┬─────────────────────────────────┐ │ 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 │ └────────────────┴────────────────┴─────────────────────────────────┘
Ön uç kodunuzu üretime değil, emülatöre bağlayalım. public/js/homepage.js
dosyasını açın ve onDocumentReady
işlevini bulun. Kodun standart Firestore ve Auth örneklerine eriştiğini görüyoruz:
public/js/homepage.js
const auth = firebaseApp.auth();
const db = firebaseApp.firestore();
Şimdi de db
ve auth
nesnelerini yerel emülatörlere yönlendirecek şekilde güncelleyelim:
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);
}
Artık uygulama yerel makinenizde çalışırken (Hosting Emulator tarafından sunulur) Firestore istemcisi de üretim veritabanı yerine yerel emülatöre yönlendirilir.
EmulatorUI'yı açma
Web tarayıcınızda http://127.0.0.1:4000/ adresine gidin. Emulator Suite kullanıcı arayüzünü görmeniz gerekir.
Firestore Emulator'ın kullanıcı arayüzünü görmek için tıklayın. items
koleksiyonu, --import
işaretiyle içe aktarılan veriler nedeniyle zaten veri içeriyor.
4. Uygulamayı çalıştırma
Uygulamayı aç
Web tarayıcınızda http://127.0.0.1:5000 adresine gidin. The Fire Store'un makinenizde yerel olarak çalıştığını görürsünüz.
Uygulamayı kullanma
Ana sayfada bir öğe seçip Sepete Ekle'yi tıklayın. Maalesef aşağıdaki hatayla karşılaşacaksınız:
Bu hatayı düzeltelim. Her şey emülatörlerde çalıştığından, gerçek verileri etkileme endişesi olmadan denemeler yapabiliriz.
5. Uygulamada hata ayıklama
Hatayı bulma
Şimdi Chrome geliştirici konsoluna bakalım. Konsoldaki hatayı görmek için Control+Shift+J
(Windows, Linux, Chrome OS) veya Command+Option+J
(Mac) tuşuna basın:
addToCart
yönteminde bir hata olduğu anlaşılıyor. Buna bir göz atalım. Bu yöntemde uid
adlı öğeye nereden erişmeye çalışıyoruz ve neden null
oluyor? Şu anda public/js/homepage.js
'da yöntem şu şekilde görünür:
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);
}
Aha! Uygulamada oturum açmadık. Firebase Authentication belgelerine göre oturum açmadığımızda auth.currentUser
, null
olur. Bunun için bir kontrol ekleyelim:
public/js/homepage.js
addToCart(id, itemData) {
// ADD THESE LINES
if (this.auth.currentUser === null) {
this.showError("You must be signed in!");
return;
}
// ...
}
Uygulamayı test etme
Şimdi sayfayı yenileyin ve Sepete Ekle'yi tıklayın. Bu kez daha iyi bir hata mesajı alırsınız:
Ancak üst araç çubuğunda Oturum Aç'ı ve ardından Sepete Ekle'yi tekrar tıkladığınızda sepetin güncellendiğini görürsünüz.
Ancak sayılar doğru görünmüyor:
Endişelenmeyin, bu hatayı kısa süre içinde düzelteceğiz. Öncelikle, sepetinize bir ürün eklediğinizde ne olduğunu ayrıntılı olarak inceleyelim.
6. Yerel işlev tetikleyicileri
Sepete Ekle'yi tıkladığınızda birden fazla emülatörün dahil olduğu bir etkinlik zinciri başlar. Firebase CLI günlüklerinde, alışveriş sepetinize bir öğe ekledikten sonra aşağıdaki gibi mesajlar görmeniz gerekir:
i functions: Beginning execution of "calculateCart" i functions: Finished "calculateCart" in ~1s
Bu günlüklerin ve gözlemlediğiniz kullanıcı arayüzü güncellemesinin oluşturulmasına neden olan dört önemli etkinlik vardı:
1) Firestore Yazma - İstemci
Firestore koleksiyonuna /carts/{cartId}/items/{itemId}/
yeni bir doküman eklenir. Bu kodu public/js/homepage.js
içindeki addToCart
işlevinde görebilirsiniz:
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 Function Tetiklendi
calculateCart
Cloud Functions işlevi, functions/index.js
bölümünde görebileceğiniz onWrite
tetikleyicisini kullanarak sepet öğelerinde gerçekleşen tüm yazma etkinliklerini (oluşturma, güncelleme veya silme) dinler:
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
işlevi, alışveriş sepetindeki tüm öğeleri okur, toplam miktarı ve fiyatı toplar, ardından "alışveriş sepeti" belgesini yeni toplamlarla günceller (yukarıdaki cartRef.update(...)
bölümüne bakın).
4) Firestore Okuma - İstemci
Web ön ucu, alışveriş sepetindeki değişikliklerle ilgili güncellemeleri almak için abone olur. Cloud Functions yeni toplamları yazıp kullanıcı arayüzünü güncelledikten sonra gerçek zamanlı bir güncelleme alır. Bu durumu public/js/homepage.js
bölümünde görebilirsiniz:
public/js/homepage.js
this.cartUnsub = cartRef.onSnapshot(cart => {
// The cart document was changed, update the UI
// ...
});
Özet
Bravo! Tamamen yerel test için üç farklı Firebase emülatörü kullanan tamamen yerel bir uygulama oluşturdunuz.
Bir saniye, hepsi bu kadar değil! Bir sonraki bölümde şunları öğreneceksiniz:
- Firebase emülatörlerini kullanan birim testleri yazma
- Güvenlik kurallarınızda hata ayıklamak için Firebase Emülatörleri'ni kullanma
7. Uygulamanıza özel güvenlik kuralları oluşturma
Web uygulamamız verileri okuyup yazıyor ancak şu ana kadar güvenlik konusunda pek endişelenmedik. Cloud Firestore, verileri okuma ve yazma erişimine kimlerin sahip olduğunu bildirmek için "Güvenlik Kuralları" adlı bir sistem kullanır. Emulator Suite, bu kuralların prototipini oluşturmak için harika bir yöntemdir.
Düzenleyicide emulators-codelab/codelab-initial-state/firestore.rules
dosyasını açın. Kurallarımızda üç ana bölüm olduğunu göreceksiniz:
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;
}
}
}
Şu anda herkes veritabanımızda veri okuyup yazabilir. Yalnızca geçerli işlemlerin gerçekleştirilmesini ve hassas bilgilerin sızdırılmamasını sağlamak istiyoruz.
Bu codelab sırasında, en az ayrıcalık ilkesine uyarak tüm dokümanları kilitleyecek ve tüm kullanıcılar ihtiyaç duydukları erişimlere sahip olana kadar erişimi kademeli olarak ekleyeceğiz. Koşulu false
olarak ayarlayarak ilk iki kuralı erişimi reddedecek şekilde güncelleyelim:
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. Emülatörleri ve testleri çalıştırma
Emülatörleri başlatma
Komut satırında emulators-codelab/codelab-initial-state/
içinde olduğunuzdan emin olun. Önceki adımlardan kalan emülatörler çalışıyor olabilir. Değilse emülatörleri tekrar başlatın:
$ firebase emulators:start --import=./seed
Emülatörler çalıştıktan sonra bunlara karşı yerel olarak testler yapabilirsiniz.
Testleri çalıştırma
emulators-codelab/codelab-initial-state/
dizininden yeni bir terminal sekmesinde komut satırında
Öncelikle işlevler dizinine gidin (codelab'in geri kalanında burada kalacağız):
$ cd functions
Şimdi işlevler dizininde mocha testlerini çalıştırın ve çıktının en üstüne kaydırın:
# 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
Şu anda dört başarısızlık var. Kurallar dosyasını oluştururken daha fazla testin başarılı olduğunu görerek ilerleme durumunu ölçebilirsiniz.
9. Alışveriş sepetine güvenli erişim
İlk iki hata, aşağıdakileri test eden "alışveriş sepeti" testleridir:
- Kullanıcılar yalnızca kendi alışveriş sepetlerini oluşturabilir ve güncelleyebilir.
- Kullanıcılar yalnızca kendi alışveriş sepetlerini okuyabilir.
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());
});
Bu testlerin geçmesini sağlayalım. Düzenleyicide güvenlik kuralları dosyasını (firestore.rules
) açın ve match /carts/{cartID}
içindeki ifadeleri güncelleyin:
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;
}
// ...
}
}
Bu kurallar artık yalnızca alışveriş sepeti sahibinin okuma ve yazma erişimine izin veriyor.
Gelen verileri ve kullanıcının kimlik doğrulamasını doğrulamak için her kuralın bağlamında kullanılabilen iki nesne kullanırız:
request
nesnesi, denenmekte olan işlemle ilgili verileri ve meta verileri içerir.- Bir Firebase projesi Firebase Authentication kullanıyorsa
request.auth
nesnesi, isteği yapan kullanıcıyı açıklar.
10. Test alışveriş sepeti erişimi
Emulator Suite, firestore.rules
kaydedildiğinde kuralları otomatik olarak günceller. Emülatörün kuralları güncellediğini, emülatörü çalıştıran sekmede Rules updated
mesajını arayarak doğrulayabilirsiniz:
Testleri yeniden çalıştırın ve ilk iki testin başarılı olduğunu doğrulayın:
$ 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
Tebrikler! Artık alışveriş sepetlerine güvenli bir şekilde erişebilirsiniz. Bir sonraki başarısız teste geçelim.
11. Kullanıcı arayüzünde "alışveriş sepetine ekleme" akışını kontrol edin.
Şu anda alışveriş sepeti sahipleri, sepetlerine okuma ve yazma işlemi yapabilse de sepetlerindeki öğeleri tek tek okuyamaz veya yazamaz. Bunun nedeni, sahiplerin alışveriş sepeti dokümanına erişimi olmasına rağmen alışveriş sepetinin öğeler alt koleksiyonuna erişimlerinin olmamasıdır.
Bu durum, kullanıcılar için bozuk bir durumdur.
http://127.0.0.1:5000,
üzerinde çalışan web kullanıcı arayüzüne dönün ve sepetinize bir şeyler eklemeyi deneyin. Kullanıcılara henüz items
alt koleksiyonunda oluşturulan dokümanlara erişim izni vermediğimiz için hata ayıklama konsolundan görülebilen bir Permission Denied
hatası alırsınız.
12. Alışveriş sepeti öğelerine erişime izin verin
Bu iki test, kullanıcıların yalnızca kendi alışveriş sepetlerine öğe ekleyebildiğini veya kendi alışveriş sepetlerindeki öğeleri okuyabildiğini doğrular:
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
}));
});
Bu nedenle, mevcut kullanıcının UID'si, alışveriş sepeti dokümanındaki ownerUID ile aynıysa erişime izin veren bir kural yazabiliriz. create, update, delete
için farklı kurallar belirtmenize gerek olmadığından, verileri değiştiren tüm istekler için geçerli olan bir write
kuralı kullanabilirsiniz.
Öğeler alt koleksiyonundaki dokümanlar için kuralı güncelleyin. Koşullu ifadede yer alan get
, Firestore'dan bir değer okur. Bu örnekte, alışveriş sepeti dokümanındaki ownerUID
değerini okur.
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. Alışveriş sepeti öğelerine erişimi test etme
Şimdi testi yeniden çalıştırabiliriz. Çıkışın en üstüne gidin ve daha fazla testin başarılı olduğunu doğrulayın:
$ 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
Güzel! Artık tüm testlerimiz başarılı oluyor. Beklemede olan bir test var ancak bu konuya birkaç adım sonra değineceğiz.
14. "Alışveriş sepetine ekleme" akışını tekrar kontrol edin.
Web ön ucuna ( http://127.0.0.1:5000) dönün ve alışveriş sepetine bir öğe ekleyin. Bu, testlerimizin ve kurallarımızın müşterinin istediği işlevselliğe uygun olduğunu doğrulamak için önemli bir adımdır. (Kullanıcıların, kullanıcı arayüzünü son denediğimizde sepete öğe ekleyemediğini hatırlatırız.)
firestore.rules
kaydedildiğinde istemci kuralları otomatik olarak yeniden yükler. Bu nedenle, sepete bir ürün eklemeyi deneyin.
Özet
Bravo! Uygulamanızın güvenliğini artırdınız. Bu, uygulamanızı üretime hazırlamak için önemli bir adımdır. Bu bir üretim uygulaması olsaydı bu testleri sürekli entegrasyon işlem hattımıza ekleyebilirdik. Bu sayede, başkaları kuralları değiştirse bile alışveriş sepeti verilerimizin bu erişim kontrollerine sahip olacağından emin olabiliriz.
Ama dahası da var!
Devam ederseniz şunları öğreneceksiniz:
- Firestore etkinliği tarafından tetiklenen bir işlev yazma
- Birden fazla emülatörde çalışan testler oluşturma
15. Cloud Functions testleri ayarlama
Şimdiye kadar web uygulamamızın ön ucuna ve Firestore güvenlik kurallarına odaklandık. Ancak bu uygulama, kullanıcının alışveriş sepetini güncel tutmak için Cloud Functions'ı da kullandığından bu kodu da test etmek istiyoruz.
Emulator Suite, Cloud Firestore ve diğer hizmetleri kullanan işlevler de dahil olmak üzere Cloud Functions'ı test etmeyi çok kolay hale getirir.
Düzenleyicide emulators-codelab/codelab-initial-state/functions/test.js
dosyasını açın ve dosyadaki son teste gidin. Şu anda beklemede olarak işaretlenmiştir:
// 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 () => {
...
});
});
Testi etkinleştirmek için .skip
simgesini kaldırın. Kod şu şekilde görünmelidir:
describe("adding an item to the cart recalculates the cart total. ", () => {
// ...
it("should sum the cost of their items", async () => {
...
});
});
Ardından, dosyanın en üstündeki REAL_FIREBASE_PROJECT_ID
değişkenini bulup gerçek Firebase proje kimliğinizle değiştirin:
// CHANGE THIS LINE
const REAL_FIREBASE_PROJECT_ID = "changeme";
Proje kimliğinizi unuttuysanız Firebase proje kimliğinizi Firebase konsolundaki Proje Ayarları'nda bulabilirsiniz:
16. Functions testlerini inceleme
Bu test, Cloud Firestore ile Cloud Functions arasındaki etkileşimi doğruladığından önceki codelab'lerdeki testlere kıyasla daha fazla kurulum gerektirir. Bu testi inceleyerek ne beklendiği hakkında fikir edinelim.
Alışveriş sepeti oluşturma
Cloud Functions, güvenilir bir sunucu ortamında çalışır ve Yönetici SDK'sı tarafından kullanılan hizmet hesabı kimlik doğrulamasını kullanabilir . Öncelikle, initializeApp
yerine initializeAdminApp
kullanarak bir uygulamayı başlatırsınız. Ardından, öğe ekleyeceğimiz alışveriş sepeti için bir DocumentReference oluşturur ve alışveriş sepetini başlatırsınız:
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 });
...
});
İşlevi tetikleme
Ardından, işlevi tetiklemek için sepet dokümanımızın items
alt koleksiyonuna doküman ekleyin. İşlevde gerçekleşen eklemeyi test ettiğinizden emin olmak için iki öğe ekleyin.
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 });
...
});
});
Test beklentilerini belirleme
Alışveriş sepeti dokümanındaki değişiklikler için bir dinleyici kaydetmek üzere onSnapshot()
kullanın. onSnapshot()
, dinleyicinin kaydını silmek için çağırabileceğiniz bir işlev döndürür.
Bu test için toplam maliyeti 9, 98 ABD doları olan iki ürün ekleyin. Ardından, alışveriş sepetinde beklenen itemCount
ve totalPrice
olup olmadığını kontrol edin. Bu durumda işlev görevini yerine getirmiş demektir.
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. Testleri çalıştırma
Önceki testlerden kalan emülatörler çalışıyor olabilir. Başlamadıysa emülatörleri başlatın. Komut satırından şunu çalıştırın:
$ firebase emulators:start --import=./seed
Yeni bir terminal sekmesi açın (emülatörler çalışmaya devam etsin) ve functions dizinine gidin. Güvenlik kuralları testlerinden bu pencere açık kalmış olabilir.
$ cd functions
Şimdi birim testlerini çalıştırın. Toplam 5 test görmeniz gerekir:
$ 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
Belirli bir hataya baktığınızda bunun zaman aşımı hatası olduğu görülüyor. Bunun nedeni, testin işlevin doğru şekilde güncellenmesini beklemesi ancak işlevin hiçbir zaman güncellenmemesidir. Şimdi de testi karşılayacak işlevi yazmaya hazırız.
18. İşlev yazma
Bu testi düzeltmek için functions/index.js
uygulamasındaki işlevi güncellemeniz gerekir. Bu işlevin bir kısmı yazılmış olsa da tamamlanmamıştır. İşlev şu anda aşağıdaki gibi görünmektedir:
// 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) {
}
});
İşlev, alışveriş sepeti referansını doğru şekilde ayarlıyor ancak daha sonra totalPrice
ve itemCount
değerlerini hesaplamak yerine bunları sabit kodlanmış değerlerle güncelliyor.
items
alt koleksiyon
itemsSnap
alt koleksiyonu olacak yeni bir sabit (itemsSnap
) başlatın.items
Ardından, koleksiyondaki tüm dokümanları yineleyin.
// 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 ve itemCount değerlerini hesaplama
Öncelikle totalPrice
ve itemCount
değerlerini sıfır olarak başlatalım.
Ardından, mantığı yineleme bloğumuza ekleyin. Öncelikle öğenin fiyatı olup olmadığını kontrol edin. Öğenin miktarı belirtilmemişse varsayılan olarak 1
olsun. Ardından, miktarı itemCount
toplamına ekleyin. Son olarak, öğenin fiyatını miktarla çarpıp totalPrice
toplamına ekleyin:
// 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) {
}
});
Başarı ve hata durumlarında hata ayıklamaya yardımcı olması için günlüğe kaydetme de ekleyebilirsiniz:
// 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. Testleri yeniden çalıştırma
Komut satırında, emülatörlerin hâlâ çalıştığından emin olun ve testleri yeniden çalıştırın. Emülatörler, işlevlerdeki değişiklikleri otomatik olarak algıladığı için yeniden başlatmanız gerekmez. Tüm testlerin başarılı olduğunu görmeniz gerekir:
$ 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)
Tebrikler!
20. Vitrin kullanıcı arayüzünü kullanarak deneme
Son test için web uygulamasına ( http://127.0.0.1:5000/) dönün ve alışveriş sepetine bir öğe ekleyin.
Alışveriş sepetinin doğru toplamla güncellendiğini onaylayın. Çok teşekkür ederim.
Özet
Cloud Functions for Firebase ile Cloud Firestore arasındaki karmaşık bir test senaryosunu incelediniz. Testin geçmesini sağlamak için bir Cloud Functions işlevi yazdınız. Ayrıca yeni işlevin kullanıcı arayüzünde çalıştığını da onayladınız. Tüm bu işlemleri yerel olarak, emülatörleri kendi makinenizde çalıştırarak yaptınız.
Ayrıca, yerel emülatörlere karşı çalışan bir web istemcisi oluşturdunuz, verileri korumak için güvenlik kurallarını özelleştirdiniz ve güvenlik kurallarını yerel emülatörleri kullanarak test ettiniz.