Skip to content

Instantly share code, notes, and snippets.

@karol-majewski
Created March 31, 2020 22:53
Show Gist options
  • Save karol-majewski/83f7d5cc91e4a1dbe1251b1c1310f4a5 to your computer and use it in GitHub Desktop.
Save karol-majewski/83f7d5cc91e4a1dbe1251b1c1310f4a5 to your computer and use it in GitHub Desktop.
Things I wish I knew before I started writing Cypress tests

Cypress gotchas

Can I spy on a global method?

In theory, yes. Cypress documentation recommends adding spies in the onBeforeLoad phase.

cy.visit('/', {
  onBeforeLoad(win) {
    cy.spy(win, 'snowplow');
  },
});

However, at this point the spied property may not exist yet. To make sure snowplow exists on window, do:

cy.visit('/', {
  onLoad(window$) {
    cy.spy(window$, 'snowplow');
  },
});

or

cy.visit('/');
cy.window().should('have.property', 'snowplow');
cy.window().then(window$ => cy.spy(window$, 'snowplow');

See What's the best way to stub functions and methods below for possible workarounds.

Can I stub a window method?

Yes, if it's a built-in method like window.addEventListener. Cypress recommends using the onBeforeLoad hook for that.

However, if the method is added by your application, then it will not exist when onBeforeLoad is called. In that scenario, you must use the onLoad hook, which may be already too late to spy on your method since it could have already been called.

How do I manage spies?

You can alias your spy by using the as method.

cy.spy(win, 'snowplow').as('snowplow');

If you prefix your alias with the @ symbol, you can access it with cy.get.

cy.get('@snowplow');

You can still reference your original function without an alias. These two are equivalent.

expect(window$.snowplow).to.exist;
expect('@snowplow').to.exist;

Notice that the first variant uses a reference to your original method. Cypress proxies original methods, but does not change their behavior. You can still call them the way you did.

Note: when chaining calls, only the former will work.

cy.window()
  .then(window$ => {
    window$.snowplow?.('trackPageView', null);

    return window$;
  })
  .then(window$ => {
    // Bad: expect('@snowplow').to.have.been.called;
    expect(window$.snowplow).to.have.been.called;
  });

Note: function calls are counted correctly only when you call your method from within a Cypress test. If the method is called by your application, any calls to it will not be counted.

Note: cy.spy() creates spies in a sandbox, so all spies created are automatically reset/restored between tests without you having to explicitly reset/restore them source.

How many Window objects are in play?

Two. One for your application, and one for the Cypress UI.

Currently, the type definition for Window is reused for both. If you have augmented the basic Window interface (for example, you added global variables your application depends on), this may lead to bugs.

See this for more context, and remember to use cy.window().then(window$ => { /* ... */ }) to access your application window.

Note: the dollar sign is used as a suffix when the referenced window is the application window (sometimes called remote window by Cypress).

How do I use should callbacks with the Window object?

Cypress recommends yielding the Window using the cy.window() method, and then wrapping the yielded window with cy.wrap.

cy.window().then(window => {
  cy.wrap(window).should(window$ => {
    const context = getSnowplowSearchPhotosContext(window$);

    expect(validate.context.search(context)).to.be.true;
    expect(context.data.isDefault).to.be.false;
  });
});

However, using assertion without the should callback also works:

cy.window().then(window$ => {
  const context = getSnowplowSearchPhotosContext(window$);

  expect(validate.context.search(context)).to.be.true;
  expect(context.data.isDefault).to.be.false;
});

When in the test lifecycle should I add a spy?

Just before the test suite is ran. Use the beforeEach callback.

You can also do it right in the test case.

cy.window().then(window$ => cy.spy(window$, 'snowplow').as('snowplow'));

cy.visit('/');

cy.get('@snowplow').should('exist');

Can I listen to network requests being made?

In theory, yes.

cy.server();
cy.route('GET', /foo/).as('endpoint');

Key points:

  • route is for listening to requests, and stubbing requests (if you provide the response option)
  • request is for making requests
  • Remember to call server before calling route. See documentation
  • Server can be started before you call cy.visit() source

Note: Cypress only currently supports intercepting XMLHttpRequests. Requests using the Fetch API and other types of network requests like page loads and <script> tags will not be intercepted or visible in the Command Log. See this for more details and temporary workarounds.

Note: by default, Cypress is configured to ignore requests that are used to fetch static content like .js or .html files. This keeps the Command Log less noisy. This option can be changed by overriding the default whitelisting in the cy.server() options. (We should be able to test code splitting this way)

Note: this does seem to work only for requests made to our server, not any server. See this spec for details.

To listen to outgoing requests:

cy.route({
  url: 'https://logger.unsplash.com/i?*',
  onRequest: req => {
    cy.log('A tracking request has been made. Now we can use `expect`');
  },
});

Source

Note: network requests made to analytics are not XHR requests (they are image requests, which is made to bypass origin policies). If the requested resource is an image, and it lives in a different origin then your server, cy.route will not be able to see it.

Known workardounds are either to associate the image with a DOM element, or to collect or network requests by calling window$.performance.getEntries().

window.performance.getEntries().filter(entry => entry.name.includes(SNOWPLOW_TRACKING_ENDPOINT));

Another solution is to stub the service.

My spy doesn't see all function calls

If you spy on a method that gets overriden during the lifecycle of your app, the reference to the spied method will change.

For example, when a third-party <script> such as Snowplow bootstraps itself, it will mutate the Window object and add a property snowplow. If you started spying on (or stubbed) window.snowplow before it happened, you won't be able to spy on it before the reference is mutated.

If that is the case, use global event handlers to stub/spy on your global methods.

What's the best way to stub functions and methods?

There are two ways: global and local. They have different use cases.

Global

Put it outside your top-level describe block. It will affect all tests in that file.

Cypress.on('window:before:load', win => {
  win.snowplow = cy.stub().as('snowplow');
});

Use this method to stub global methods.

Note: once you stub a property this way, it cannot be “unstubbed” or overriden. Every test case in tha file will always refer to the global spy or stub. If you're using this method to stub a property added asynchronously, the real code will have no effect on your tests.

See this for more details.

Local

Put it in the beginning of your describe block, inside a before or beforeEach callback.

beforeEach(() => {
  cy.visit('/', {
    onBeforeLoad: window$ => {
      window$.snowplow = cy.stub().as('snowplow');
    },
  });
});

Because before and beforeEach blocks work for specific describe blocks, you can use this method to mock globals in certain scenarios but not in others.

Note: if your spy/stub is assigned this way, it can be mutated by your application. In that scenario, your alias will no longer work.

I'm seeing a lot of similar stubs in my SPIES/STUBS pane.

It's normal. Cypress re-creates the same stub each time the page is loaded. If you're calling cy.visit a lot in your code, multiple stubs (with the same alias, if you're using one) will be generated.

On the contrary, client-side navigation reuses the same stub.

When asserting the number of calls made to the stub, only the last instance of the stub is considered (as expected).

Can I override an environment variable for a single test only?

Yes. You can, for example, have it disabled for all tests in cypress.json and enable it only for a single test with Cypress.env.

Is it safe to throw in my tests?

Yes. When a test case throws, it simply fails, and the test runner continues to run the remaining test cases in your suite.

If throwing is actually the desired behavior of your app, uses expect(foo).to.throw.

How can I deterministically tell Cypress the app has loaded?

Do this:

if (window.Cypress) {
  window.appReady = true;
}

Await appReady in your test.

beforeEach(() => {
  cy.visit('/');
  cy.window().should('have.property', 'appReady', true);
});

You may want to set appReady when your root component mounts. Keep in mind componentDidMount on classes works differently than its equivalent achieved with React Hooks — componentDidMount is called once all its children have mounted (recursively, bottom-up) but components using Hooks can falsely report their readiness.

See documentation.

Should I use expect or should?

You should use the methods that have the auto-awaiting mechanism built into them. This includes, but is not limited to methods like should, get, find, and contains.

Key takeaways:

When should I use selectors provided by testing-library?

Whenever you can. There are some limitations:

  • Methods like findByPlaceholderText must yield a single element. To find the input element you're interested in, you must first target its parent.

    cy.get('header').findByPlaceholderText('Search free high-resolution photos');
  • Labels and placeholders must consist of plain text only. If, say, your <label> has a <span> inside, findByLabelText will not find your input field.

How can I validate the arguments passed to my spy if I don't know their exact values?

Use sinon.match. The helpers it provides are essentially like propTypes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment