Mocking and Syntax - Comparing Cypress and TestCafe

Lets take a look at the differences between Cypress and TestCafe are when writing and mocking tests.

Fri, 05 Jul 2019

If you want to learn more about TestCafe, please check out my course End-to-end Web Testing with TestCafe: Getting Started on Pluralsight.

notepad unsplash

Writing end-to-end tests has gotten a lot easier to do with tools like Cypress and TestCafe. These two libraries are very similar. They both allow you to mock HTTP requests, although in slightly different ways. They both have a promise-based API, although Cypress has it’s own “promise” in place.

I wanted to put together an article showing you what it looks like in both frameworks to mock an HTTP request and explain why you may want to use this practice. I also wanted to show you why you can’t use async/await for Cypress tests.

Writing Tests: TestCafe vs Cypress

Let’s compare what writing a test looks like in Cypress and TestCafe. For some context, imagine we have a UI that consists of a list of products, with a text input used for filtering down the list. When a user types into the input, the list of products should immediately update to filter out products that don’t match the user input.

In TestCafe, this test could look like this:

test('should properly filter products based on the query entered', async t => {
  // Get the count of products on initial page load
  const originalProductCount = await Selector(`[data-locator='individual-product']`).count;

  // Type a string into the filter input, to filter out products that don't match
  await t.typeText(Selector(`[data-locator='filter-input']`), 'Ben');

  // Get the new count of visible products on the page
  const countOfFilteredProducts = await Selector(`[data-locator='individual-product']`).count;
  await t.expect(originalProductCount).notEql(countOfFilteredProducts);
  await t.expect(countOfFilteredProducts).lt(originalProductCount);
  await t.expect(countOfFilteredProducts).eql(2);
});

This same test in Cypress would look like this:

it('should properly filter products based on the query entered', () => {
  // Get the count of products on initial page load
  cy.get('[data-locator=individual-product]').then(products => {
    let originalProductsCount = products.length;

    // Type a string into the filter input, to filter out products that don't match
    cy.get(`[data-locator='filter-input']`).type('Ben');

    // Get the new count of visible products on the page
    cy.get('[data-locator=individual-product]').then(filteredProducts => {
      let filteredProductsCount = filteredProducts.length;
      expect(originalProductsCount).not.to.equal(filteredProductsCount);
    });
  });
});

Promises in Cypress

You may have noticed that I’m not using async/await with the Cypress tests. Unfortunately, that is a trade-off when using Cypress. The cy commands do not return actual promises, even though you can use .then(...) on each command. The “promise” that is returned is a Chainer object, which is basically a wrapper of a real promise. They chose this design to make Cypress much more deterministic and stable, as normal JS promises have no concept of retries, or retry-ablity.

From the docs:

The Cypress API is not an exact 1:1 implementation of Promises. They have Promise like qualities and yet there are important differences you should be aware of.

  1. You cannot race or run multiple commands at the same time (in parallel).
  2. You cannot ‘accidentally’ forget to return or chain a command.
  3. You cannot add a .catch error handler to a failed command.

The downside is I can’t do something like:

// async / await will not work
it('does something cool', async () => {
  // `cy` commands will never return a useful value
  const value = await cy.get('.product-list');
  expect(value.length).to.equal.(10);
})

Instead you’re code would look closer to this:

it('should have only 10 products', () => {
  cy.get('.product-list')
    .then(products => {
      expect(products.length).to.equal.(10);
    });
})

This should be very familiar since before async/await all of our promises were written this way.

Mocking HTTP Requests

Both TestCafe and Cypress provide a way to mock HTTP requests in your test. This can speed up your test runs since you won’t have to wait for real server responses, which can sometimes take a while, depending on what your app is doing. You should still have a few tests that depend on actual server responses, like for testing login flow. But when testing more granular functionality you should mock some of your requests. I know it sounds weird to do that, but I think of it as I’m testing the UI right now, not the API. You most likely already have tests in place for your API and you don’t need to test it twice. You may ask “But what if the API model changed? Won’t my test fail now?“. If the API changes, that most likely means your UI that handles the responses/requests would need to change as well, which means you probably need to update your tests anyways.

If mocking your HTTP requests still has you worried, you don’t HAVE to do it. It’s totally up to you and your team to decide what will bring you the most confidence in your tests.

Mocking in TestCafe

To mock HTTP requests in TestCafe you would use the RequestMock constructor.

// api-mocks.ts
import { RequestMock } from 'testcafe';
const mock = RequestMock()
  .onRequestTo('https://some.domain.com/api/products')
  .respond({ data: 'data from API' }, 200); // returns JSON response { data }, and 200 statusCode

export default mock;

Then import the mock into your test and use the fixture or test hook called requestHooks:

// spec.ts
import { Selector } from 'testcafe';
import mock from './api-mocks';

fixture('A fixture')
  .page(...)
  .requestHooks(mock)

You can also chain multiple requests and responses to the mock:

// api-mocks.ts
import { RequestMock } from 'testcafe';
const mock = RequestMock()
  .onRequestTo('https://some.domain.com/api/products')
  .respond({ data: 'data from API' }, 200)
  .onRequestTo('https://some.domain.com/api/users')
  .respond(null, 204) // returns empty response
  .onRequestTo('https://some.domain.com/api/products/123')
  .respond((req, res) => {
    // returns a custom response
    res.statusCode = '200';
    res.setBody({ data: { name: 'product name', id: 123 } });
  });

export default mock;

Mocking in Cypress

Stubbing responses in Cypress is slightly different. To tell your tests to start stubbing specified requests, first you need to call cy.server() to enable it. To tell the server what requests to stub and what response to return you then call cy.route(<options>).

cy.server();
cy.route({
  method: 'GET',
  url: '/products',
  response: [{ id: 1 }, { id: 2 }], // list of mock products.
});

You could also skip the route object and use parameters instead:

cy.server();
cy.route('GET', '/products', [{ id: 1 }, { id: 2 }]);

Cypress has this idea of fixtures. Unlike TestCafe, Cypress fixtures are JSON objects that hold the data you’d like to use in a mocked response. This is very useful since sometimes an API can return complex data, and having that in a separate file keeps your spec file clean. So instead of specifying a response inline within the cy.route method, you can specify a fixture to be used. All you need to do is create a new .json file within the /cypress/fixtures/ directory.

// cypress/fixtures/products.json
[
  {
    id: 1,
    name: 'Product A',
  },
  {
    id: 2,
    name: 'Product B',
  },
];

// spec.js
it('Should display a list of products', () => {
  cy.server();
  cy.route('GET', '/api/products', 'fixture:products.json').as('getProducts');

  cy.visit('/a-page-with-a-list-of-products');
  cy.wait(['@getProducts']);

  cy.queryByText('Product A').should('exist');
  cy.queryByText('Product B').should('exist');
});

Conclusion

I hope this helps someone decide between what framework to use for end-to-end testing. I think both of these options here are great choices, and you can’t really go wrong. Mocking your applications HTTP requests can really speed up your test runs. You should still fully test the critical paths, but for the rest of your tests, you should mock. When you are mocking while using Cypress, I recommend utilizing fixtures to define your data. It just keeps things organized.

If you’re just getting started with end-to-end testing check out my course on Pluralsight, End-to-end Web Testing with TestCafe: Getting Started.

Thanks for reading :)

Loading...
marques woodson

Marques approves this article.