• 18-19 College Green, Dublin 2
  • 01 685 9088
  • info@cunninghamwebsolutions.com
  • cunninghamwebsolutions
    Cunningham Web Solutions
    • HomeHome
    • About UsAbout Us
    • Our ServicesOur Services
      • Web Design
      • Digital Marketing
      • SEO Services
      • E-commerce Websites
      • Website Redevelopment
      • Social Media Services
    • Digital MarketingDigital Marketing
      • Adwords
      • Social Media Services
      • Email Marketing
      • Display Advertising
      • Remarketing
    • PortfolioPortfolio
    • FAQ’sFAQ’s
    • BlogBlog
    • Contact UsContact Us
    MENU CLOSE back  
    • Home
    • About Us
    • Our Services
      • back
      • Web Design
      • Digital Marketing
      • SEO Services
      • E-commerce Websites
      • Website Redevelopment
      • Social Media Services
    • Digital Marketing
      • back
      • Adwords
      • Social Media Services
      • Email Marketing
      • Display Advertising
      • Remarketing
    • Portfolio
    • FAQ’s
    • Blog
    • Contact Us

    Mirage JS Deep Dive: Using Mirage JS And Cypress For UI Testing (Part 4)

    You are here:
    1. Home
    2. Web Design
    3. Mirage JS Deep Dive: Using Mirage JS And Cypress For UI Testing (Part 4)
    Thumbnail for 25137
    Smashing Editorial

    Mirage JS Deep Dive: Using Mirage JS And Cypress For UI Testing (Part 4)

    Mirage JS Deep Dive: Using Mirage JS And Cypress For UI Testing (Part 4)

    Kelvin Omereshone

    2020-06-17T10:30:00+00:00
    2020-06-17T12:38:09+00:00

    One of my favorite quotes about software testing is from the Flutter documentation. It says:

    “How can you ensure that your app continues to work as you add more features or change existing functionality? By writing tests.”

    On that note, this last part of the Mirage JS Deep Dive series will focus on using Mirage to test your JavaScript front-end application.

    Note: This article assumes a Cypress environment. Cypress is a testing framework for UI testing. You can, however, transfer the knowledge here to whatever UI testing environment or framework you use.

    Read Previous Parts Of The Series:

    • Part 1: Understanding Mirage JS Models And Associations
    • Part 2: Understanding Factories, Fixtures And Serializers
    • Part 3: Understanding Timing, Response And Passthrough

    UI Tests Primer

    UI or User Interface test is a form of acceptance testing done to verify the user flows of your front-end application. The emphasis of these kinds of software tests is on the end-user that is the actual person who will be interacting with your web application on a variety of devices ranging from desktops, laptops to mobile devices. These users would be interfacing or interacting with your application using input devices such as a keyboard, mouse, or touch screens. UI tests, therefore, are written to mimic the user interaction with your application as close as possible.

    Let’s take an e-commerce website for example. A typical UI test scenario would be:

    • The user can view the list of products when visiting the homepage.

    Other UI test scenarios might be:

    • The user can see the name of a product on the product’s detail page.
    • The user can click on the “add to cart” button.
    • The user can checkout.

    You get the idea, right?

    In making UI Tests, you will mostly be relying on your back-end states, i.e. did it return the products or an error? The role Mirage plays in this is to make those server states available for you to tweak as you need. So instead of making an actual request to your production server in your UI tests, you make the request to Mirage mock server.

    For the remaining part of this article, we will be performing UI tests on a fictitious e-commerce web application UI. So let’s get started.

    Our First UI Test

    As earlier stated, this article assumes a Cypress environment. Cypress makes testing UI on the web fast and easy. You could simulate clicks and navigation and you can programmatically visit routes in your application. See the docs for more on Cypress.

    So, assuming Cypress and Mirage are available to us, let’s start off by defining a proxy function for your API request. We can do so in the support/index.js file of our Cypress setup. Just paste the following code in:

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

    Then, in your app bootstrapping file (main.js for Vue, index.js for React), we’ll use Mirage to proxy your app’s API requests to the handleFromCypress function only when Cypress is running. Here is the code for that:

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

    With that setup, anytime Cypress is running, your app knows to use Mirage as the mock server for all API requests.

    Let’s continue writing some UI tests. We’ll begin by testing our homepage to see if it has 5 products displayed. To do this in Cypress, we need to create a homepage.test.js file in the tests folder in the root of your project directory. Next, we’ll tell Cypress to do the following:

    • Visit the homepage i.e / route
    • Then assert if it has li elements with the class of product and also checks if they are 5 in numbers.

    Here is the code:

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

    You might have guessed that this test would fail because we don’t have a production server returning 5 products to our front-end application. So what do we do? We would mock out the server in Mirage! If we bring in Mirage, it can intercept all network calls in our tests. Let’s do this below and start the Mirage server before each test in the beforeEach function and also shut it down in the afterEach function. The beforeEach and afterEach functions are both provided by Cypress and they were made available so you could run code before and after each test run in your test suite — hence the name. So let’s see the code for this:

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

    Okay, we are getting somewhere; we have imported the Server from Mirage and we are starting it and shutting it down in beforeEach and afterEach functions respectively. Let’s go about mocking our product resource.

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

    Note: You can always take a peek at the previous parts of this series if you don’t understand the Mirage bits of the above code snippet.

    • Part 1: Understanding Mirage JS Models And Associations
    • Part 2: Understanding Factories, Fixtures And Serializers
    • Part 3: Understanding Timing, Response And Passthrough

    Okay, we have started fleshing out our Server instance by creating the product model and also by creating the route handler for the /api/products route. However, if we run our tests, it will fail because we don’t have any products in the Mirage database yet.

    Let’s populate the Mirage database with some products. In order to do this, we could have used the create() method on our server instance, but creating 5 products by hand seems pretty tedious. There should be a better way.

    Ah yes, there is. Let’s utilize factories (as explained in the second part of this series). We would create our product factory like so:

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

    Then, finally, we’ll use createList() to quickly create the 5 products that our test needs to pass.

    Let’s do this:

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

    So when we run our test, it passes!

    Note: After each test, Mirage’s server is shutdown and reset, so none of this state will leak across tests.

    Avoiding Multiple Mirage Server

    If you have been following along this series, you’d notice when we were using Mirage in development to intercept our network requests; we had a server.js file in the root of our app where we set up Mirage. In the spirit of DRY (Don’t Repeat Yourself), I think it would be good to utilize that server instance instead of having two separate instances of Mirage for both development and testing. To do this (in case you don’t have a server.js file already), just create one in your project src directory.

    Note: Your structure will differ if you are using a JavaScript framework but the general idea is to setup up the server.js file in the src root of your project.

    So with this new structure, we would export a function in server.js that is responsible for creating our Mirage server instance. Let’s do that:

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

    Let’s complete the implementation of the makeServer function by removing the Mirage JS server we created in homepage.test.js and adding it to the makeServer function body:

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

    Now all you have to do is import makeServer in your test. Using a single Mirage Server instance is cleaner; this way you don’t have to maintain two server instances for both development and test environments.

    After importing the makeServer function, our test should now look like this:

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

    So we now have a central Mirage server that serves us in both development and testing. You can also use the makeServer function to start Mirage in development (see first part of this series).

    Your Mirage code should not find it’s way into production. Therefore, depending on your build setup, you would need to only start Mirage during development mode. Read my article on how to set up API Mocking with Mirage and Vue.js to see how I did that in Vue so you could replicate in whatever front-end framework you use.

    Testing Environment

    Mirage has two environments: development (default) and test. In development mode, the Mirage server will have a default response time of 400ms(which you can customize. See the third article of this series for that), logs all server responses to the console, and loads the development seeds.

    However, in the test environment, we have:

    • 0 delays to keep our tests fast
    • Mirage suppresses all logs so as not to pollute your CI logs
    • Mirage will also ignore the seeds() function so that your seed data can be used solely for development but won’t leak into your tests. This helps keep your tests deterministic.

    Let’s update our makeServer so we can have the benefit of the test environment. To do that we would make it accept an object with the environment option(we will default it to development and override it in our test). Our server.js should now look like this:

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

    Also note that we are passing the environment option to the Mirage server instance using the ES6 property shorthand. Now with this in place, let’s update our test to override the environment value to test. Our test now looks like this:

    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 Testing

    Mirage encourages a standard for testing called the triple-A or AAA testing approach. This stands for Arrange, Act and Assert. You could see this structure in our above test already:

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

    You might need to break this pattern but 9 times out of 10 it would work just fine for your tests.

    Let’s Test Errors

    So far, we’ve tested our homepage to see if it has 5 products, however, what if the server is down or something went wrong with fetching the products? We don’t need to wait for the server to be down to work on how our UI would look like in such a case. We can simply simulate that scenario with Mirage.

    Let’s return a 500 (Server error) when the user is on the homepage. As we have seen in a previous article, to customize Mirage responses we make use of the Response class. Let’s import it and write our test.

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

    What a world of flexibility! We just override the response Mirage would return in order to test how our UI would display if it failed fetching products. Our overall homepage.test.js file would now look like this:

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

    Note the modification we did to the /api/products handler only lives in our test. That means it works as we previously define when you are in development mode.

    So when we run our tests, both should pass.

    Note: I believe its worthy of noting that the elements we are querying for in Cypress should exist in your front-end UI. Cypress doesn’t create HTML elements for you.

    Testing The Product Detail Page

    Finally, we would test the UI of the product detail page. So this is what we are testing for:

    • User can see the product name on the product detail page

    Let’s get to it. First, we create a new test to test this user flow.

    Here is the 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');
    });
    

    Your homepage.test.js should finally look like this.

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

    When you run your tests, all three should pass.

    Wrapping Up

    It’s been fun showing you the inners of Mirage JS in this series. I hope you have been better equipped to start having a better front-end development experience by using Mirage to mock out your back-end server. I also hope you’ll use the knowledge from this article to write more acceptance/UI/end-to-end tests for your front-end applications.

    • Part 1: Understanding Mirage JS Models And Associations
    • Part 2: Understanding Factories, Fixtures And Serializers
    • Part 3: Understanding Timing, Response And Passthrough
    • Part 4: Using Mirage JS And Cypress For UI Testing

    (ra, il)

    From our sponsors: Mirage JS Deep Dive: Using Mirage JS And Cypress For UI Testing (Part 4)

    Posted on 17th June 2020Web Design
    FacebookshareTwittertweetGoogle+share

    Related posts

    Archived
    22nd March 2023
    Archived
    18th March 2023
    Archived
    20th January 2023
    Thumbnail for 25788
    Handling Continuous Integration And Delivery With GitHub Actions
    19th October 2020
    Thumbnail for 25778
    A Monthly Update With New Guides And Community Resources
    19th October 2020
    Thumbnail for 25781
    Supercharge Testing React Applications With Wallaby.js
    19th October 2020
    Latest News
    • Archived
      22nd March 2023
    • Archived
      18th March 2023
    • Archived
      20th January 2023
    • 20201019 ML Brief
      19th October 2020
    • Thumbnail for 25788
      Handling Continuous Integration And Delivery With GitHub Actions
      19th October 2020
    • Thumbnail for 25786
      The Future of CX with Larry Ellison
      19th October 2020
    News Categories
    • Digital Marketing
    • Web Design

    Our services

    Website Design
    Digital Marketing
    SEO
    E-commerce
    Social Media Services
    Website Design
    Website Design

    A website is an important part of any business. Professional website development is an essential element of a successful online business.

    We provide website design services for every type of website imaginable. We supply brochure websites, E-commerce websites, bespoke website design, custom website development and a range of website applications. We love developing websites, come and talk to us about your project and we will tailor make a solution to match your requirements.

    You can contact us by phone, email or send us a request through our online form and we can give you a call back.

    More Information

    Digital Marketing
    Digital Marketing

    Our digital marketeers have years of experience in developing and excuting digital marketing strategies. We can help you promote your business online with the most effective methods to achieve the greatest return for your marketing budget. We offer a full service with includes the following:

    1. Social Media Marketing

    2. Email & Newsletter Advertising

    3. PPC - Pay Per Click

    4. A range of other methods are available

    More Information

    SEO
    SEO Services

    SEO is an essential part of owning an online property. The higher up the search engines that your website appears, the more visitors you will have and therefore the greater the potential for more business and increased profits.

    We offer a range of SEO services and packages. Our packages are very popular due to the expanse of on-page and off-page SEO services that they cover. Contact us to discuss your website and the SEO services that would best suit to increase your websites ranking.

    More Information

    E-commerce
    E-commerce Websites

    E-commerce is a rapidly growing area with sales online increasing year on year. A professional E-commerce store online is essential to increase sales and is a reflection of your business to potential customers. We provide professional E-commerce websites custom built to meet our clients requirements.

    Starting to sell online can be a daunting task and we are here to make that journey as smooth as possible. When you work with Cunningham Web Solutions on your E-commerce website, you will benefit from the experience of our team and every detail from the website design to stock management is carefully planned and designed with you in mind.

    More Information

    Social Media Services
    Social Media Services

    Social Media is becoming an increasingly effective method of marketing online. The opportunities that social media marketing can offer are endless and when managed correctly can bring great benefits to every business.

    Social Media Marketing is a low cost form of advertising that continues to bring a very good ROI for our clients. In conjuction with excellent website development and SEO, social media marketing should be an essential part of every digital marketing strategy.

    We offer Social Media Management packages and we also offer Social Media Training to individuals and to companies. Contact us to find out more.

    More Information

    Cunningham Web Solutions
    © Copyright 2025 | Cunningham Web Solutions
    • Home
    • Our Services
    • FAQ's
    • Account Services
    • Privacy Policy
    • Contact Us