Mirage JS Deep Dive: UI Testi için Mirage JS ve Cypress'i Kullanma (4. Bölüm)

Yayınlanan: 2022-03-10
Kısa özet ↬ Mirage JS Derin Dalış serisinin bu son bölümünde, geçmiş seride öğrendiğimiz her şeyi Mirage JS ile UI testlerinin nasıl gerçekleştirileceğini öğrenmeye koyacağız.

Yazılım testiyle ilgili en sevdiğim alıntılardan biri Flutter belgelerine aittir. Diyor ki:

"Daha fazla özellik eklerken veya mevcut işlevleri değiştirirken uygulamanızın çalışmaya devam etmesini nasıl sağlayabilirsiniz? Testler yazarak.”

Bu notta, Mirage JS Deep Dive serisinin bu son bölümü, JavaScript ön uç uygulamanızı test etmek için Mirage kullanmaya odaklanacaktır.

Not : Bu makale bir Cypress ortamını varsaymaktadır. Cypress, UI testi için bir test çerçevesidir. Bununla birlikte, buradaki bilgiyi, kullandığınız UI test ortamına veya çerçevesine aktarabilirsiniz.

Serinin Önceki Bölümlerini Okuyun:

  • Bölüm 1: Mirage JS Modellerini ve İlişkilendirmeleri Anlama
  • Bölüm 2: Fabrikaları, Fikstürleri ve Serileştiricileri Anlama
  • Bölüm 3: Zamanlamayı, Yanıtı ve Geçişi Anlama

UI Testleri Astarı

UI veya Kullanıcı Arayüzü testi, ön uç uygulamanızın kullanıcı akışlarını doğrulamak için yapılan bir kabul testi şeklidir. Bu tür yazılım testlerinin vurgusu, web uygulamanızla masaüstü bilgisayarlardan dizüstü bilgisayarlardan mobil cihazlara kadar çeşitli cihazlarda etkileşime girecek gerçek kişi olan son kullanıcı üzerindedir. Bu kullanıcılar , klavye, fare veya dokunmatik ekranlar gibi giriş aygıtlarını kullanarak uygulamanızla arabirim veya etkileşimde bulunacaklardır. Bu nedenle UI testleri, uygulamanızla kullanıcı etkileşimini mümkün olduğunca yakın taklit etmek için yazılır.

Örneğin bir e-ticaret sitesini ele alalım. Tipik bir UI test senaryosu şöyle olacaktır:

  • Kullanıcı, ana sayfayı ziyaret ederken ürün listesini görüntüleyebilir.

Diğer UI test senaryoları şunlar olabilir:

  • Kullanıcı, ürünün detay sayfasında bir ürünün adını görebilir.
  • Kullanıcı "sepete ekle" butonuna tıklayabilir.
  • Kullanıcı ödeme yapabilir.

Fikri anladın, değil mi?

UI Testleri yaparken, çoğunlukla arka uç durumlarınıza güveneceksiniz, yani ürünleri mi döndürdü yoksa bir hata mı verdi? Mirage'ın bunda oynadığı rol, bu sunucu durumlarını ihtiyaç duyduğunuz şekilde ince ayar yapabilmeniz için kullanılabilir hale getirmektir. Bu nedenle, UI testlerinizde üretim sunucunuza gerçek bir istekte bulunmak yerine, Mirage sahte sunucuya istekte bulunursunuz.

Bu makalenin geri kalan kısmında, hayali bir e-ticaret web uygulaması UI üzerinde UI testleri gerçekleştireceğiz. O halde başlayalım.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

İlk Kullanıcı Arayüzü Testimiz

Daha önce belirtildiği gibi, bu makale bir Cypress ortamını varsayar. Cypress, web üzerinde kullanıcı arayüzünü test etmeyi hızlı ve kolay hale getirir. Tıklamaları ve navigasyonu simüle edebilir ve uygulamanızdaki rotaları programlı olarak ziyaret edebilirsiniz. Cypress hakkında daha fazla bilgi için belgelere bakın.

