ch15-api-json-server
Şimdiye kadar Heroes
bileşeninde src/heroes/heroes.json
adlı bir json dosyası kullanıyorduk. Uygulamamız bir arka uçla iletişim kurmuyor. Bunun yerine sahte bir REST API'ye sahip olmak ideal olacaktır ve json-server
bunu sağlayabilir. Uygulamamıza aşağıdaki paketleri ekleyin:
yarn add -D concurrently json-server
Proje kökünde bir db.json
dosyası oluşturun ve aşağıdaki içeriği içine kopyalayın.
{
"heroes": [
{
"id": "HeroAslaug",
"name": "Aslaug",
"description": "warrior queen"
},
{
"id": "HeroBjorn",
"name": "Bjorn Ironside",
"description": "king of 9th century Sweden"
},
{
"id": "HeroIvar",
"name": "Ivar the Boneless",
"description": "commander of the Great Heathen Army"
},
{
"id": "HeroLagertha",
"name": "Lagertha the Shieldmaiden",
"description": "aka Hlaðgerðr"
},
{
"id": "HeroRagnar",
"name": "Ragnar Lothbrok",
"description": "aka Ragnar Sigurdsson"
},
{
"id": "HeroThora",
"name": "Thora Town-hart",
"description": "daughter of Earl Herrauðr of Götaland"
}
],
"villains": [
{
"id": "VillainMadelyn",
"name": "Madelyn",
"description": "the cat whisperer"
},
{
"id": "VillainHaley",
"name": "Haley",
"description": "pen wielder"
},
{
"id": "VillainElla",
"name": "Ella",
"description": "fashionista"
},
{
"id": "VillainLandon",
"name": "Landon",
"description": "Mandalorian mauler"
}
]
}
package.json
yakınında bulunan "start"
script'ine bir script ekleyin. Bu, db.json
dosyasını kullanarak localhost:4000
adresinde içeriği döndürecek ve 1 saniyelik simüle edilmiş ağ gecikmesi sağlayacaktır.
{
"scripts": {
"start:api": "json-server --watch db.json --port 4000 --delay 1000"
}
}
yarn start:api
komutunu çalıştırın ve http://localhost:4000/heroes veya http://localhost:4000/villains adreslerine gidin. Bazı veriler görmelisiniz.

Aşağıdaki gibi package.json
script'lerini güncelleyin. Değişiklikler, UI sunucusunun her zaman API sunucusuyla birlikte sunulmasını sağlar. Bu sayede repo kullanıcısı, repoyu kullanırken arka uç ihtiyaçlarını soyutlamış olur.
"scripts": {
"start": "react-scripts start",
"start:api": "json-server --watch db.json --port 4000 --delay 1000",
"dev": "concurrently -k yarn:start yarn:start:api",
"build": "react-scripts build",
"test": "react-scripts test",
"test:coverage": "yarn test --watchAll=false --coverage",
"test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --watch --runInBand",
"format": "prettier --ignore-path .gitignore --write \"**/*.+(js|json|css|md|mdx|html)\"",
"lint": "eslint . --cache-location node_modules/.cache/eslint",
"typecheck": "tsc --noEmit",
"validate": "npm-run-all --parallel format lint typecheck build",
"cy:open": "cypress open --e2e --browser chrome",
"cy:open-e2e": "concurrently -k yarn:start:api 'server-test 3000 cy:open'",
"cy:run-e2e": "concurrently -k yarn:start:api 'server-test 3000 cy:run'",
"cy:open-ct": "cypress open --component --browser chrome",
"cy:run-ct": "cypress run --component --browser chrome"
},
CI için, .github/workflows/main.yml
dosyasındaki cypress-e2e-test
bölümünü > Cypress GitHub eylem komutunu > start
özelliğini yarn start
'tan yarn dev
'e güncelleyin. Bu, başlatma komutunun sadece UI sunucusunu başlatmakla kalmayıp, aynı zamanda arka ucu da başlatacağından emin olacaktır.
cypress-e2e-test:
needs: [install-dependencies]
runs-on: ubuntu-latest
container: cypress/included:10.7.0 # or whatever is the latest
steps:
- uses: actions/checkout@v3
- uses: bahmutov/npm-install@v1.8.21
with: { useRollingCache: true }
- name: Cypress e2e tests 🧪
uses: cypress-io/github-action@v4.2.0
with:
install: false
# update from yarn start to yarn dev
start: yarn dev
wait-on: "http://localhost:3000"
browser: chrome
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Arka Uç-e2e
Cypress, canlı bir arka uç testi için harika bir API istemcisi aracıdır; Cypress ile Dağıtılmış Hizmette CRUD API Testi yazısına göz atın. Arka uç düzgün çalıştığından emin olmak için arka uç e2e testleri yazabiliriz. json-server
ile bu gerekli değildir, ancak gerçek yaşam örneğini göstermek istiyoruz ve ayrıca bu API komutlarının bazılarını UI e2e CRUD testlerinde durumu ayarlamak ve temizlemek için kullanacağız. Gerçek dünyada, büyük olasılıkla arka uç ayrı bir depoda olacak ve kendi Cypress api e2e testlerine sahip olacaktır. Ön uçta da kullanılacak olan ortak komutlar büyük olasılıkla dahili bir Cypress test paketinde barındırılacaktır; bu konuda bir rehberi burada bulabilirsiniz.
Cypress API testi ile TDD'yi göstermek için bir arka uç-e2e testi oluşturuyoruz. cypress/e2e/backend/crud.cy.ts
adında bir dosya oluşturun ve basit bir GET
isteği yapın.
// cypress/e2e/backend/crud.cy.ts
describe("Backend e2e", () => {
it("should ", () => {
const url = "http://localhost:4000/heroes";
cy.request({
method: "GET",
url,
})
.its("body")
.should("have.length.gt", 0);
});
});
Arka uç e2e testlerini başlatmak için yarn cy:open-e2e
komutunu kullanın ve testi çalıştırın. Konsolu incelediğimizde, body
içinde 6 kahramanın dizisini görebiliriz.

Her varlık için id
, name
ve description
özelliklerine sahip olduğundan emin olmak için daha derinlemesine test edebiliriz.
// cypress/e2e/backend/crud.cy.ts
describe("Backend e2e", () => {
it("should ", () => {
const url = "http://localhost:4000/heroes";
type Hero = { id: string; name: string; description: string };
cy.request({
method: "GET",
url,
})
.its("body")
.should("have.length.gt", 0)
.each((entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
});
});
});
Arka ucun /api
rotasıyla sunulması daha ideal olurdu, böylece ön uçla karıştırılmaz ve daha uyumlu hale getirilir. Bu değişiklik için başarısız bir testle URL'yi değiştirin (Kırmızı 1).
// cypress/e2e/backend/crud.cy.ts
describe("Backend e2e", () => {
it("should ", () => {
const url = "http://localhost:4000/api/heroes";
type Hero = { id: string; name: string; description: string };
cy.request({
method: "GET",
url,
})
.its("body")
.should("have.length.gt", 0)
.each((entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
});
});
});
Değişikliği gerçekleştirmek için 2 değişiklik yapmamız gerekiyor. Önce, projenin kökünde routes.json
adlı bir dosya oluşturun:
{
"/api/*": "/$1"
}
Bu dosyanın kullanılması için package.json
betiğini --routes routes.json
ekleyerek değiştirin.
"start:api": "json-server --watch db.json --port 4000 --delay 1000 --routes routes.json",

