Mirage JS 심층 분석: UI 테스트에 Mirage JS 및 Cypress 사용(4부)

게시 됨: 2022-03-10
빠른 요약 ↬ Mirage JS Deep Dive 시리즈의 마지막 부분에서는 Mirage JS를 사용하여 UI 테스트를 수행하는 방법을 배우는 데 지난 시리즈에서 배운 모든 내용을 적용할 것입니다.

소프트웨어 테스팅에 관해 제가 가장 좋아하는 인용문 중 하나는 Flutter 문서에서 발췌한 것입니다. 그것은 말한다:

"더 많은 기능을 추가하거나 기존 기능을 변경할 때 앱이 계속 작동하도록 하려면 어떻게 해야 합니까? 테스트를 작성함으로써.”

참고로 Mirage JS 심층 분석 시리즈의 마지막 부분에서는 Mirage를 사용하여 JavaScript 프런트 엔드 애플리케이션을 테스트하는 데 중점을 둡니다.

참고 : 이 문서는 Cypress 환경을 가정합니다. Cypress는 UI 테스트를 위한 테스트 프레임워크입니다. 그러나 여기에서 지식을 사용하는 UI 테스트 환경이나 프레임워크로 전송할 수 있습니다.

시리즈의 이전 부분 읽기:

  • 1부: Mirage JS 모델 및 연결 이해
  • 2부: 공장, 설비 및 직렬 변환기 이해
  • 3부: 타이밍, 응답 및 통과 이해

UI 테스트 입문서

UI 또는 사용자 인터페이스 테스트는 프런트 엔드 애플리케이션의 사용자 흐름을 확인하기 위해 수행되는 승인 테스트의 한 형태입니다. 이러한 종류의 소프트웨어 테스트는 데스크톱, 랩톱에서 모바일 장치에 이르는 다양한 장치에서 웹 응용 프로그램과 상호 작용할 실제 사람인 최종 사용자에게 중점을 둡니다. 이러한 사용자 는 키보드, 마우스 또는 터치 스크린과 같은 입력 장치를 사용하여 애플리케이션과 인터페이스하거나 상호 작용합니다. 따라서 UI 테스트는 가능한 한 가깝게 애플리케이션과의 사용자 상호 작용을 모방하도록 작성됩니다.

전자상거래 웹사이트를 예로 들어 보겠습니다. 일반적인 UI 테스트 시나리오는 다음과 같습니다.

  • 사용자는 홈페이지를 방문하면 제품 목록을 볼 수 있습니다.

다른 UI 테스트 시나리오는 다음과 같습니다.

  • 사용자는 상품의 상세 페이지에서 상품명을 볼 수 있습니다.
  • 사용자는 "장바구니에 추가" 버튼을 클릭할 수 있습니다.
  • 사용자가 결제할 수 있습니다.

당신은 아이디어를 이해, 그렇지?

UI 테스트를 할 때 주로 백엔드 상태에 의존하게 됩니다. 즉, 제품이나 오류를 반환했습니까? 여기서 Mirage의 역할은 해당 서버 상태를 필요에 따라 조정할 수 있도록 하는 것입니다. 따라서 UI 테스트에서 프로덕션 서버에 실제 요청을 하는 대신 Mirage 모의 서버에 요청합니다.

이 기사의 나머지 부분에서는 가상의 전자 상거래 웹 애플리케이션 UI에 대한 UI 테스트를 수행합니다. 시작하겠습니다.

점프 후 더! 아래에서 계속 읽기 ↓

첫 번째 UI 테스트

앞서 언급했듯이 이 문서에서는 Cypress 환경을 가정합니다. Cypress를 사용하면 웹에서 UI를 빠르고 쉽게 테스트할 수 있습니다. 클릭과 탐색을 시뮬레이션할 수 있고 애플리케이션에서 프로그래밍 방식으로 경로를 방문할 수 있습니다. Cypress에 대한 자세한 내용은 문서를 참조하십시오.

따라서 Cypress와 Mirage를 사용할 수 있다고 가정하고 API 요청에 대한 프록시 기능을 정의하여 시작하겠습니다. Cypress 설정의 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 )에서 Mirage를 사용하여 Cypress가 실행 중일 때만 앱의 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) }) }) }, }) }

이 설정을 사용하면 Cypress가 실행될 때마다 앱에서 Mirage를 모든 API 요청에 대한 모의 서버로 사용한다는 것을 알게 됩니다.