Cypress ve Mirage'ın bizim için uygun olduğunu varsayarak, API isteğiniz için bir proxy işlevi tanımlayarak başlayalım. Bunu Cypress kurulumumuzun support/index.js dosyasında yapabiliriz. Sadece aşağıdaki kodu yapıştırın:

 // cypress/support/index.js Cypress.on("window:before:load", (win) => { win.handleFromCypress = function (request) { return fetch(request.url, { method: request.method, headers: request.requestHeaders, body: request.requestBody, }).then((res) => { let content = res.headers.map["content-type"] === "application/json" ? res.json() : res.text() return new Promise((resolve) => { content.then((body) => resolve([res.status, res.headers, body])) }) }) } })

Ardından, uygulamanızın önyükleme dosyanızda (Vue için main.js , React için index.js ), uygulamanızın API isteklerini yalnızca Cypress çalışırken handleFromCypress işlevine proxy yapmak için Mirage'ı kullanırız. İşte bunun için kod:

 import { Server, Response } from "miragejs" if (window.Cypress) { new Server({ environment: "test", routes() { let methods = ["get", "put", "patch", "post", "delete"] methods.forEach((method) => { this[method]("/*", async (schema, request) => { let [status, headers, body] = await window.handleFromCypress(request) return new Response(status, headers, body) }) }) }, }) }

Bu kurulumla, Cypress her çalıştığında, uygulamanız tüm API istekleri için Mirage'ı sahte sunucu olarak kullanmayı bilir.

Bazı UI testleri yazmaya devam edelim. Görüntülenen 5 ürün olup olmadığını görmek için ana sayfamızı test ederek başlayacağız. Bunu Cypress'te yapmak için proje dizininizin kök dizinindeki tests klasöründe bir homepage.test.js dosyası oluşturmamız gerekiyor. Ardından, Cypress'e aşağıdakileri yapmasını söyleyeceğiz:

  • Ana sayfayı ziyaret edin, yani / rota
  • Ardından, product sınıfına sahip li öğeleri olup olmadığını ve sayı olarak 5 olup olmadığını kontrol edin.

İşte kod:

 // homepage.test.js it('shows the products', () => { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Ön uç uygulamamıza 5 ürün döndüren bir üretim sunucumuz olmadığı için bu testin başarısız olacağını tahmin etmiş olabilirsiniz. Peki ne yapıyoruz? Sunucuyu Mirage'da alay ediyoruz! Mirage'ı getirirsek, testlerimizde tüm ağ aramalarını durdurabilir. Bunu aşağıda yapalım ve beforeEach afterEach . beforeEach ve afterEach işlevlerinin ikisi de Cypress tarafından sağlanır ve test takımınızdaki her test çalıştırmasından önce ve sonra kod çalıştırabilmeniz için kullanıma sunulmuştur - dolayısıyla adı. Öyleyse bunun kodunu görelim:

 // homepage.test.js import { Server } from "miragejs" let server beforeEach(() => { server = new Server() }) afterEach(() => { server.shutdown() }) it("shows the products", function () { cy.visit("/") cy.get("li.product").should("have.length", 5) })

Tamam, bir yere varıyoruz; Sunucuyu Mirage'dan içe aktardık ve sırasıyla beforeEach ve afterEach fonksiyonlarında başlatıp kapatıyoruz. Ürün kaynağımızla alay etmeye başlayalım.

 // homepage.test.js import { Server, Model } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Not : Yukarıdaki kod parçacığının Mirage bitlerini anlamadıysanız, bu dizinin önceki bölümlerine her zaman göz atabilirsiniz.

  • Bölüm 1: Mirage JS Modellerini ve İlişkilendirmeleri Anlama
  • Bölüm 2: Fabrikaları, Fikstürleri ve Serileştiricileri Anlama
  • Bölüm 3: Zamanlamayı, Yanıtı ve Geçişi Anlama

Tamam, ürün modelini ve ayrıca /api/products yolu için yol işleyicisini oluşturarak Sunucu örneğimizi detaylandırmaya başladık. Ancak, testlerimizi çalıştırırsak başarısız olacaktır çünkü Mirage veritabanında henüz ürünümüz yoktur.

Mirage veritabanını bazı ürünlerle dolduralım. Bunu yapmak için sunucu örneğimizde create() yöntemini kullanabilirdik, ancak elle 5 ürün oluşturmak oldukça sıkıcı görünüyor. Daha iyi bir yol olmalı.