Benzer bir testi, villains
için de hafif bir düzenlemeyle yapabiliriz (Düzenleme 1).
// cypress/e2e/backend/crud.cy.ts
describe("Backend e2e", () => {
it("should GET heroes and villains ", () => {
const apiUrl = "http://localhost:4000/api";
type Hero = { id: string; name: string; description: string };
cy.request({
method: "GET",
url: `${apiUrl}/heroes`,
})
.its("body")
.should("have.length.gt", 0)
.each((entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
});
cy.request({
method: "GET",
url: `${apiUrl}/villains`,
})
.its("body")
.should("have.length.gt", 0)
.each((entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
});
});
});
Daha kuru olacak şekilde bunu daha da yeniden düzenleyebiliriz.
// cypress/e2e/backend/crud.cy.ts
describe("Backend e2e", () => {
const apiUrl = "http://localhost:4000/api";
type Hero = { id: string; name: string; description: string };
const assertProperties = (entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
};
const getRoute = (route: string) =>
cy.request({
method: "GET",
url: `${apiUrl}/${route}`,
});
it("should GET heroes and villains ", () => {
getRoute("heroes")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
getRoute("villains")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
});
});
Yeni bir test yazalım, bu sefer bir kahraman ekleyip eklenip eklenmediğini doğrulayalım.
// cypress/e2e/backend/crud.cy.ts
describe("Backend e2e", () => {
const apiUrl = "http://localhost:4000/api";
type Hero = { id: string; name: string; description: string };
const assertProperties = (entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
};
const getRoute = (route: string) =>
cy.request({
method: "GET",
url: `${apiUrl}/${route}`,
});
it("should GET heroes and villains ", () => {
getRoute("heroes")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
getRoute("villains")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
});
const postRoute = (route: string, body: Hero) =>
cy.request({
method: "POST",
url: `${apiUrl}/${route}`,
body,
});
it("should create a new hero entity", () => {
const newHero = { id: "Ragnarok", name: "Ragnar", description: "Lothbrok" };
postRoute("heroes", newHero);
getRoute("heroes")
.its("body")
.then((body) => {
expect(body.at(-1)).to.deep.eq(newHero);
});
});
});
Test başlangıçta çalışıyor gibi görünüyor, ancak yeniden çalıştırıldığında 500 hata alıyoruz, çünkü oluşturduğumuz varlık zaten mevcut (Kırmızı 2).

