Mirage JS Deep Dive:UIテストにMirage JSとCypressを使用する(パート4)

公開: 2022-03-10
クイックサマリー↬MirageJSDeepDiveシリーズのこの最後のパートでは、これまでのシリーズで学んだことをすべて、MirageJSでUIテストを実行する方法を学習します。

ソフトウェアテストについての私のお気に入りの引用の1つは、Flutterのドキュメントからです。 それは言う:

「機能を追加したり、既存の機能を変更したりしても、アプリが引き続き機能することをどのように確認できますか? テストを書くことによって。」

その点で、Mirage JS Deep Diveシリーズのこの最後の部分では、Mirageを使用してJavaScriptフロントエンドアプリケーションをテストすることに焦点を当てます。

この記事はサイプレス環境を想定しています。 サイプレスは、UIテスト用のテストフレームワークです。 ただし、ここでの知識は、使用するUIテスト環境またはフレームワークに転送できます。

シリーズの前の部分を読む:

  • パート1:MirageJSモデルとアソシエーションを理解する
  • パート2:工場、備品、シリアライザーを理解する
  • パート3:タイミング、応答、パススルーを理解する

UIテスト入門書

UIまたはユーザーインターフェイステストは、フロントエンドアプリケーションのユーザーフローを検証するために行われる受け入れテストの形式です。 これらの種類のソフトウェアテストの重点は、デスクトップ、ラップトップからモバイルデバイスに至るまでのさまざまなデバイスでWebアプリケーションを操作する実際の人であるエンドユーザーにあります。 これらのユーザーは、キーボード、マウス、タッチスクリーンなどの入力デバイスを使用してアプリケーションとインターフェイスまたは対話します。 したがって、UIテストは、ユーザーによるアプリケーションとの対話を可能な限り模倣するように作成されています。

たとえば、eコマースのWebサイトを見てみましょう。 典型的なUIテストシナリオは次のとおりです。

  • ユーザーはホームページにアクセスすると、製品のリストを表示できます。

その他のUIテストシナリオは次のとおりです。

  • ユーザーは、製品の詳細ページで製品の名前を確認できます。
  • ユーザーは「カートに追加」ボタンをクリックできます。
  • ユーザーはチェックアウトできます。

わかりますよね?

UIテストを行う際には、ほとんどの場合、バックエンドの状態に依存します。つまり、製品またはエラーを返しましたか? これでMirageが果たす役割は、これらのサーバー状態を必要に応じて微調整できるようにすることです。 したがって、UIテストで本番サーバーに実際のリクエストを行う代わりに、Mirageモックサーバーにリクエストを行います。

この記事の残りの部分では、架空のeコマースWebアプリケーションUIでUIテストを実行します。 それでは始めましょう。

ジャンプした後もっと! 以下を読み続けてください↓

最初のUIテスト

前述のように、この記事はサイプレス環境を想定しています。 サイプレスを使用すると、WebでのUIのテストをすばやく簡単に行うことができます。 クリックとナビゲーションをシミュレートし、アプリケーションのルートにプログラムでアクセスできます。 サイプレスの詳細については、ドキュメントを参照してください。

したがって、CypressとMirageが利用可能であると仮定して、APIリクエストのプロキシ関数を定義することから始めましょう。 これは、サイプレスセットアップのsupport/index.jsファイルで行うことができます。 次のコードを貼り付けるだけです。

 // 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])) }) }) } })

次に、アプリのブートストラップファイル(Vueの場合はmain.js 、Reactの場合はindex.js )で、Cypressが実行されている場合にのみ、Mirageを使用してアプリのAPIリクエストをhandleFromCypress関数にプロキシします。 そのためのコードは次のとおりです。

 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) }) }) }, }) }

この設定により、サイプレスが実行されているときはいつでも、アプリはすべてのAPIリクエストのモックサーバーとしてMirageを使用することを認識しています。