Ah evet, var. Fabrikaları kullanalım (bu serinin ikinci bölümünde açıklandığı gibi). Ürün fabrikamızı şu şekilde oluşturmamız gerekecek:

 // homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Son olarak, testimizin geçmesi gereken 5 ürünü hızlı bir şekilde oluşturmak için createList() 'i kullanacağız.

Bunu yapalım:

 // homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList("product", 5) cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Yani testimizi çalıştırdığımızda geçer!

Not : Her testten sonra Mirage'ın sunucusu kapatılır ve sıfırlanır, bu nedenle bu durumun hiçbiri testler arasında sızıntı yapmaz.

Çoklu Mirage Sunucularından Kaçınmak

Bu seriyi takip ediyorsanız, ağ isteklerimizi engellemek için Mirage'ı geliştirme aşamasında kullandığımızı fark etmişsinizdir; Mirage'ı kurduğumuz uygulamamızın kökünde bir server.js dosyamız vardı. DRY (Kendini Tekrar Etme) ruhuna uygun olarak, hem geliştirme hem de test için iki ayrı Mirage örneğine sahip olmak yerine o sunucu örneğini kullanmanın iyi olacağını düşünüyorum. Bunu yapmak için (zaten bir server.js dosyanız yoksa), proje src dizininizde bir tane oluşturmanız yeterlidir.

Not : Bir JavaScript çerçevesi kullanıyorsanız yapınız farklı olacaktır ancak genel fikir server.js dosyasını projenizin src kökünde kurmaktır.

Dolayısıyla bu yeni yapıyla, server.js Mirage sunucu örneğimizi oluşturmaktan sorumlu bir işlevi dışa aktaracağız. Hadi bunu yapalım:

 // src/server.js export function makeServer() { /* Mirage code goes here */}

homepage.test.js içerisinde oluşturduğumuz Mirage JS sunucusunu makeServer fonksiyon gövdesine ekleyerek makeServer fonksiyonunun uygulamasını tamamlayalım:

 import { Server, Model, Factory } from 'miragejs'; export function makeServer() { let server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }

Şimdi tek yapmanız gereken makeServer içe aktarmak. Tek bir Mirage Server örneği kullanmak daha temizdir; bu şekilde hem geliştirme hem de test ortamları için iki sunucu örneği bulundurmanız gerekmez.

makeServer işlevini içe aktardıktan sonra testimiz şu şekilde görünmelidir:

 import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer(); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });

Artık hem geliştirme hem de test etmede bize hizmet eden merkezi bir Mirage sunucumuz var. Mirage'ı geliştirme aşamasında başlatmak için makeServer işlevini de kullanabilirsiniz (bu dizinin ilk bölümüne bakın).

Mirage kodunuz üretim yolunu bulamamalıdır. Bu nedenle, yapı kurulumunuza bağlı olarak Mirage'ı yalnızca geliştirme modu sırasında başlatmanız gerekir.

Not : Vue'da bunu nasıl yaptığımı görmek için Mirage ve Vue.js ile API Mocking'in nasıl kurulacağına ilişkin makalemi okuyun, böylece kullandığınız ön uç çerçevesinde çoğaltabilirsiniz.

Test Ortamı

Mirage'ın iki ortamı vardır: geliştirme (varsayılan) ve test . Geliştirme modunda, Mirage sunucusunun varsayılan tepki süresi 400 ms'dir (ki bunu özelleştirebilirsiniz. Bunun için bu serinin üçüncü makalesine bakın), tüm sunucu yanıtlarını konsola kaydeder ve geliştirme tohumlarını yükler.

Ancak, test ortamında elimizde:

  • Testlerimizi hızlı tutmak için 0 gecikme
  • Mirage, CI günlüklerinizi kirletmemek için tüm günlükleri gizler
  • Mirage ayrıca seed seeds() işlevini yok sayar, böylece tohum verileriniz yalnızca geliştirme için kullanılabilir ancak testlerinize sızmaz. Bu, testlerinizi deterministik tutmanıza yardımcı olur.

Test ortamından faydalanabilmemiz için makeServer güncelleyelim. Bunu yapmak için, ortam seçeneğiyle bir nesneyi kabul etmesini sağlayacağız (varsayılan olarak geliştirmeye ve testimizde geçersiz kılacağız). server.js şimdi şöyle görünmelidir:

 // src/server.js import { Server, Model, Factory } from 'miragejs'; export function makeServer({ environment = 'development' } = {}) { let server = new Server({ environment, models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }

