Kent C. Dodds'tan iyi dersler alarak, önceki bölümlerde sunucu önbelleğini ayrı bir şeye bölersek, kullanıcı arayüzü durum yönetimini büyük ölçüde basitleştirebileceğimizi belirttik. Durum iki kümeye ayrılabilir:
Kullanıcı arayüzü durumu: Modal açık, öğe vurgulanmış vb. (bunun için useState kancaını kullandık)
Sunucu önbelleği: Kullanıcı verileri, tweet'ler, kişiler vb. (react-query burada yararlıdır)
Neden react-query? Yeniden çoğaltılmış veri almayı önlemek için, tüm veri alıcı kodunu merkezi bir depoya taşımak ve ona ihtiyaç duyan bileşenlerden bu tek kaynağa erişmek istiyoruz. React Query ile böyle bir depo oluşturma işleminde herhangi bir iş yapmamıza gerek yok. Veri alıcı kodunu veriye ihtiyaç duyan bileşenlerde tutmamıza izin verir, ancak arka planda bir veri önbelleği yönetir ve bileşenler onları istediğinde zaten alınan verileri iletir.
React-query'nin useQuery kancası, veriyi anahtarla alarak ve önbelleğe alarak güncellemek içindir. Bunu bir GET isteği gibi düşünün. Anahtar argümanı, önbellekteki sorgu / veri için benzersiz bir tanımlayıcıdır; dize, dizi veya nesne. İkinci argüman, veriyi döndüren bir async işlevidir.
useMutation, useQuery'nin yazma yansımasıdır. Bunu PUT ve POST isteklerimize benzer şekilde düşünün. useMutation, data, status, error gibi useQuery ile aynı değerleri sağlar. İlk argüman, idempotent olmayan bir isteği yürüten bir işlevidir. İkinci argüman, onMutate özelliğine sahip bir nesnedir.
useQuery durumu alır: Kullanıcı arayüzü durumu <- sunucu / url ve onu önbelleğe alır.
useMutation tam tersidir: Kullanıcı arayüzü durumu -> sunucu ve yine de önbelleğe alır.
Bu bölümde API'mizi oluşturacak, kahramanlar üzerinde CRUD işlemleri için kancalar oluşturacak ve bunları bileşenlerde kullanacağız.
API
useAxios kancasındaki getItem işlevini çoğaltacağız ve diğer CRUD istekleriyle uyumlu hale getireceğiz. src/hooks/api.ts adında bir dosya oluşturun ve aşağıdaki kodu yapıştırın. Axios'u saran tür korumalı bir client işlevimiz var, bununla herhangi bir CRUD isteği yapabiliriz. Daha az argümanlı işlevlerle bunları tekrar sararız, bu şekilde kullanmaları daha kolaydır.
Basit bir kanca oluşturabilir ve karmaşık useAxios'umuzu, react-query tarafından önbellek yönetiminin performans kazanımlarını gösterirken değiştirebiliriz. yarn add react-query kullanarak ekleyin ve src/hooks/useGetHeroes.ts adında bir dosya oluşturun. react-query'in useQuery işlevi, özel useAxios'umuza benzer: bir url alır, veri, durum ve hata nesnesi döndürür.
Durumu ve kullanmadığımız hatayı da döndüren useAxios ile karşılaştırın:
const {data: heroes = []} = useAxios('heroes')
Herhangi bir bileşen daha sonra anahtarla useQuery'yi çağırdığında, react-query önbelleğinden önceden alınan verileri döndürür ve ardından arka planda en son verileri alır (PWAs ve hizmet işçilerine çok benzer). Sorgu anahtarımız burada heroes dizisi ve geri çağırma işlevi api'den /heroes rotasını çağıran getItem'dır. useQuerydata, status ve error döndürür, bunları bileşenimizde daha kolay kullanım için güzel bir şekilde yeniden şekillendiririz.
Heroes bileşeninde useAxios'u değiştirmeden önce, uygulama JSX'ini QueryClientProvider adlı bir sağlayıcı bileşenle sarmamız, bir queryClient örneği oluşturmamız ve bunu QueryClientProvider'ın client prop'u olarak kullanmamız gerekmektedir. Böylece önbelleği bileşenlerin erişip paylaşabileceği şekilde kullanılabilir hale getiririz.
useGetHeroes, useAxios için yer değiştirme özelliğine sahiptir ve hatta bir argümana bile ihtiyaç duymaz. status ve getError'ı bir sonraki bölümde kullanacağız.
Uygulamayı yarn dev ile çalıştırın ve useAxios ile useGetHeroes arasında geçiş yapın. Sekmeler arasında geçiş yaparak önbelleğin sağladığı performans farkını gözlemleyin.
usePostHero
Şimdiye kadar kahraman eklemek için bir özelliğe sahip değildik. Arka uç tarafı bunu destekliyor, ancak ön uç tarafı desteklemiyor. Kahraman ekleme akışı ile başarısız olan bir testle başlayalım. Yeni testimiz, ana yolu ziyaret etmek, ekle düğmesine tıklamak, yeni sayfayı doğrulamak, rastgele kahraman adı ve açıklaması doldurmak ve değişiklikleri kaydetmek için basitçe çalışır (Kırmızı 1).
// cypress/e2e/create-hero.cy.tsimport { faker } from"@faker-js/faker";describe("Create hero", () => {it("should go through the refresh flow", () => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`).as("getHeroes");cy.visit("/");cy.wait("@getHeroes");cy.location("pathname").should("eq","/heroes");cy.getByCy("add-button").click();cy.location("pathname").should("eq","/heroes/add-hero");cy.getByCy("hero-detail").should("be.visible");cy.getByCy("input-detail-id").should("not.exist");cy.getByCy("refresh-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible"); });it("should go through the cancel flow and perform direct navigation", () => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`).as("getHeroes");cy.visit("/heroes/add-hero");cy.wait("@getHeroes");cy.getByCy("cancel-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible"); });it.only("should go through the add hero flow (ui-e2e)", () => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`).as("getHeroes");cy.visit("/");cy.wait("@getHeroes");cy.location("pathname").should("eq","/heroes");cy.getByCy("add-button").click();cy.location("pathname").should("eq","/heroes/add-hero");cy.getByCy("hero-detail").should("be.visible");cy.getByCy("input-detail-id").should("not.exist");constnewHero= { name:faker.internet.userName(), description:`description ${faker.internet.userName()}`, };cy.getByCy("input-detail-name").type(newHero.name);cy.getByCy("input-detail-description").type(newHero.description);cy.getByCy("save-button").click();// things work until herecy.location("pathname").should("eq","/heroes"); });});
useMutation hatırlayalım ve daha ileri gitmeden önce.
useParams (react-router'dan) ve useQuery (react-query'den) durumu getirir:
UI durumu <- sunucu/url ve bunu önbelleğe alır
useMutation tam tersidir:
UI durumu -> sunucu ve yine de bunu önbelleğe alır
useMutation tıpkı useQuery gibi veri, durum ve hata sağlar.
İlk argüman, idempotent olmayan bir isteği gerçekleştiren bir fonksiyondur. İkinci argüman, başarı durumunda bir nesnedir.
Bu bilgiden türetilen eksik kancamız şu şekildedir. Api çağrımızla arka planda bir şeyler oluşturacağını, bazı yeni verileri günlüğe kaydedeceğini ve /heroes adresine yönlendireceğini bekliyoruz.
HeroDetail bileşeninde, console.log kullanan bir createHero fonksiyonumuz var. Yerine koyabileceğimiz createHero fonksiyonu, kancadan elde edilen mutate değeri olabilir. Kancanın dönüş değerlerini şu şekilde atayabiliriz:
const createHero = () => console.log('createHero') ifadesini kaldırabiliriz, bunun yerine kancadan elde edilen createHero'yu kullanabiliriz. Ona bir öğe argümanı iletmemiz gerekiyor. Türü, usePostHero kancasının useMutation geri çağrısından gelir:
handleSave içindeki üçlü operatör için son bir güncelleme yapmamız gerekiyor. Şu anda hero.name üzerinde çalışıyor ve hero durum tarafından yönlendirildiği için, hero.name bizim yazdığımız şeydir. Her zaman doğru olacaktır. Yeni bir kahraman durumunda mevcut olmayan bir şeye dayandırmamız gerekiyor ve bu, url'den aldığımız ad ve açıklama olabilir:
const {name, description} = useHeroParams()
Kahraman yoksa, url arama parametreleri de yoktur. Bu nedenle, üçlü operatörü ve card-header-title için {hero.name} jsx'ini sadece name ile değiştirebiliriz.
Şimdi, kaydettikten sonra listeye yeni oluşturulan kahramanın görünüp görünmediğini doğrulayabiliriz. Kısaltma amacıyla yalnızca çalışan testi (Kırmızı 2) gösteriyoruz.
// cypress/e2e/create-hero.cy.tsit.only("should go through the add hero flow (ui-e2e)", () => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`).as("getHeroes");cy.visit("/");cy.wait("@getHeroes");cy.location("pathname").should("eq","/heroes");cy.getByCy("add-button").click();cy.location("pathname").should("eq","/heroes/add-hero");cy.getByCy("hero-detail").should("be.visible");cy.getByCy("input-detail-id").should("not.exist");constnewHero= { name:faker.internet.userName(), description:`description ${faker.internet.userName()}`, };cy.getByCy("input-detail-name").type(newHero.name);cy.getByCy("input-detail-description").type(newHero.description);cy.getByCy("save-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible").should("contain",newHero.name).and("contain",newHero.description);});
Yeni oluşturulan varlığı arka uçta (db.json güncellendi) görüyoruz ve başka bir sekmeye gidip geri dönersek, yeni oluşturulan varlığı da görüyoruz. Ne yazık ki, kaydettikten hemen sonra HeroList'te değil. Bu, önbellek yönetimindeki bir eksikliğe işaret ediyor. Arka ucunu değiştirdiğimizde, aynı zamanda yeni önbelleği de güncellememiz gerekiyor. Bunun için queryClient'ın setQueryData metodunu kullanıyoruz. setQueryData, ilk argüman olarak bir anahtar, ikinci argüman olarak eski sorgu önbelleğini alan ve yeni olanını döndüren bir geri arama alır. Bu geliştirmelerle, testimiz geçiyor (Yeşil 2).
// cypress/e2e/create-hero.cy.tsimport { faker } from"@faker-js/faker";describe("Create hero", () => {it("should go through the refresh flow", () => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`).as("getHeroes");cy.visit("/");cy.wait("@getHeroes");cy.location("pathname").should("eq","/heroes");cy.getByCy("add-button").click();cy.location("pathname").should("eq","/heroes/add-hero");cy.getByCy("hero-detail").should("be.visible");cy.getByCy("input-detail-id").should("not.exist");cy.getByCy("refresh-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible"); });it("should go through the cancel flow and perform direct navigation", () => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`).as("getHeroes");cy.visit("/heroes/add-hero");cy.wait("@getHeroes");cy.getByCy("cancel-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible"); });it("should go through the add hero flow (ui-e2e)", () => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`).as("getHeroes");cy.visit("/");cy.wait("@getHeroes");cy.location("pathname").should("eq","/heroes");cy.getByCy("add-button").click();cy.location("pathname").should("eq","/heroes/add-hero");cy.getByCy("hero-detail").should("be.visible");cy.getByCy("input-detail-id").should("not.exist");constnewHero= { name:faker.internet.userName(), description:`description ${faker.internet.userName()}`, };cy.getByCy("input-detail-name").type(newHero.name);cy.getByCy("input-detail-description").type(newHero.description);cy.getByCy("save-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible").should("contain",newHero.name).and("contain",newHero.description); });});
ui-e2e ile ui-integration testleri arasındaki fark
İlk iki teste dikkat edin. Ekleme akışını iptal veya yenileme ile bitiriyorlar. İkisi de arka uca yazma isteği göndermez, sadece veriyi oradan okurlar. Veriyi arka uçtan okuduğumuz gerçeği ve arka uca yazdığımız gerçeği, e2e olarak kalması gereken 3. testte kapsanmıştır. Ancak ilk iki test tamamen ağı taklit ederek ui-integration testlerine dönüşebilir. ui-integration testleri nedir? Test Metodolojileri Listesi yazısından:
Bunlar, UI e2e testlerine benzer, ancak ağı tamamen taklit ederler ve gerçek bir sunucuya çarpmadan çalıştırılırlar. Ağ endişesi olmadığından daha hızlıdırlar ve geleneksel UI e2e testlerinden daha az kırılgandırlar. Shift-left yaklaşımı için harikadırlar ve arka uç önemli olduğunda dağıtımlarda test etmeden önce kullanıcı arayüzü işlevini izole etmek için kullanışlıdırlar.
Eğer Kent C. Dodd'un Epic React serisinden geçtiyseniz, React Testing Library kullanarak entegrasyon testlerinin kendi sürümünü görmüş olabilirsiniz. Buradaki ayrım, gerçek kullanıcı arayüzünü kullanarak bileşenlerin entegrasyonunu daha yüksek bir düzeyde test ediyor olmamız ve sadece ağ verilerini taklit etmemizdir. Bazıları için gerçek tarayıcıyı, hatta gerçek uygulamayı görmek daha kolay ve güvenilir olabilir ve bu, bu kurs boyunca izleyeceğimiz yoldur.
Uygulamanızın işlevselliğine güven duymak için arka uca ihtiyacınız olup olmadığını her zaman değerlendirin. Bu güvene ihtiyaç duyduğunuzda yalnızca gerçek e2e testleri kullanmalı ve aynı maliyetli testleri her yerde tekrarlamamalısınız. Bunun yerine ui-integration testlerini kullanın. Eğer arka ucunuz kendi e2e testleriyle test edilmişse, ön ucun gerçek e2e ihtiyaçları daha da azdır; arka uç çabalarını çoğaltmamaya dikkat edin. Repo'muzda, cypress/e2e/backend/crud.cy.ts arka uç e2e testinin iyi bir örneğidir. Tesadüfen e2e'mizde veritabanı durumunu sıfırlamak veya kurmak için bazı komutlarını kullanacağız.
Test dosyamızı iptal ve yenileme akışları için ui-integration testlerini kullanacak şekilde yeniden düzenleyelim. Ağ verileri yerine, cypress/fixtures altındaki heroes.json dosyasını kullanacağız. Ayrıca testler arasındaki ortak gezinmeyi de yeniden düzenleyeceğiz (Düzenleme 2).
// cypress/e2e/create-hero.cy.tsimport { faker } from"@faker-js/faker";describe("Create hero", () => {constnavToAddHero= () => {cy.location("pathname").should("eq","/heroes");cy.getByCy("add-button").click();cy.location("pathname").should("eq","/heroes/add-hero");cy.getByCy("hero-detail").should("be.visible");cy.getByCy("input-detail-id").should("not.exist"); };it("should go through the refresh flow (ui-integration)", () => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`, { fixture:"heroes", }).as("stubbedGetHeroes");cy.visit("/");cy.wait("@stubbedGetHeroes");navToAddHero();cy.getByCy("refresh-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible"); });it("should go through the cancel flow and perform direct navigation (ui-integration)", () => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`, { fixture:"heroes", }).as("stubbedGetHeroes");cy.visit("/heroes/add-hero");cy.wait("@stubbedGetHeroes");cy.getByCy("cancel-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible"); });it("should go through the add hero flow (ui-e2e)", () => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`).as("getHeroes");cy.visit("/");cy.wait("@getHeroes");navToAddHero();constnewHero= { name:faker.internet.userName(), description:`description ${faker.internet.userName()}`, };cy.getByCy("input-detail-name").type(newHero.name);cy.getByCy("input-detail-description").type(newHero.description);cy.getByCy("save-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible").should("contain",newHero.name).and("contain",newHero.description); });});
json-server için gecikmemiz 1 saniye olduğundan, test şimdi 2 saniye daha hızlı, daha az kırılgan ve üçüncü testte gerçek GET isteğini zaten kapsadığımız için güvenimiz azalmamıştır.
İki yeniden düzenleme daha kaldı. Stub'lanmış ve doğal yollarla baseUrl'e gitmemiz gerekeceği için, bu ikisi Cypress komutları haline gelebilir. Ayrıca, üçüncü test her çalıştırıldığında veritabanını şişiriyoruz. Arka uç-e2e süitinden sil komutunu kullanabiliriz ve aynı zamanda arka uç süiti gibi veritabanını sıfırlayabiliriz.
// cypress.d.ts/* eslint-disable @typescript-eslint/no-explicit-any */import { MountOptions, MountReturn } from"cypress/react";importtype { Hero } from"./cypress/support/commands";export {};declare global {namespaceCypress {interfaceChainable {/** Yields elements with a data-cy attribute that matches a specified selector. * ``` * cy.getByCy('search-toggle') // where the selector is [data-cy="search-toggle"] * ``` */getByCy(qaSelector:string, args?:any):Chainable<JQuery<HTMLElement>>;/** Yields elements with data-cy attribute that partially matches a specified selector. * ``` * cy.getByCyLike('chat-button') // where the selector is [data-cy="chat-button-start-a-new-claim"] * ``` */getByCyLike( qaSelector:string, args?:any ):Chainable<JQuery<HTMLElement>>;/** Yields the element that partially matches the css class * ``` * cy.getByClassLike('StyledIconBase') // where the class is class="StyledIconBase-ea9ulj-0 lbJwfL" * ``` */getByClassLike( qaSelector:string, args?:any ):Chainable<JQuery<HTMLElement>>;/** Mounts a React node * @param component React Node to mount * @param options Additional options to pass into mount */mount( component:React.ReactNode, options?:MountOptions ):Cypress.Chainable<MountReturn>;/** Visits baseUrl, uses real network, verifies path */visitHeroes():Cypress.Chainable<string>;/** Visits baseUrl, uses stubbed network, verifies path */visitStubbedHeroes():Cypress.Chainable<string>;/** * Gets an entity by name. * ```js * cy.getEntityByName(newHero.name).then(myHero => ...) * ``` * @param name: Hero['name'] */getEntityByName(name:Hero["name"]):Cypress.Chainable<Hero>;/** * Performs crud operations GET, POST, PUT and DELETE. * * `body` and `allowedToFail are optional. * * If they are not passed in, body is empty but `allowedToFail` still is `false`. * * If the body is passed in and the method is `POST` or `PUT`, the payload will be taken, * otherwise undefined for `GET` and `DELETE`. * @param method * @param route * @param options: {body?: Hero | object; allowedToFail?: boolean} */crud( method:"GET"|"POST"|"PUT"|"DELETE", route:string, { body, allowedToFail =false, }: { body?:Hero|object; allowedToFail?:boolean } = {} ):Cypress.Chainable<Response<Hero[] &Hero>>;/** * Resets the data in the database to the initial data. */resetData():Cypress.Chainable<Response<Hero[] &Hero>>; } }}
Yeniden düzenlenmiş testte, her çalıştırmadan önce veritabanını sıfırlarız, başlangıç navigasyonu için komutlar ve yardımcıları kullanırız (Düzenleme) ve en sonunda üçüncü testte ona verdiğimiz isimden kahramanı çıkarır ve bir API komutu kullanarak onu sileriz.
Bitirmeden önce, cypress/e2e/network.cy.ts testine bir göz atın. Bu testin ana kontrolleri, Heroes bileşeninin ve HeroList bileşenlerinin render edilmesini sağlamaktır:
Yeni özellikler için kısmen yinelenen testler yazmaktansa, zaten mevcut olan testi değiştirmek için fırsatlar aramaya her zaman dikkat edin. Bir test açısından önemli olan şey, bir testin başlangıç durumudur; bu duruma ulaşmak ortaksa, bu, test geliştirmesi ve kısmi test yinelemesi arasında bir fırsattır.
Kahramanlar listesine geri dönerken, createhero e2e testine iki kontrolü ekleyebilir ve cypress/e2e/network.cy.ts dosyasını tamamen kaldırabiliriz. Test ve birkaç saniye test etmeden 8 satır kod tasarrufu yapıyoruz ve test süresinde yeni bir zaman eklemiyoruz; bu, BDD çerçevesinde kolay bir karar olmazdı.
// cypress/e2e/create-hero.cy.tsimport { faker } from"@faker-js/faker";describe("Create hero", () => {before(cy.resetData);constnavToAddHero= () => {cy.location("pathname").should("eq","/heroes");cy.getByCy("add-button").click();cy.location("pathname").should("eq","/heroes/add-hero");cy.getByCy("hero-detail").should("be.visible");cy.getByCy("input-detail-id").should("not.exist"); };it("should go through the refresh flow (ui-integration)", () => {cy.visitStubbedHeroes();navToAddHero();cy.getByCy("refresh-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible"); });it("should go through the cancel flow and perform direct navigation (ui-integration)", () => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`, { fixture:"heroes", }).as("stubbedGetHeroes");cy.visit("/heroes/add-hero");cy.wait("@stubbedGetHeroes");cy.getByCy("cancel-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible"); });it("should go through the add hero flow (ui-e2e)", () => {cy.visitHeroes();navToAddHero();constnewHero= { name:faker.internet.userName(), description:`description ${faker.internet.userName()}`, };cy.getByCy("input-detail-name").type(newHero.name);cy.getByCy("input-detail-description").type(newHero.description);cy.getByCy("save-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("heroes").should("be.visible");cy.getByCyLike("hero-list-item").should("have.length.gt",0);cy.getByCy("hero-list").should("contain",newHero.name).and("contain",newHero.description);cy.getEntityByProperty(newHero.name).then((myHero) =>cy.crud("DELETE",`heroes/${myHero.id}`) ); });});
cypress/e2e/routes-nav.cy.ts dosyasında bir ui-integration iyileştirmesi daha yapabiliriz. Testler, uygulamanın yollarını, arka ucun önemsiz olduğu şekilde kapsamaktadır. Herhangi bir veri olduğu sürece memnundurlar. Ağı taklit ederek bunu bir ui-integration testine dönüştürebiliriz.
İpucu: Büyük bir projede, ui-integration testlerini ui-e2e testlerinden klasörlerde ayırıyor olabilirsiniz. Ve/veya seçici testin diğer türlerini uygulayabilirsiniz (32'den fazlası var), örneğin testleri cypress-grep ile etiketleyerek.
// cypress/e2e/routes-nav.cy.tsdescribe("routes navigation", () => {beforeEach(() => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`, { fixture:"heroes", }).as("stubbedGetHeroes"); });it("should land on baseUrl, redirect to /heroes", () => {cy.visit("/");cy.getByCy("header-bar").should("be.visible");cy.getByCy("nav-bar").should("be.visible");cy.location('pathname').should('eq'"/heroes");cy.getByCy("heroes").should("be.visible"); });it("should direct-navigate to /heroes", () => {constroute="/heroes";cy.visit(route);cy.location('pathname').should('eq' route);cy.getByCy("heroes").should("be.visible"); });it("should land on not found when visiting an non-existing route", () => {constroute="/route48";cy.visit(route);cy.location('pathname').should('eq' route);cy.getByCy("not-found").should("be.visible"); });it("should direct-navigate to about", () => {constroute="/about";cy.visit(route);cy.location('pathname').should('eq' route);cy.getByCy("about").contains("CCTDD"); });it("should cover route history with browser back and forward", () => {cy.visit("/");constroutes= ["villains","heroes","about"];cy.wrap(routes).each((route:string) =>cy.get(`[href="/${route}"]`).click() );constlastIndex=routes.length-1;cy.location('pathname').should('eq' routes[lastIndex]);cy.go("back");cy.location('pathname').should('eq' routes[lastIndex -1]);cy.go("back");cy.location('pathname').should('eq' routes[lastIndex -2]);cy.go("forward").go("forward");cy.location('pathname').should('eq' routes[lastIndex]); });});
usePutHero
Bir API çağrısıyla bir kahraman ekleyen ve ardından düzenlemeye başlayan bir testle başlayacağız.
findHeroIndex komutunu ve tip tanımını ekleyerek başlayın. Kısalık adına sadece dosyaların yeni bölümlerini gösteriyoruz.
// cypress.d.ts/*** Given a hero property (name, description or id),* returns the index of the hero in the collection*/findHeroIndex( property: Hero['name'] | Hero['description'] | Hero['id'], ): number
Şimdi bir API çağrısıyla veritabanına bir kahraman ekleyebilir, uygulamayı ziyaret edebilir, kahramanın indeksini alabilir ve nth Düzenle düğmesine tıklayabiliriz. Şu ana kadar, mevcut işlevselliğe bağlı bir kahramana yönlendirmek dışında, özellikle ilgili bir şey test etmiyoruz.
// cypress/e2e/edit-hero.cy.tsimport { faker } from"@faker-js/faker";import { Hero } from"../../src/models/Hero";describe("Edit hero", () => {beforeEach(() => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`).as("getHeroes");cy.visit("/");cy.wait("@getHeroes");cy.location("pathname").should("eq","/heroes"); });it("should go through the cancel flow", () => {cy.fixture("heroes").then((heroes) => {cy.getByCy("edit-button").eq(0).click();cy.location("pathname").should("include",`/heroes/edit-hero/${heroes[0].id}` );cy.getByCy("hero-detail").should("be.visible");cy.getByCy("input-detail-id").should("be.visible");cy.findByDisplayValue(heroes[0].id).should("be.visible");cy.findByDisplayValue(heroes[0].name).should("be.visible");cy.findByDisplayValue(heroes[0].description).should("be.visible");cy.getByCy("cancel-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible"); }); });it("should go through the cancel flow for another hero", () => {cy.fixture("heroes").then((heroes) => {cy.getByCy("edit-button").eq(1).click();cy.location("pathname").should("include",`/heroes/edit-hero/${heroes[1].id}` );cy.getByCy("hero-detail").should("be.visible");cy.getByCy("input-detail-id").should("be.visible");cy.findByDisplayValue(heroes[1].id).should("be.visible");cy.findByDisplayValue(heroes[1].name).should("be.visible");cy.findByDisplayValue(heroes[1].description).should("be.visible");cy.getByCy("cancel-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible"); }); });it("should navigate to add from an existing hero", () => {cy.fixture("heroes").then((heroes) => {cy.getByCy("edit-button").eq(1).click();cy.getByCy("add-button").click();cy.getByCy("input-detail-id").should("not.exist");cy.findByDisplayValue(heroes[1].name).should("not.exist");cy.findByDisplayValue(heroes[1].description).should("not.exist"); }); });it.only("should go through the edit flow (ui-e2e)", () => {constnewHero:Hero= { id:faker.datatype.uuid(), name:faker.internet.userName(), description:`description ${faker.internet.userName()}`, };cy.crud("POST","heroes", { body: newHero });cy.visitHeroes();cy.findHeroIndex(newHero.id).then((heroIndex) =>cy.getByCy("edit-button").eq(heroIndex).click() ); });});
İlk iki testimize bakarsak, HeroList ile HeroDetails arasındaki tıklama-navigasyonunu zaten kapsıyoruz. Aynısını son testte tekrar ediyoruz. Bunun yerine url üzerinden doğrudan yönlendirebiliriz. Sorgu parametrelerini cy.visit komutuna iletebiliriz. E2e testlerle, test çabasını başka yerlerde tekrar ettiğinizi düşünün. Maliyet ek bir güven sağlamıyorsa, farklı işlevleri kapsama fırsatları bulun. Bu durumda, doğrudan yönlendirme konusunda güven kazanıyoruz, ancak aynı zamanda url'den durumu çıkarma yeteneği (hatırlayın useParams ve useSearchParams).
// cypress/e2e/edit-hero.cy.tsit.only("should go through the edit flow (ui-e2e)", () => {constnewHero:Hero= { id:faker.datatype.uuid(), name:faker.internet.userName(), description:`description ${faker.internet.userName()}`, };cy.crud("POST","heroes", { body: newHero });cy.visit(`heroes/edit-hero/${newHero.id}`, { qs: { name:newHero.name, description:newHero.description }, });});
Testin geri kalanında yapmamız gereken tek şey adı ve açıklamayı değiştirmek, kaydetmeye tıklamak, HeroList üzerinde son bulmak ve yeni verileri doğrulamaktır. Okunabilirliği kolaylaştırmak için görevler arasındaki ayrımı iletmek amacıyla nasıl boşluk kullanıldığına dikkat edin. Bu, ilk başarısız testimiz (Kırmızı 3).
usePutHero adlı özel kancamız için zamanı geldi. src/hooks/usePutHero.ts adlı bir dosya oluşturun. usePostHeroya benzer bir kullanımı taklit edin, bu sefer API'den editItem kullanarak:
(item: Hero) => editItem(heroes/${item.id}, item)
Döndürme için, bileşende değişkenlerin adlarını takma ad olarak kullanmak yerine, bir nesneyi yerinde takma ad olarak döndürme şeklinde nasıl yapılacağını gösterebiliriz.
Hook'umuzdan dönen updateHeroyu kullanabiliriz. usePostHeroya benzer şekilde, mutate (sırasıyla createHero ve updateHero olarak adlandırdığımız) bir kahraman argümanı alır ve bu, updateHeronun beklediğidir.
Aşağıdaki değişikliklerle, PUT isteğini gönderiyoruz ve düzenledikten sonra kahramanı değiştiriyoruz. Bunu koşucuda ve db.jsonda doğrulayabiliriz. Ayrıca kahramanlar listesine geri dönmekteyiz. Ancak, benzer bir önbellek sorunumuz var; başka bir yere gidip geri dönersek düzenlenmiş öğe orada, ancak hemen güncellenmiyor.
Önbellekteki kahramanı güncellenmiş sürümle değiştirmenin bir yoluna ihtiyacımız var. Önce önbellekteki tüm kahramanları alırız. Sonra düzenlenen kahramanın önbellekteki indeksini buluruz. Kahraman bulunursa, düzenlenmeden önceki kahramanı güncellenmiş olanla değiştiririz, aksi takdirde bir şey yapmayız. Bu değişiklikle, e2e düzenleme testi geçer (Yeşil 3)
// src/hooks/usePutHero.tsimport { Hero } from"models/Hero";import { useMutation, useQueryClient } from"react-query";importtype { QueryClient } from"react-query";import { useNavigate } from"react-router-dom";import { editItem } from"./api";/** * Helper for PUT to `/heroes` route * @returns{object} {updateHero, isUpdating, isUpdateError, updateError} */exportfunctionusePutHero() {constqueryClient=useQueryClient();constnavigate=useNavigate();constmutation=useMutation( (item:Hero) =>editItem(`heroes/${item.id}`, item), {onSuccess: (updatedHero:Hero) => {updateHeroesCache(updatedHero, queryClient);navigate(`/heroes`); }, } );return { updateHero:mutation.mutate, isUpdating:mutation.isLoading, isUpdateError:mutation.isError, updateError:mutation.error, };}/** Replace a hero in the cache with the updated version. */functionupdateHeroesCache(updatedHero:Hero, queryClient:QueryClient) {// get all the heroes from the cachelet heroesCache:Hero[] =queryClient.getQueryData("heroes") || [];// find the index in the cache of the hero that's been editedconstheroIndex=heroesCache.findIndex((h) =>h.id ===updatedHero.id);if (heroIndex !==-1) {// if the hero is found, replace the pre-edited hero with the updated one// this is just replacing an array item in place,// while not mutating the original array heroesCache =heroesCache.map((preEditedHero) =>preEditedHero.id ===updatedHero.id ? updatedHero : preEditedHero );// use queryClient's setQueryData to set the cache// takes a key as the first arg, the 2nd arg is the new cachereturnqueryClient.setQueryData(["heroes"], heroesCache); } elsereturnnull;}
Testimizi ve komutlarımızı yeniden düzenlemeye başlayabiliriz. Kahraman ekleme akışına benzer şekilde, son test gerçek arka ucunu okuyor ve gerçek arka uca yazıyor. Diğer testler iptal akışları ve mevcut bir kahramandan ekleme işlemine yönlendirmeyi kapsayan kenar durumudur. Bu testlerde, arka uç okunur ve verilerin ne olduğu önemli değildir, yeter ki bir veri olsun. Bu nedenle, bir kez daha ui-integrasyon testi uygulayabiliriz.
Dizideki kahraman n ve kahraman n + 1 indeksini kapsamak için rastgeleleştirme de kullanabiliriz; veriler çubuklu ve sabit olduğu için, bu durumda rastgeleleştirme uygundur.
Başlangıçta veritabanını sıfırlamayı ve e2e testinin ardından eklenen kahramanı silerek temizlemeyi unutmayalım. getEntityByName komutunu herhangi bir özellikle iletilen varlığı alacak bu şekilde yükseltebiliriz.
// cypress.d.ts/* eslint-disable @typescript-eslint/no-explicit-any */import { MountOptions, MountReturn } from"cypress/react";importtype { Hero } from"./cypress/support/commands";export {};declare global {namespaceCypress {interfaceChainable {/** Yields elements with a data-cy attribute that matches a specified selector. * ``` * cy.getByCy('search-toggle') // where the selector is [data-cy="search-toggle"] * ``` */getByCy(qaSelector:string, args?:any):Chainable<JQuery<HTMLElement>>;/** Yields elements with data-cy attribute that partially matches a specified selector. * ``` * cy.getByCyLike('chat-button') // where the selector is [data-cy="chat-button-start-a-new-claim"] * ``` */getByCyLike( qaSelector:string, args?:any ):Chainable<JQuery<HTMLElement>>;/** Yields the element that partially matches the css class * ``` * cy.getByClassLike('StyledIconBase') // where the class is class="StyledIconBase-ea9ulj-0 lbJwfL" * ``` */getByClassLike( qaSelector:string, args?:any ):Chainable<JQuery<HTMLElement>>;/** Mounts a React node * @param component React Node to mount * @param options Additional options to pass into mount */mount( component:React.ReactNode, options?:MountOptions ):Cypress.Chainable<MountReturn>;/** Visits baseUrl, uses real network, verifies path */visitHeroes():Cypress.Chainable<string>;/** Visits baseUrl, uses stubbed network, verifies path */visitStubbedHeroes():Cypress.Chainable<string>;/** * Gets an entity by name. * ```js * cy.getEntityByName(newHero.name).then(myHero => ...) * ``` * @param name: Hero['name'] */getEntityByProperty( property:Hero["name"] |Hero["description"] |Hero["id"] ):Cypress.Chainable<Hero>;/** * Given a hero property (name, description or id), * returns the index of the hero in the collection */findHeroIndex( property:Hero["name"] |Hero["description"] |Hero["id"] ):Cypress.Chainable<number>;/** * Performs crud operations GET, POST, PUT and DELETE. * * `body` and `allowedToFail are optional. * * If they are not passed in, body is empty but `allowedToFail` still is `false`. * * If the body is passed in and the method is `POST` or `PUT`, the payload will be taken, * otherwise undefined for `GET` and `DELETE`. * @param method * @param route * @param options: {body?: Hero | object; allowedToFail?: boolean} */crud( method:"GET"|"POST"|"PUT"|"DELETE", route:string, { body, allowedToFail =false, }: { body?:Hero|object; allowedToFail?:boolean } = {} ):Cypress.Chainable<Response<Hero[] &Hero>>;/** * Resets the data in the database to the initial data. */resetData():Cypress.Chainable<Response<Hero[] &Hero>>; } }}
// cypress/e2e/edit-hero.cy.tsimport { faker } from"@faker-js/faker";import { Hero } from"../../src/models/Hero";describe("Edit hero", () => {before(cy.resetData);/** Verifies hero info on Edit page */constverifyHero= (heroes:Hero[], heroIndex:number) => {cy.location("pathname").should("include","/heroes/edit-hero/");cy.getByCy("hero-detail").should("be.visible");cy.getByCy("input-detail-id").should("be.visible");cy.findByDisplayValue(heroes[heroIndex].id);cy.findByDisplayValue(heroes[heroIndex].name);cy.findByDisplayValue(heroes[heroIndex].description); };constrandomHeroIndex= (heroes:Hero[]) =>Cypress._.random(0,heroes.length-1);it("should go through the cancel flow for a random hero (ui-integration)", () => {cy.visitStubbedHeroes();cy.fixture("heroes").then((heroes) => {constheroIndex=randomHeroIndex(heroes);cy.getByCy("edit-button").eq(heroIndex).click();verifyHero(heroes, heroIndex); });cy.getByCy("cancel-button").click();cy.location("pathname").should("eq","/heroes");cy.getByCy("hero-list").should("be.visible"); });it("should navigate to add from an existing hero (ui-integration)", () => {cy.visitStubbedHeroes();cy.fixture("heroes").then((heroes) => {constheroIndex=randomHeroIndex(heroes);cy.getByCy("edit-button").eq(heroIndex).click();verifyHero(heroes, heroIndex);cy.getByCy("add-button").click();cy.getByCy("input-detail-id").should("not.exist");cy.findByDisplayValue(heroes[heroIndex].name).should("not.exist");cy.findByDisplayValue(heroes[heroIndex].description).should("not.exist"); }); });it("should go through the edit flow (ui-e2e)", () => {constnewHero:Hero= { id:faker.datatype.uuid(), name:faker.internet.userName(), description:`description ${faker.internet.userName()}`, };cy.crud("POST","heroes", { body: newHero });cy.visit(`heroes/edit-hero/${newHero.id}`, { qs: { name:newHero.name, description:newHero.description }, });consteditedHero= { name:faker.internet.userName(), description:`description ${faker.internet.userName()}`, };cy.getByCy("input-detail-name").find(".input").clear().type(`${editedHero.name}`);cy.getByCy("input-detail-description").find(".input").clear().type(`${editedHero.description}`);cy.getByCy("save-button").click();cy.getByCy("hero-list").should("be.visible").should("contain",editedHero.name).and("contain",editedHero.description);cy.getEntityByProperty(newHero.id).then((myHero) =>cy.crud("DELETE",`heroes/${myHero.id}`) ); });});
useDeleteHero
Son kancamızın yaratılmasına her zamanki gibi bir test ile başlıyoruz. Api'yi kullanarak yeni bir kahraman yaratmak, kahramanın indeksini bilmek ve ardından onu silmeye çalışmak istiyoruz. Kahramanın indeksini bulmak için cy.findHeroIndex komutumuz var, bu komut tüm kahramanları alır ve aradığımız kahramanın indeksini bulur. Onun yerine bir nesne döndüren ve kahramanın indeksini ve kahramanlar dizisini döndüren bir komut kullanabiliriz. findHeroIndex komutunu ve tip tanımını şu şekilde değiştirin:
// cypress.d.ts/** * Given a hero property (name, description or id), * returns the index of the hero, and the entire collection, as an object.*/findHeroIndex( property: Hero['name'] | Hero['description'] | Hero['id'], ): Cypress.Chainable<{heroIndex: number; heroesArray: Hero[]}>
İşte tüm testimiz. Kahramanı silmeyi iptal etme akışını bir ui-entegrasyon testi olarak kapsıyoruz. Kahramanı siliyoruz ve listede bulamayı beklemiyoruz (Kırmızı 4).
// cypress/e2e/delete-hero.cy.tsimport { faker } from"@faker-js/faker";import { Hero } from"../../src/models/Hero";describe("Delete hero", () => {before(cy.resetData);it("should go through the cancel flow (ui-integration)", () => {cy.visitStubbedHeroes();cy.getByCy("delete-button").first().click();cy.getByCy("modal-yes-no").within(() =>cy.getByCy("button-no").click());cy.getByCy("heroes").should("be.visible");cy.get("modal-yes-no").should("not.exist"); });it("should go through the edit flow (ui-e2e)", () => {consthero:Hero= { id:faker.datatype.uuid(), name:faker.internet.userName(), description:`description ${faker.internet.userName()}`, };cy.crud("POST","heroes", { body: hero });cy.visitHeroes();cy.findHeroIndex(hero.id).then(({ heroIndex, heroesArray }) => {cy.getByCy("delete-button").eq(heroIndex).click();cy.getByCy("modal-yes-no").within(() =>cy.getByCy("button-yes").click());cy.getByCy("hero-list").should("be.visible").should("not.contain", heroesArray[heroIndex].name).and("not.contain", heroesArray[heroIndex].description); }); });});
Test başarısız olduğunda, Heroes bileşeninde bulunan handleDeleteFromModal adlı bir console.log görüyoruz. Kancamız için useMutation'ı tekrar kullanacağız. Api'den deleteItem kullanıyoruz. onSuccess şimdiye kadar kullanılan ilk argümanı kullanacak (oluşturulan / düzenlenen öğe), ikinci argüman olan orijinal (silinen) öğeyi kullanacaktır. Önbellek tekrar yönetilmelidir; önbellekteki tüm kahramanları almalı ve silinen kahraman olmadan önbelleği ayarlamalıyız. POST ve PUT işlemleri için kullanılan aynı önbellek anahtarını kullanmalıyız; ['heroes']. usePutHero kanca için benzer bir dönüş değeri uygulayacağız.
// src/hooks/useDeleteHero.tsimport { Hero } from"models/Hero";import { useMutation, useQueryClient } from"react-query";import { useNavigate } from"react-router-dom";import { deleteItem } from"./api";/** * Helper for DELETE to `/heroes` route * @returns{object} {deleteHero, isDeleting, isDeleteError, deleteError} */exportfunctionuseDeleteHero() {constnavigate=useNavigate();constqueryClient=useQueryClient();constmutation=useMutation( (item:Hero) =>deleteItem(`heroes/${item.id}`), {// on success receives the original item as a second argument// if you recall, the first argument is the created itemonSuccess: (_, deletedHero:Hero) => {// get all the heroes from the cacheconstheroes:Hero[] =queryClient.getQueryData(["heroes"]) || [];// set the heroes cache without the delete onequeryClient.setQueryData( ["heroes"],heroes.filter((h) =>h.id !==deletedHero.id) );navigate("/heroes"); }, } );return { deleteHero:mutation.mutate, isDeleting:mutation.isLoading, isDeleteError:mutation.isError, deleteError:mutation.error, };}
Heroes bileşenimiz şu şekilde kullanmaya başlayabilir:
Bileşen sadece hangi kahramanı silmesi gerektiğini bilmekle kalmaz, tüm listeyi görüntülerken, aynı zamanda bu bilgiyi HeroList bileşenine bir özellik olan handleDeleteHero ile iletmelidir. Kahramanı useState aracılığıyla tanımlayabiliriz; başlangıçta kahramanı bilmiyoruz, HeroListe bildirdiğimizde kahramanı tanımlayabiliriz.
İlerlemeye devam etmeden önce, HeroListe küçük bir değişiklik yapmamız gerekiyor. handleDeleteHero özelliği, bir kahramanı argüman olarak almalıdır. Ayrıca, handleDeleteHero için tıklama işleyicisi, handleSelectHeroya benzer şekilde, handleDeleteHeroyu döndüren bir işleve dönüşmelidir.
Şimdi Heroes bileşenine anahtar değişiklikleri uygulayabiliriz (Yeşil 4).
// we identify the hero to delete as stateconst [heroToDelete,setHeroToDelete] =useState<Hero|null>(null);// we use the new hookconst { deleteHero,isDeleteError } =useDeleteHero();// when closing the modal, we should negat the heroToDelete stateconsthandleCloseModal= () => {setHeroToDelete(null); // wesetShowModal(false);};// we let the HeroList know about which hero to delete// and HeroList decides the hero in its click handler// onClick={() => handleDeleteHero(hero)}consthandleDeleteHero= (hero:Hero) => {setHeroToDelete(hero);setShowModal(true);};// on clicking Yes in the modal, we invoke the hookconsthandleDeleteFromModal= () => { heroToDelete ?deleteHero(heroToDelete) :null;setShowModal(false);};
Bunu başarmak için, handleSelectHero işlevini köri yapmamız gerekiyor. Bölüm 14'e benzer şekilde, dış işlev özel argümanımızı alır ve olayı alan bir işlevi döndürür. İletişim kurmak için özellik türünü hizalamamız gerekiyor.
Şimdi react-query'nin QueryClientProvider kullanıldığına göre, Heroes ve HeroDetail bileşenleri de QueryClientProvider içinde sarılmalıdır. Bileşen testlerini çalıştırdığımızda şu hatayı alıyoruz: ``(uncaught exception)**Error: No QueryClient set, use QueryClientProvider to set one..
HeroDetail bileşen testini aşağıdaki gibi güncelliyoruz. should handle Save testini kapsadığımızda, artık gönderilen iptal edilmiş bir POST isteği görüyoruz ve bunu doğrulayabilir ve yapılacaklar listesini tamamlayabiliriz. Bunun dışında, sadece bileşen yüklemelerini QueryClientProvider içinde sarmalıyoruz.
Heroes bileşen testinde bileşeni QueryClientProvider içinde sarmalıyoruz. Ayrıca console.log üzerinde casusluk yapmayı durdurabilir ve bunun yerine kahramanı silme modül akışı sırasında gönderilen DELETE isteğini öğelerine ayırabiliriz.
// src/heroes/Heroes.cy.tsximport Heroes from"./Heroes";import { BrowserRouter } from"react-router-dom";import { QueryClient, QueryClientProvider } from"react-query";import"../styles.scss";describe("Heroes", () => {let queryClient:QueryClient;beforeEach(() => {cy.intercept("GET",`${Cypress.env("API_URL")}/heroes`, { fixture:"heroes.json", }).as("getHeroes"); queryClient =newQueryClient();cy.mount( <QueryClientProviderclient={queryClient}> <BrowserRouter> <Heroes /> </BrowserRouter> </QueryClientProvider> ); });it("should display the hero list on render, and go through hero add & refresh flow", () => {cy.wait("@getHeroes");cy.getByCy("list-header").should("be.visible");cy.getByCy("hero-list").should("be.visible");cy.getByCy("add-button").click();cy.location("pathname").should("eq","/heroes/add-hero");cy.getByCy("refresh-button").click();cy.location("pathname").should("eq","/heroes"); });constinvokeHeroDelete= () => {cy.getByCy("delete-button").first().click();cy.getByCy("modal-yes-no").should("be.visible"); };it("should go through the modal flow", () => {cy.getByCy("modal-yes-no").should("not.exist");cy.log("do not delete flow");invokeHeroDelete();cy.getByCy("button-no").click();cy.getByCy("modal-yes-no").should("not.exist");cy.log("delete flow");invokeHeroDelete();cy.intercept("DELETE","*", { statusCode:200 }).as("deleteHero");cy.getByCy("button-yes").click();cy.wait("@deleteHero");cy.getByCy("modal-yes-no").should("not.exist"); });});
Summary
useAxios hook'ünü useGetHeroes ile değiştirdik ve önbellekleme sayesinde performans artışı gördük.
UI üzerinden bir kahraman ekleyip yönlendirme işlemini kontrol eden bir e2e testi yazdık (Red 1).
usePostHero hook'unu yazdık ve HeroDetail bileşeninde kullandık (Green 1).
Testi geliştirerek, yeni kahramanın listeye eklenip eklenmediğini kontrol ettik (Red 2).
usePostHero hook'unda, kahramanın eklenmesiyle birlikte listeye görünmesini sağlamak için queryClient'ın setQueryData özelliğini kullandık (Green 2).
Backend verilerine ihtiyaç duymayan testlerimizi ui-integrasyon testlerine dönüştürdük (Düzenleme 2).
baseUrl yüklendiğinde doğal veya taklit veriler kullanabilmek için komutlar oluşturduk. Ayrıca, testlerin stateless olmasını sağladık; her testten önce db'yi sıfırlayarak ve oluşturduğumuz api komutlarını kullanarak kendini temizleyerek.
Bir kahramana gitmek, düzenlemek, yönlendirmeyi ve güncellenmiş verileri kontrol eden bir e2e testi yazdık (Red 3).
usePutHero hook'unu, güncelleme için önbellekleme desteğiyle birlikte yazdık ve HeroDetail bileşeninde kullandık (Green 3).
Backend verilerine ihtiyaç duymayan testleri tekrar ui-integrasyon testlerine dönüştürdük (Düzenleme 3).
Bir kahramanı silmek için bir test yazdık (Red 4).
useDeleteHero hook'umuzu yazdık ve Heroes bileşeninde kullandık (Green 4).
Event handler'ları currying ile yeniden düzenledik ve bileşen testlerini güncelledik (Düzenleme 4).
Çıkarılacak Dersler
useQuery, durumu getirir: UI state <- sunucu/url , ve önbelleğe alır.
useMutation, tam tersi olarak çalışır: UI state -> sunucu , ve yine önbelleğe alır.
Backend'i değiştirdiğimizde, yeni önbelleği de güncellememiz gerekiyor.
react-query kullandığımızda, bazı component test mount'larımız QueryClientProvider ile sarmalanması gerekebilir.
Uygulamanızın işlevselliğinde güven kazanmak için backend'e ihtiyacınız olup olmadığını her zaman değerlendirin. Güveninizi sağlamak için gerçek e2e testler yalnızca gerektiğinde kullanmalı ve aynı maliyetli e2e testleri her yerde tekrarlamamalısınız. Bunun yerine, ui-integration testlerini kullanın. Backend, kendi e2e testleriyle test ediliyorsa, UI e2e ihtiyacınız daha azdır; backend çabasını fazla tekrarlamamaya dikkat edin.
Sırası önemli olmayan, durumsuz testler, yan etkiler olmadan izole bir şekilde geçebilmesi gereken temel unsurlar olmalıdır. Bu, özellikle idempotent olmayan istekler yapan e2e testlerinde önemlidir. Bu nedenle, önceki turlarda oluşturduğumuz API komutlarını kullanarak her testten önce veritabanını sıfırlıyor ve kendi kendimize temizliyoruz.
Testler yazarken, herhangi bir yerde kapsama sağlayan testleri tekrarlamadığınızı düşünün. Maliyet herhangi bir güven sağlamıyorsa, farklı işlevsellikleri kapsayacak fırsatlar bulun. Kapsam sağlayan başka bir test tarafından sağlanabilen kapsama tekrarlamasını kaldırdık. Tıklama gezinimini tekrarlamamak için doğrudan yönlendirmeyi kullandık.
E2e testinde amaçları / eylemleri iletişim kurmak ve ayırmak için boşluk kullanın.