몇 가지 UI 테스트를 계속 작성해 보겠습니다. 먼저 5개의 제품 이 표시되는지 확인하기 위해 홈페이지를 테스트합니다. Cypress에서 이를 수행하려면 프로젝트 디렉토리의 루트에 있는 tests 폴더에 homepage.test.js 파일을 생성해야 합니다. 다음으로 Cypress에 다음을 수행하도록 지시합니다.

  • 홈페이지 방문 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를 도입하면 테스트에서 모든 네트워크 호출을 가로챌 수 있습니다. 아래에서 이것을 수행하고 beforeEach 함수에서 각 테스트 전에 Mirage 서버를 시작하고 beforeEach 함수에서도 종료해 afterEach . beforeEachafterEach 함수는 모두 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에서 서버를 가져왔고 각각 beforeEachafterEach 함수에서 서버를 시작하고 종료합니다. 제품 리소스를 조롱하는 방법을 살펴보겠습니다.

 // 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부: Mirage JS 모델 및 연결 이해
  • 2부: 공장, 설비 및 직렬 변환기 이해
  • 3부: 타이밍, 응답 및 통과 이해

좋습니다. 제품 모델을 생성하고 /api/products 라우트에 대한 라우트 핸들러를 생성하여 Server 인스턴스를 구체화하기 시작했습니다. 그러나 테스트를 실행하면 Mirage 데이터베이스에 아직 제품이 없기 때문에 실패합니다.

Mirage 데이터베이스를 일부 제품으로 채우겠습니다. 이렇게 하려면 서버 인스턴스에서 create() 메서드를 사용할 수 있었지만 손으로 ​​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() { 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(Don't Repeat Yourself) 정신으로, 개발과 테스트를 위해 Mirage의 두 인스턴스를 별도로 두는 대신 해당 서버 인스턴스를 활용하는 것이 좋다고 생각합니다. 이렇게 하려면(이미 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에서 생성한 Mirage JS 서버를 제거하고 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 를 가져오기만 하면 됩니다. 단일 Mirage Server 인스턴스를 사용하는 것이 더 깔끔합니다. 이렇게 하면 개발 및 테스트 환경 모두에 대해 두 개의 서버 인스턴스를 유지 관리할 필요가 없습니다.

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

따라서 이제 개발 및 테스트 모두를 지원하는 중앙 Mirage 서버가 있습니다. makeServer 함수를 사용하여 개발 중인 Mirage를 시작할 수도 있습니다(이 시리즈의 첫 번째 부분 참조).

Mirage 코드는 프로덕션에 들어가는 방법을 찾지 않아야 합니다. 따라서 빌드 설정에 따라 개발 모드 중에만 Mirage를 시작해야 합니다.

참고 : Mirage 및 Vue.js를 사용하여 API Mocking을 설정하는 방법에 대한 내 기사를 읽고 Vue에서 이를 어떻게 수행하여 사용하는 모든 프론트엔드 프레임워크에서 복제할 수 있는지 확인하십시오.

테스트 환경

Mirage에는 개발 (기본값) 및 테스트 라는 두 가지 환경이 있습니다. 개발 모드에서 Mirage 서버의 기본 응답 시간은 400ms이며(사용자 정의 가능. 이에 대해서는 이 시리즈의 세 번째 기사 참조) 모든 서버 응답을 콘솔에 기록하고 개발 시드를 로드합니다.

그러나 테스트 환경에는 다음이 있습니다.

  • 테스트를 빠르게 유지하기 위해 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는 Triple-A 또는 AAA 테스트 접근 방식이라는 테스트 표준을 권장합니다. 이는 Arrange , ActAssert 의 약자입니다. 위의 테스트에서 이미 이 구조를 볼 수 있습니다.

 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 핸들러에 대한 수정 사항은 테스트에만 있습니다. 즉, 개발 모드에 있을 때 이전에 정의한 대로 작동합니다.

따라서 테스트를 실행할 때 둘 다 통과해야 합니다.

참고 : Cypress에서 쿼리하는 요소는 프런트 엔드 UI에 있어야 합니다. Cypress는 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'); });

테스트를 실행할 때 세 가지 모두를 통과해야 합니다.

마무리

이번 시리즈에서 Mirage JS의 내부를 보여줘서 즐거웠습니다. Mirage를 사용하여 백엔드 서버를 조롱함으로써 더 나은 프론트엔드 개발 경험을 시작할 준비가 되었기를 바랍니다. 또한 이 기사의 지식을 사용하여 프런트 엔드 애플리케이션에 대한 더 많은 승인/UI/엔드 투 엔드 테스트를 작성하기를 바랍니다.

  • 1부: Mirage JS 모델 및 연결 이해
  • 2부: 공장, 설비 및 직렬 변환기 이해
  • 3부: 타이밍, 응답 및 통과 이해
  • 4부: UI 테스트에 Mirage JS 및 Cypress 사용