Bunu ele almanın birkaç yolu vardır. Varlığı rastgeleleştirebilir ve/veya testin sonunda varlığı silebiliriz. En iyi uygulamaları göstermek için bunları yapacağız, ancak bununla başa çıkmak için kesin yol, testin başında sunucu durumunu sıfırlamaktır. İlerleyen zamanlarda testin kendi kendini temizlemesini sağlayabiliriz.
json-server-reset uygulamasını yarn add -D json-server-reset
ile yükleyin. package.json
dosyasını düzenleyerek, api için bir /reset
rotası sağlayan middleware'i ekleyin. Herhangi bir yük ile reset
rotasına POST
isteği yapıldığında, db.json
dosyası o yüke sıfırlanır.
"start:api": "json-server --watch db.json --port 4000 --delay 1000 --routes routes.json --middlewares ./node_modules/json-server-reset"
İlk testte eklenen ek varlığı db.json dosyasından kaldırın ve db'yi orijinal durumuna sıfırlayın. Kaldırın:
{
"id": "Ragnarok",
"name": "Ragnar",
"description": "Lothbrok"
}
Şimdi ihtiyacımız olan tek şey bir yüktür, ancak db.json
dosyasını testimize dahil edip aynı zamanda kendisini sıfırlamak için kullanırsak, sıfırlama çağrısı sonsuz bir şekilde tekrarlanacaktır. db.json
dosyasının bir kopyasını cypress/fixtures/db.json
şeklinde oluşturun, böylece oradan içe aktarabilir ve ağımızı tamamen taklit edebiliriz. Aşağıda gösterildiği gibi, verileri her testten önce sıfırlamak için komut dosyamızı değiştirebiliriz (Yeşil 2).
// cypress/e2e/backend/crud.cy.ts
import data from "../../fixtures/db.json";
describe("Backend e2e", () => {
const apiUrl = "http://localhost:4000/api";
type Hero = { id: string; name: string; description: string };
const assertProperties = (entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
};
const getRoute = (route: string) =>
cy.request({
method: "GET",
url: `${apiUrl}/${route}`,
});
const postRoute = (route: string | "reset", body: Hero | object) =>
cy.request({
method: "POST",
url: `${apiUrl}/${route}`,
body,
});
const resetData = () => postRoute("reset", data);
beforeEach(resetData);
it("should GET heroes and villains ", () => {
getRoute("heroes")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
getRoute("villains")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
});
it("should create a new hero entity", () => {
const newHero = { id: "Ragnarok", name: "Ragnar", description: "Lothbrok" };
postRoute("heroes", newHero);
getRoute("heroes")
.its("body")
.then((body) => {
expect(body.at(-1)).to.deep.eq(newHero);
});
});
});
Şimdi kahramanı güncellemek ve silmek için testin geri kalanını ekleyebiliriz. Bir rotada id
özelliğini kullanarak doğrudan o varlığı almak, güncellemek veya silmek için kullanabileceğimize dikkat edin. Başarılı bir testimiz olduğu için, başarısız olana kadar yeni testler eklemeye devam ediyoruz. json-server
ı test ediyoruz, bu yüzden başarısızlıklar olası değildir. Bu, geliştirme tamamlandıktan sonra TDD benzeri bir yaklaşım uygulandığında yaygın bir senaryodur, bu nedenle TDD'nin gerçek değeri geliştirme sırasında anlaşılır. Testi güncelleme ve silme ile geliştirelim (Düzenleme 2).
// cypress/e2e/backend/crud.cy.ts
import data from "../../fixtures/db.json";
describe("Backend e2e", () => {
const apiUrl = "http://localhost:4000/api";
type Hero = { id: string; name: string; description: string };
const assertProperties = (entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
};
const getRoute = (route: string) =>
cy.request({
method: "GET",
url: `${apiUrl}/${route}`,
});
const postRoute = (route: string, body: Hero | object) =>
cy.request({
method: "POST",
url: `${apiUrl}/${route}`,
body,
});
const editRoute = (route: string, body: Hero | object) =>
cy.request({
method: "PUT",
url: `${apiUrl}/${route}`,
body,
});
const deleteRoute = (route: string) =>
cy.request({
method: "DELETE",
url: `${apiUrl}/${route}`,
});
const resetData = () => postRoute("reset", data);
beforeEach(resetData);
it("should GET heroes and villains ", () => {
getRoute("heroes")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
getRoute("villains")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
});
it("should create a new hero entity", () => {
const newHero = { id: "Ragnarok", name: "Ragnar", description: "Lothbrok" };
postRoute("heroes", newHero);
getRoute("heroes")
.its("body")
.then((body) => {
expect(body.at(-1)).to.deep.eq(newHero);
});
const editedHero = { ...newHero, name: "Murat" };
editRoute(`heroes/${editedHero.id}`, editedHero);
getRoute(`heroes/${editedHero.id}`)
.its("body")
.should("deep.eq", editedHero);
deleteRoute(`heroes/${editedHero.id}`);
});
});
Testin, veritabanı durumundan sorumlu olması en iyi uygulamadır. Burada tek bir blok altında oluşturma, güncelleme ve silme işlemlerini yapıyoruz, ancak başka bir yaklaşım aşağıdaki gibi olabilir. Çoğaltılan alt adımlar kalın ile vurgulanmıştır.
Oluşturma
UI oluştur
API sil
Güncelleme
API oluştur
UI güncelle
API sil
Silme
API oluştur
UI sil
Gördüğünüz gibi, güncelleme senaryosunu kapsamak her şeyi tatmin eder, aksi takdirde testler arasında çoğaltma vardır.
Son silme işleminden sonra, varlığın veritabanından kaldırıldığından emin olmalıyız. Bunu kontrol etmek için son bir get işlemi ekleyin (Kırmızı 3).
// cypress/e2e/backend/crud.cy.ts
import data from "../../fixtures/db.json";
describe("Backend e2e", () => {
const apiUrl = "http://localhost:4000/api";
type Hero = { id: string; name: string; description: string };
const assertProperties = (entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
};
const getRoute = (route: string) =>
cy.request({
method: "GET",
url: `${apiUrl}/${route}`,
});
const postRoute = (route: string, body: Hero | object) =>
cy.request({
method: "POST",
url: `${apiUrl}/${route}`,
body,
});
const editRoute = (route: string, body: Hero | object) =>
cy.request({
method: "PUT",
url: `${apiUrl}/${route}`,
body,
});
const deleteRoute = (route: string) =>
cy.request({
method: "DELETE",
url: `${apiUrl}/${route}`,
});
const resetData = () => postRoute("reset", data);
beforeEach(resetData);
it("should GET heroes and villains ", () => {
getRoute("heroes")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
getRoute("villains")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
});
it("should CRUD a new hero entity", () => {
const newHero = { id: "Ragnarok", name: "Ragnar", description: "Lothbrok" };
postRoute("heroes", newHero);
getRoute("heroes")
.its("body")
.then((body) => {
expect(body.at(-1)).to.deep.eq(newHero);
});
const editedHero = { ...newHero, name: "Murat" };
editRoute(`heroes/${editedHero.id}`, editedHero);
getRoute(`heroes/${editedHero.id}`)
.its("body")
.should("deep.eq", editedHero);
deleteRoute(`heroes/${editedHero.id}`);
getRoute(`heroes/${editedHero.id}`).its("status").should("eq", 404);
});
});
Cypress, başarılı olana kadar komutları yeniden dener (4 saniye) olduğu için 404 durumu hakkında bir hata alacağız. 200 dışındaki kodlar için kontrolümüz varsa, api çağrılarının başarısız olmasına izin vermelidir.
cy.request kontrol etmeyi sevdiğimiz iki özelliğe sahiptir:
retryOnStatusCodeFailure
: Cypress'in, hata kodlarını otomatik olarak yeniden denemesi gerekip gerekmediği. Eğer bu true olarak ayarlanırsa, Cypress bir isteği en fazla 4 kez yeniden dener.failOnStatusCode
:2xx
ve3xx
dışındaki yanıt kodlarına göre başarısız olup olmamak
Bu ikisini, varsayılan değeri false
olan allowedToFail
adlı bir argüman bayrağı ile birlikte kontrol edebiliriz. 200 dışındaki durum kodlarını beklediğimizde, bunu true olarak ayarlayabiliriz (Yeşil 3).
// cypress/e2e/backend/crud.cy.ts
import data from "../../fixtures/db.json";
describe("Backend e2e", () => {
const apiUrl = "http://localhost:4000/api";
type Hero = { id: string; name: string; description: string };
const assertProperties = (entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
};
const getRoute = (route: string, allowedToFail = false) =>
cy.request({
method: "GET",
url: `${apiUrl}/${route}`,
retryOnStatusCodeFailure: !allowedToFail,
failOnStatusCode: !allowedToFail,
});
const postRoute = (route: string, body: Hero | object) =>
cy.request({
method: "POST",
url: `${apiUrl}/${route}`,
body,
});
const editRoute = (route: string, body: Hero | object) =>
cy.request({
method: "PUT",
url: `${apiUrl}/${route}`,
body,
});
const deleteRoute = (route: string) =>
cy.request({
method: "DELETE",
url: `${apiUrl}/${route}`,
});
const resetData = () => postRoute("reset", data);
beforeEach(resetData);
it("should GET heroes and villains ", () => {
getRoute("heroes")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
getRoute("villains")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
});
it("should CRUD a new hero entity", () => {
const newHero = { id: "Ragnarok", name: "Ragnar", description: "Lothbrok" };
postRoute("heroes", newHero);
getRoute("heroes")
.its("body")
.then((body) => {
expect(body.at(-1)).to.deep.eq(newHero);
});
const editedHero = { ...newHero, name: "Murat" };
editRoute(`heroes/${editedHero.id}`, editedHero);
getRoute(`heroes/${editedHero.id}`)
.its("body")
.should("deep.eq", editedHero);
deleteRoute(`heroes/${editedHero.id}`);
getRoute(`heroes/${editedHero.id}`, true).its("status").should("eq", 404);
});
});
API'yi daha okunabilir ve genişletilebilir hale getirmek için seçenekleri bir nesneye koyarak yeniden düzenleyebiliriz. Bu şekilde, birden fazla seçenek varsa, sıraları önemli değildir. Hiçbir seçenek iletilmezse, varsayılan olarak boş bir nesneye dönüşür. Bu yeniden düzenleme, gelecekte yeni seçenekler eklememize ve API kullanıcısının hangilerini kullanmak istediğini seçmesine olanak tanır.
Ayrıca, cy.request
yanıtını dökerek türü iyileştirebiliriz ve alacağımız değerin türünü belirtiriz.
const getRoute = (
// required args
route: string,
// options, some can have default values
// {optional args}: {their types} = {}
{ allowedToFail = false }: { allowedToFail?: boolean } = {}
) =>
cy.request<Hero[] & Hero>({
method: "GET",
url: `${apiUrl}/${route}`,
retryOnStatusCodeFailure: !allowedToFail,
failOnStatusCode: !allowedToFail,
});
İşte yeniden yapılandırılmış test (Düzenleme 3).
// cypress/e2e/backend/crud.cy.ts
import data from "../../fixtures/db.json";
describe("Backend e2e", () => {
const apiUrl = "http://localhost:4000/api";
type Hero = { id: string; name: string; description: string };
const assertProperties = (entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
};
const getRoute = (
route: string,
{ allowedToFail = false }: { allowedToFail?: boolean } = {}
) =>
cy.request<Hero[] & Hero>({
method: "GET",
url: `${apiUrl}/${route}`,
retryOnStatusCodeFailure: !allowedToFail,
failOnStatusCode: !allowedToFail,
});
const postRoute = (route: string, body: Hero | object) =>
cy.request<Hero>({
method: "POST",
url: `${apiUrl}/${route}`,
body,
});
const editRoute = (route: string, body: Hero | object) =>
cy.request<Hero>({
method: "PUT",
url: `${apiUrl}/${route}`,
body,
});
const deleteRoute = (route: string) =>
cy.request<Hero>({
method: "DELETE",
url: `${apiUrl}/${route}`,
});
const resetData = () => postRoute("reset", data);
beforeEach(resetData);
it("should GET heroes and villains ", () => {
getRoute("heroes")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
getRoute("villains")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
});
it("should CRUD a new hero entity", () => {
const newHero = { id: "Ragnarok", name: "Ragnar", description: "Lothbrok" };
postRoute("heroes", newHero);
getRoute("heroes")
.its("body")
.then((body) => {
expect(body.at(-1)).to.deep.eq(newHero);
});
const editedHero = { ...newHero, name: "Murat" };
editRoute(`heroes/${editedHero.id}`, editedHero);
getRoute(`heroes/${editedHero.id}`)
.its("body")
.should("deep.eq", editedHero);
deleteRoute(`heroes/${editedHero.id}`);
getRoute(`heroes/${editedHero.id}`, { allowedToFail: true })
.its("status")
.should("eq", 404);
});
});
Daha iyi okunabilir bir API olmasına rağmen, CRUD işlevleri arasında biraz yineleme vardır. Aşağıdaki işlevi düşünün; tip güvenliğiyle herhangi bir CRUD işlemini gerçekleştirebilir. method
ve route
gerekli olan parametrelerdir. body
ve allowedToFail
bayrağı isteğe bağlıdır; eğer geçirilmezlerse, body
boş olacak ama allowedToFail
yine de false
olacaktır. Eğer body
geçirilir ve method
POST
veya PUT
ise, yük alınacaktır; aksi takdirde, GET
ve DELETE
için tanımsızdır.
const crud = (
// required args
method: "GET" | "POST" | "PUT" | "DELETE",
route: string,
// optional args
// {optional args}: {their types} = {}
{
body,
allowedToFail = false,
}: { body?: Hero | object; allowedToFail?: boolean } = {}
) =>
cy.request<Hero[] & Hero>({
method: method,
url: `${apiUrl}/${route}`,
body: method === "POST" || method === "PUT" ? body : undefined,
retryOnStatusCodeFailure: !allowedToFail,
failOnStatusCode: !allowedToFail,
});
İşte yukarıdaki fonksiyonu kullanarak yapılan yeniden düzenleme (Düzenleme 4):
import data from "../../fixtures/db.json";
describe("Backend e2e", () => {
const apiUrl = "http://localhost:4000/api";
type Hero = { id: string; name: string; description: string };
const assertProperties = (entity: Hero) => {
expect(entity.id).to.be.a("string");
expect(entity.name).to.be.a("string");
expect(entity.description).to.be.a("string");
};
const crud = (
method: "GET" | "POST" | "PUT" | "DELETE",
route: string,
{
body,
allowedToFail = false,
}: { body?: Hero | object; allowedToFail?: boolean } = {}
) =>
cy.request<Hero[] & Hero>({
method: method,
url: `${apiUrl}/${route}`,
body: method === "POST" || method === "PUT" ? body : undefined,
retryOnStatusCodeFailure: !allowedToFail,
failOnStatusCode: !allowedToFail,
});
const resetData = () => crud("POST", "reset", { body: data });
beforeEach(resetData);
it("should GET heroes and villains ", () => {
crud("GET", "heroes")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
crud("GET", "villains")
.its("body")
.should("have.length.gt", 0)
.each(assertProperties);
});
it("should CRUD a new hero entity", () => {
const newHero = { id: "Ragnarok", name: "Ragnar", description: "Lothbrok" };
crud("POST", "heroes", { body: newHero });
crud("GET", "heroes")
.its("body")
.then((body) => {
expect(body.at(-1)).to.deep.eq(newHero);
});
const editedHero = { ...newHero, name: "Murat" };
crud("PUT", `heroes/${editedHero.id}`, { body: editedHero });
crud("GET", `heroes/${editedHero.id}`)
.its("body")
.should("deep.eq", editedHero);
crud("DELETE", `heroes/${editedHero.id}`);
crud("GET", `heroes/${editedHero.id}`, { allowedToFail: true })
.its("status")
.should("eq", 404);
});
});
UI e2e testlerimiz olduğunda, bu komutu kurulum ve sökme için kullanmak isteyeceğiz. Bunu bir yardımcı dosyaya taşıyabilir ve onu ihtiyacı olan her testten içe aktarabiliriz. Genel bir kural olarak, 2-3 içe aktarma olduğunda yardımcı bir dosyayı kullanmak ve oradan fonksiyonu içe aktarmak uygundur. Bundan daha fazlası, Cypress komutundan alınması daha uygundur. Bunu nasıl başarılacağını göstermek için bir komut oluşturalım.
Testler eklemeye devam ettik ve tekrar tekrar yeşil ışık aldık. Şimdi sonuçtan memnun kalana kadar yeniden düzenlemeye devam edebiliriz.
crud
ve resetData
komutlarını Cypress komutlarına ekleyin (Düzenleme 4).
// cypress/support/commands.ts
import "@testing-library/cypress/add-commands";
import data from "../fixtures/db.json";
Cypress.Commands.add("getByCy", (selector, ...args) =>
cy.get(`[data-cy="${selector}"]`, ...args)
);
Cypress.Commands.add("getByCyLike", (selector, ...args) =>
cy.get(`[data-cy*=${selector}]`, ...args)
);
Cypress.Commands.add("getByClassLike", (selector, ...args) =>
cy.get(`[class*=${selector}]`, ...args)
);
export type Hero = { id: string; name: string; description: string };
Cypress.Commands.add(
"crud",
(
method: "GET" | "POST" | "PUT" | "DELETE",
route: string,
{
body,
allowedToFail = false,
}: { body?: Hero | object; allowedToFail?: boolean } = {}
) =>
cy.request<Hero[] & Hero>({
method: method,
url: `http://localhost:4000/api/${route}`,
body: method === "POST" || method === "PUT" ? body : undefined,
retryOnStatusCodeFailure: !allowedToFail,
failOnStatusCode: !allowedToFail,
})
);
Cypress.Commands.add("resetData", () =>
cy.crud("POST", "reset", { body: data })
);
Komutun tip tanımını ./cypress.d.ts
dosyasına ekleyin.
// cypress.d.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import { MountOptions, MountReturn } from "cypress/react";
import type { Hero } from "./cypress/support/commands";
export {};
declare global {
namespace Cypress {
interface Chainable {
/** 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>;
/**
* 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>>;
}
}
}
Komutları spec dosyasında kullanın. Burada çalıştığımız veriler için faker
kullanarak son bir dokunuş yapacağız. yarn add -D @faker-js/faker
(Düzenleme 4).
// cypress/e2e/backend/crud.cy.ts
import type {Hero} from '../../support/commands'
import {faker} from '@faker-js/faker'
describe('Backend e2e', () => {
const assertProperties = (entity: Hero) => {
expect(entity.id).to.be.a('string')
expect(entity.name).to.be.a('string')
expect(entity.description).to.be.a('string')
}
beforeEach(() => cy.resetData())
it('should GET heroes and villains ', () => {
cy.crud('GET', 'heroes')
.its('body')
.should('have.length.gt', 0)
.each(assertProperties)
cy.crud('GET', 'villains')
.its('body')
.should('have.length.gt', 0)
.each(assertProperties)
})
it('should CRUD a new hero entity', () => {
const newHero = {
id: faker.datatype.uuid(),
name: faker.internet.userName(),
description: `description ${faker.internet.userName()}`,
}
cy.crud('POST', 'heroes', {body: newHero}).its('status').should('eq', 201)
cy.crud('GET', 'heroes')
.its('body')
.then(body => {
expect(body.at(-1)).to.deep.eq(newHero)
})
const editedHero = {...newHero, name: 'Murat'}
cy.crud('PUT', `heroes/${editedHero.id}`, {body: editedHero})
.its('status')
.should('eq', 200)
cy.crud('GET', `heroes/${editedHero.id}`)
.its('body')
.should('deep.eq', editedHero)
cy.crud('DELETE', `heroes/${editedHero.id}`).its('status').should('eq', 200)
cy.crud('GET', `heroes/${editedHero.id}`, {allowedToFail: true})
.its('status')
.should('eq', 404)
})
Son kod karmaşık görünebilir, ancak oraya ulaşmak için testlerin çalışmasını sağlama ve yeniden düzenleme döngülerinden birçok kez geçti.
Özet
Uygulamamızın konuşabileceği sahte bir sunucu oluşturduk; json-server
kullanarak bunu gerçekleştirdik.
Bu işlem, db.json
dosyasıyla veritabanını seedleme ve uygulamamız başlatıldığında api sunucusunu başlatma gerektirdi.
package.json
komutları ve CI değiştirilmeliydi.
Seedlenen verileri doğrulamak için bir GET
testiyle başladık. Sunucuyu benzersiz bir önek api
kullanacak şekilde değiştirdik ve kahramanları ve kötü adamları doğruladık (Kırmızı 1, Yeşil 1, Düzenleme 1).
Yeni bir kahraman eklemek için yeni bir test oluşturduk. Ancak testin arkasında durum bırakması nedeniyle, testi yeniden çalıştırmak sorunlara neden oldu (Kırmızı 2).
Her testten önce veritabanını orijinal haline döndürmek için json-server-reset
kullandık (Yeşil 2).
Testi güncelleme ve silme ile geliştirdik (Düzenleme 2).
Silinen öğenin veritabanından kaldırıldığından emin olmak için yeni bir test ekledik (Kırmızı 3).
200 dışındaki durum kodlarını doğrulamak için test api'sini geliştirdik (Yeşil 3).
İsteğe bağlı allowedToFail
için bir nesne kullanarak api'yi daha iyi okunabilir ve genişletilebilir hale getirdik (Düzenleme 3).
CRUD komutlarını tek bir işleve taşıdık (Düzenleme 4).
Test eklemeye devam ettik ve tekrar tekrar yeşil ışık aldık.
O noktadan sonra, Cypress komutları, sahte veriler, daha iyi tipler vb. kullanarak sonuçtan memnun kalana kadar yeniden yapılandırmaya devam ettik.
Çıkarılacak Dersler
TDD'nin gerçek değeri geliştirme sırasında anlaşılır. Geliştirme sonrasında da uygulanabilir, ancak KırmızıYeşilRefaktor döngüleri daha çok YeşilRefaktor gibi olur. Kapsamlı testlere ve refaktör edilmiş koda doğru ilerleyen küçük, artan adımlar yine aynıdır.
Cypress ile başparmak kuralı, 2-3 referans / içe aktarma olduğunda yardımcı bir dosya kullanmaktır ve işlevleri oradan içe aktarmaktır. Bunun ötesinde, içe aktarma yapmadan her spec'te kullanılabilen bir Cypress komutu ile daha uygun hale gelir. Okuyucuların aşina olmayabileceği daha sofistike bir yaklaşımı göstermek için bir komut kullanmayı tercih ettik.
Last updated