Ayrıca, ES6 özelliği kısayolunu kullanarak ortam seçeneğini Mirage sunucu örneğine aktardığımızı unutmayın. Şimdi bunu yerine getirerek, test edilecek ortam değerini geçersiz kılmak için testimizi güncelleyelim. Testimiz şimdi şöyle görünüyor:

 import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });

AAA Testi

Mirage, üçlü A veya AAA test yaklaşımı olarak adlandırılan bir test standardını teşvik eder. Bu, Düzenle , Harekete Geç ve İddia Et anlamına gelir. Bu yapıyı yukarıdaki testimizde zaten görebilirsiniz:

 it("shows all the products", function () { // ARRANGE server.createList("product", 5) // ACT cy.visit("/") // ASSERT cy.get("li.product").should("have.length", 5) })

Bu kalıbı kırmanız gerekebilir, ancak 10'dan 9'u testleriniz için gayet iyi çalışmalıdır.

Hataları Test Edelim

Şimdiye kadar ana sayfamızı 5 ürün olup olmadığını görmek için test ettik, ancak ya sunucu çalışmıyorsa veya ürünleri getirirken bir şeyler ters gittiyse? Böyle bir durumda kullanıcı arayüzümüzün nasıl görüneceği konusunda çalışmak için sunucunun kapalı olmasını beklememize gerek yok. Bu senaryoyu Mirage ile basitçe simüle edebiliriz.

Kullanıcı ana sayfadayken 500 (Sunucu hatası) döndürelim. Bir önceki makalede gördüğümüz gibi, Mirage yanıtlarını özelleştirmek için Response sınıfını kullanıyoruz. İçeri aktaralım ve testimizi yazalım.

 homepage.test.js import { Response } from "miragejs" it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });

Ne esnek bir dünya! Ürün getirmede başarısız olursa, kullanıcı arayüzümüzün nasıl görüntüleneceğini test etmek için Mirage'ın döndüreceği yanıtı geçersiz kılıyoruz. Genel homepage.test.js dosyamız şimdi şöyle görünür:

 // homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });

/api/products işleyicisine yaptığımız değişikliğin yalnızca testimizde yaşadığına dikkat edin. Bu, geliştirme modundayken daha önce tanımladığımız gibi çalıştığı anlamına gelir.

Yani testlerimizi çalıştırdığımızda her ikisi de geçmeli.

Not : Cypress'te sorguladığımız öğelerin ön uç kullanıcı arayüzünüzde bulunması gerektiğini belirtmeye değer olduğuna inanıyorum. Cypress sizin için HTML öğeleri oluşturmaz.

Ürün Detay Sayfasının Test Edilmesi

Son olarak, ürün detay sayfasının kullanıcı arayüzünü test edelim. Yani bunun için test ediyoruz:

  • Kullanıcı, ürün detay sayfasında ürün adını görebilir

Hadi hadi bakalım. Öncelikle bu kullanıcı akışını test etmek için yeni bir test oluşturuyoruz.

İşte test:

 it("shows the product's name on the detail route", function() { let product = this.server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });

homepage.test.js sonunda böyle görünmelidir.

 // homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { console.log(server); server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); }); it("shows the product's name on the detail route", function() { let product = server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });

Testlerinizi yaptığınızda, üçünün de geçmesi gerekir.

Toplama

Bu seride size Mirage JS'nin içindekileri göstermek eğlenceliydi. Umarım arka uç sunucunuzu taklit etmek için Mirage kullanarak daha iyi bir ön uç geliştirme deneyimi yaşamaya başlamak için daha donanımlısınızdır. Ayrıca, ön uç uygulamalarınız için daha fazla kabul/kullanıcı arayüzü/uçtan uca testler yazmak için bu makaledeki bilgileri kullanacağınızı umuyorum.

  • Bölüm 1: Mirage JS Modellerini ve İlişkilendirmeleri Anlama
  • Bölüm 2: Fabrikaları, Fikstürleri ve Serileştiricileri Anlama
  • Bölüm 3: Zamanlamayı, Yanıtı ve Geçişi Anlama
  • Bölüm 4: UI Testi İçin Mirage JS ve Cypress'i Kullanma