いくつかのUIテストを書き続けましょう。 まず、ホームページをテストして、 5つの製品が表示されているかどうかを確認します。 サイプレスでこれを行うには、プロジェクトディレクトリのルートにあるtestsフォルダーにhomepage.test.jsファイルを作成する必要があります。 次に、サイプレスに次のことを行うように指示します。

  • ホームページにアクセスするie / route
  • 次に、 productのクラスにli要素があるかどうかをアサートし、それらの数が5であるかどうかも確認します。

コードは次のとおりです。

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

フロントエンドアプリケーションに5つの製品を返す本番サーバーがないため、このテストは失敗することを推測したかもしれません。 どうしようか? Mirageでサーバーをモックアウトします! Mirageを導入すると、テストですべてのネットワーク呼び出しを傍受できます。 以下でこれを行い、 afterEach関数で各テストの前にMirageサーバーを起動し、 beforeEach関数でシャットダウンします。 beforeEach関数とafterEach関数はどちらもCypressによって提供されており、テストスイートで各テストを実行する前後にコードを実行できるようになっています。 それでは、このためのコードを見てみましょう:

 // 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) })

さて、私たちはどこかに到達しています。 サーバーをMirageからインポートし、サーバーを起動して、 beforeEach関数とafterEach関数でそれぞれシャットダウンします。 製品リソースのモックを作成してみましょう。

 // 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); });

上記のコードスニペットのMirageビットを理解していない場合は、このシリーズの前の部分をいつでも確認できます。

  • パート1: MirageJSモデルとアソシエーションを理解する
  • パート2:工場、備品、シリアライザーを理解する
  • パート3:タイミング、応答、パススルーを理解する

さて、製品モデルを作成し、 /api/productsルートのルートハンドラーを作成することで、サーバーインスタンスの具体化を開始しました。 ただし、テストを実行すると、Mirageデータベースにまだ製品がないため、テストは失敗します。

Mirageデータベースにいくつかの製品を入力してみましょう。 これを行うには、サーバーインスタンスでcreate()メソッドを使用することもできますが、5つの製品を手動で作成するのはかなり面倒なようです。 より良い方法があるはずです。

はい、あります。 工場を活用しましょう(このシリーズの第2部で説明します)。 次のように製品ファクトリを作成する必要があります。

 // 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); });

次に、最後に、 createList()を使用して、テストに合格する必要がある5つの製品をすばやく作成します。

これをやろう:

 // 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); });

したがって、テストを実行すると、合格です。

各テストの後、Mirageのサーバーはシャットダウンおよびリセットされるため、この状態がテスト間でリークすることはありません。

複数のMirageサーバーの回避

このシリーズをフォローしていると、ネットワーク要求を傍受するために開発でMirageを使用していたことに気付くでしょう。 Mirageをセットアップしたアプリのルートにserver.jsファイルがありました。 DRY(Do n't Repeat Yourself)の精神では、開発とテストの両方にMirageの2つの別々のインスタンスを用意する代わりに、そのサーバーインスタンスを利用するのが良いと思います。 これを行うには( server.jsファイルがまだない場合)、プロジェクトのsrcディレクトリにファイルを作成するだけです。

JavaScriptフレームワークを使用している場合、構造は異なりますが、一般的な考え方は、プロジェクトのsrcルートにserver.jsファイルを設定することです。

したがって、この新しい構造を使用して、Mirageサーバーインスタンスの作成を担当する関数をserver.jsにエクスポートします。 それをしましょう:

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

homepage.test.js .test.jsで作成したMirageJSサーバーを削除し、 makeServer関数本体に追加して、 makeServer関数の実装を完了しましょう。

 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; }

これで、テストにmakeServerをインポートするだけです。 単一のMirageServerインスタンスを使用する方がクリーンです。 このようにして、開発環境とテスト環境の両方で2つのサーバーインスタンスを維持する必要はありません。

makeServer関数をインポートすると、テストは次のようになります。

 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); });

これで、開発とテストの両方で役立つ中央ミラージュサーバーができました。 makeServer関数を使用して、開発中のMirageを開始することもできます(このシリーズの最初の部分を参照)。

Mirageコードは、本番環境への道を見つけるべきではありません。 したがって、ビルドセットアップによっては、開発モード中にのみMirageを起動する必要があります。

MirageとVue.jsを使用してAPIモックを設定する方法に関する私の記事を読んで、使用するフロントエンドフレームワークで複製できるようにVueでどのように設定したかを確認してください。

テスト環境

Mirageには、開発(デフォルト)とテストの2つの環境があります。 開発モードでは、Mirageサーバーのデフォルトの応答時間は400ms(カスタマイズ可能です。これについては、このシリーズの3番目の記事を参照してください)で、すべてのサーバー応答をコンソールに記録し、開発シードをロードします。

ただし、テスト環境では、次のものがあります。

  • テストを高速に保つための遅延0
  • Mirageは、CIログを汚染しないように、すべてのログを抑制します
  • Mirageはseeds()関数も無視するため、シードデータは開発のみに使用できますが、テストに漏れることはありません。 これは、テストを決定論的に保つのに役立ちます。

テスト環境を利用できるように、 makeServerを更新しましょう。 これを行うには、環境オプションを使用してオブジェクトを受け入れるようにします(デフォルトで開発に設定し、テストでオーバーライドします)。 server.jsは次のようになります。

 // 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; }

また、ES6プロパティの省略形を使用して、環境オプションをMirageサーバーインスタンスに渡していることにも注意してください。 これが整ったら、テストを更新して、テストする環境値をオーバーライドしましょう。 テストは次のようになります。

 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テスト

Mirageは、トリプルAまたはAAAテストアプローチと呼ばれるテストの標準を推奨しています。 これは、 ArrangeAct 、およびAssertの略です。 この構造は、上記のテストですでに確認できます。

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

このパターンを破る必要があるかもしれませんが、10回のうち9回は、テストには問題なく機能するはずです。

エラーをテストしましょう

これまでのところ、ホームページで5つの商品があるかどうかをテストしましたが、サーバーがダウンしたり、商品の取得に問題が発生した場合はどうなりますか? このような場合にUIがどのように表示されるかを処理するために、サーバーがダウンするのを待つ必要はありません。 Mirageを使用してそのシナリオを簡単にシミュレートできます。

ユーザーがホームページにいるときに500(サーバーエラー)を返しましょう。 前回の記事で見たように、Mirageの応答をカスタマイズするには、Responseクラスを使用します。 それをインポートして、テストを書いてみましょう。

 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"); });

なんて柔軟性のある世界でしょう。 商品の取得に失敗した場合にUIがどのように表示されるかをテストするために、Mirageが返す応答をオーバーライドするだけです。 全体的なhomepage.test.jsファイルは次のようになります。

 // 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ハンドラーに加えた変更は、テストでのみ有効であることに注意してください。 つまり、開発モードでは、以前に定義したとおりに機能します。

したがって、テストを実行すると、両方とも合格するはずです。

サイプレスでクエリしている要素は、フロントエンドUIに存在する必要があることに注意してください。 サイプレスはHTML要素を作成しません。

製品詳細ページのテスト

最後に、製品詳細ページのUIをテストしてみましょう。 これが私たちがテストしているものです:

  • ユーザーは製品詳細ページで製品名を見ることができます

それを手に入れましょう。 まず、このユーザーフローをテストするための新しいテストを作成します。

テストは次のとおりです。

 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は最終的にこのようになるはずです。

 // 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'); });

テストを実行すると、3つすべてに合格するはずです。

まとめ

このシリーズでMirageJSの内部を紹介するのは楽しかったです。 Mirageを使用してバックエンドサーバーをモックアウトすることで、より優れたフロントエンド開発エクスペリエンスを開始できるようになりました。 また、この記事の知識を使用して、フロントエンドアプリケーションのアクセプタンス/ UI /エンドツーエンドテストをさらに作成してください。

  • パート1:MirageJSモデルとアソシエーションを理解する
  • パート2:工場、備品、シリアライザーを理解する
  • パート3:タイミング、応答、パススルーを理解する
  • パート4: UIテストにMirageJSとCypressを使用する