Cypress: Warn when defining alias in before hook - as they are not reusable in following tests.

Created on 19 Sep 2017  Â·  21Comments  Â·  Source: cypress-io/cypress

Since this is my first Issue here, I want to use this opportunity to thank you guys for your hard work. I love and enjoy working with cypress so far and I'm really looking forward to upcoming releases.

Current behaviour:

It seems like, that a defined alias can only be used once under certain circumstances. The test code will be self explanatory about my approach.

Desired behaviour:

The defined alias should be reusable as expected. Since we are doing a lot of end to end testing, we are trying hard to achieve the best possible performance. As far as I know, requery-ing the dom with cy.get() is in general a bit expensive, so we try to avoid that.
Making aliases reusable as much as possible, would also result in prettier, slimmer and easier to manage test code.

Test code:

describe('reusing alias', () => {
  before(() => {
    cy.visit('/my-page.html');
  });
  // ...
  describe('number input', () => {
    before(() => {
      cy.get('#my-input').as('input');
    });
    it('should be of type number', () => {
      cy.get('@input').should('have.attr', 'type', 'number');
    });
    it('should allow a minimum value of 1', () => {
      cy.get('@input').should('have.attr', 'min', '1');
    });
    it('should allow a maximum value of 3', () => {
      cy.get('@input').should('have.attr', 'max', '3');
    });
  });
});

Additional Info (images, stack traces, etc)

The given Example will lead to the following behaviour:

  • The first it() will succeed
  • The second and third it() will fail
    image
  • Changing the before in describe number imput into a beforeEach will solve the problem. But not really, I don't want to re-query the dom, I just want to re-use the defined alias.

Maybe I am getting something wrong here. If so, feel free to correct my way of thinking here.

┆Issue is synchronized with this Jira Features by Unito

internal-priority pkdriver ready for work hooks ↪ unexpected behavior

Most helpful comment

Clearing aliases between each test is surprisingly inflexible for no good reason. I do much data setup in before all hook and save ids of generated objects in aliases. Doing the same setup for every tests will add 10s of seconds to each test. If your remedy is to add all related tests into one single test then you end up with ugly, long test that do too many things and hard to maintain.

Even after all hook forget all the previously set aliases. Then why have before all/after all hook at all? This is just asking for developers to circumvent the alias issue by using regular variable instead.

All 21 comments

before code is only run once, and between the tests Cypress removes all of the aliases. So in subsequent tests it is not available.

That's why moving this code into a beforeEach will work.

Some of this is discussed more in this issue. https://github.com/cypress-io/cypress/issues/583

I really wouldn't worry about performance of a cy.get, or trying to over optimize this.

Cypress runs every command asynchronously automatically, and that in itself is vastly more time consuming than any single cy.get.

Thanks for your quick response here.

Is there any kind of documentation / best practice paper that teaches me about performance?

I'm used to writing it's that are just executing one or maybe two expect statements, so that my test stay as precise as possible.

But it seems like that behaviour really has a bad impact on the test run times. That is why I'm experimenting around with that topic at the moment.

I measured as good as I could, and it seems like that this ...

describe('number input', () => {
  beforeEach(() => {
    cy.get('#my-input').as('input');
  });
  it('should be of type number', () => {
    cy.get('@input').should('have.attr', 'type', 'number');
  });
  it('should allow a minimum value of 1', () => {
    cy.get('@input').should('have.attr', 'min', '1');
  });
  it('should allow a maximum value of 3', () => {
    cy.get('@input').should('have.attr', 'max', '3');
  });
});

... is about twice as slow as this ...

it('should render number input with range 1 to 3', () => {
  cy
    .get('#my-input')
    .as('input')
    .should('have.attr', 'type', 'number')
    .should('have.attr', 'min', '1')
    .should('have.attr', 'max', '3');
});

Is it the vast amount of it statements or is it because of the amount of get queries? How can I analyse things like this?

This things add up real quick. For me it makes a big difference if checking the appearance of form costs me ~55 sec or ~20 sec.

It's much more slow to split them up into individual tests because Cypress performs a series of setup and teardown events in between tests.

e2e / integration tests are not like unit tests - you should absolutely 100% group all assertions related to the same DOM elements in a single test. There is no benefit splitting things out because with Cypress you already receive a ton of feedback about assertion failures.

Cypress tests is more about testing features than anything else. If you're testing that your form components adhere to attributes you could simply add all of them in a single test. Or better yet just use them and test their values as opposed to testing their attributes.

Thanks for your thoughts. I just state on this really quick.

There is no benefit splitting things out because with Cypress you already receive a ton of feedback about assertion failures.

I tried to do it this way to optimise my tests for the headless runs, where I can not simply analyse the failure over the beautiful GUI of yours. Yes, the dashboard (which I really like btw) already supplies a stack trace, but I thought it will be easier for us to analyse errors if we split things up better.

But this is def. not worth it if it comes with big performance trade-offs like this.

Can we look forward to performance optimisations on those teardown and setup events? Is there anything on the roadmap yet? Just curious here 🤓

A failure includes a screenshot + a video so you could likely derive it from there.

The setup and teardown involve network requests and talking to things like the browser so it's not really optimizable. We have a much bigger issue open for this which we refer to as Lifecycle Events which will be an API you use to control how and when Cypress does its teardown between tests. As of right now that is not exposed to you.

Thanks for your kind support on this. This thread gave me an idea on how I can manage to make our builds faster (for example by avoiding splitting up it statements unnecessarily).
Maybe those infos should land in some way or form in the docs. I'm sure that a lot of people are looking to make their test suits more performant.

Btw: keep up the good work 👍 Love using your product.

We continue to get feedback from users - unexpectedly running into this behavior. There was a proposal within our team to potentially warn if an .as is assigned within a before, so that at least people would be warned that aliases are cleared when used in before. Another case for displaying warning in command log. https://github.com/cypress-io/cypress/issues/4917

I'll be reopening this issue and changing the scope to warning when an alias is defined in a before.

The documentation here: https://docs.cypress.io/guides/core-concepts/variables-and-aliases.html#Aliases is also mentioning using aliases with before

Using .then() callback functions to access the previous command values is great—but what happens when you’re running code in hooks like before or beforeEach?

Clearing aliases between each test is surprisingly inflexible for no good reason. I do much data setup in before all hook and save ids of generated objects in aliases. Doing the same setup for every tests will add 10s of seconds to each test. If your remedy is to add all related tests into one single test then you end up with ugly, long test that do too many things and hard to maintain.

Even after all hook forget all the previously set aliases. Then why have before all/after all hook at all? This is just asking for developers to circumvent the alias issue by using regular variable instead.

Ach, I would love to prepare setup for my tests once in before hook. Would you consider to enable aliases? I think many users would appreciate it :-(

One observation here is if the alias is a primitive value, it can be retrieved/reused in following cases. But if the value of alias is an object, then it is gone. So I do think there is a bug in clearing context or setting alias... More interesting thing is if I create an unnecessary describe/context to wrap all cases, then all alias can be retrieved/reused :) You can use this way to work around this issue.

Yeah I would like to do the below for API testing but get an error saying the 'response' alias doesn't exist for the 2nd and 3rd 'it' statements.
Currently my options are to either use 'before each' rather than 'before' which means calling the API 3 times or group them into 1 test which doesn't read very well in the editor or the cypress runner at all.

describe("Users - list", () => {
      before(() => {
          cy.request('users').as('response');
      });
      it('Response status code is correct', () => {
          // check status code of response
      });
      it('Response security headers are set', () => {
          // check security headers of response
      });
      it('Response body is correct', () => {
          // check body of response
      });
});

I also wanted to do some requests only once. I have tried this and it worked.

context('logged', () => {
  let WTFAlias
  before(() => {
    cy.fixture('login').then(login => {
      cy.request('POST', Cypress.env('API_URL') + 'login', login.mimaria).then(
        token => {
          WTFAlias = token
        }
      )
    })
  })
  beforeEach(() => {
    cy.request('POST', '/api/token', { token: WTFAlias.body.data.token })
  })
  ;['/user/settings', '/consumer/checkout', '/consumer/verify'].map(path => {
    context(path, () => {
      it('successfully loads', () => {
        cy.visit(path)
      })
    })
  })
})

Explanation of the use case.

I have an API where I need to go to do the real login, but I also have a webapp only API that I need to call to set the https only cookie that the SSR will use.

So I want to do the real login just once, and reuse the token to create the cookies.

I could do the whole thing in beforeEach, but what would be the benefit of that? It makes much more sense to get the token once, and reuse it.

Up for the fix

fwiw, aliases created in before seem to work when the creation happens within a command executed in before(). Is this intentional, or expected to be preserved behaviour?

// cypress/support/commands.js
Cypress.Commands.add("createSurvey", () => {
  cy.login()
  cy.request({
    method: 'POST',
    url: Cypress.config().apiPath + '/survey',
    body: {foo: 'bar'}
  }).its('body.survey_id').as('surveyId').then(surveyId => {
    cy.visit(`/survey/${surveyId}`)
  })
})
// cypress/integration/aliases.spec.js
describe('Cypress Aliases', () => {
  before(function () {
    cy.createSurvey()
  })

  it('runs first test', function () {
    cy.log(this.surveyId)
  })

  it('runs second test', function () {
    cy.log(this.surveyId)
  })
})

Is there any other way to share variables across tests?

we definitely need an alias with scope that can be shared across all test cases.
this is my work around:

context("test", () => {
  let obj;

  before(() => {
    // do sth, e.g. call api, to get some data
    obj = data;
  });

  beforeEach(() => {
    cy.wrap(obj).as("obj");
  });

  it("test case 1", () => {
    cy.get("@obj").then((obj) => {
      ......
    });
  });

  it("test case 2", () => {
    cy.get("@obj").then((obj) => {
      ......
    });
  });

for sure i can directly access the variable but it's not recommended and i'm still looking forward to have alias with wider scope to support this and prepare for future support.
it's not necessarily to be done in before, but tell us where to define a context-scope alias.

Assuming my approach above works (others are welcome to validate), then couldn't you just write a helper command to accommodate this for now?

// cypress/support/commands.js
Cypress.Commands.add("asPersistent", (alias, value) => {
  cy.wrap(value).as(alias)
})
// cypress/integration/foo.spec.js
describe('Something', () => {
  before(function () {
    cy.asPersistent('myAlias', {bar: 'baz'})
  })

  it('runs first test', function () {
    cy.log(this.myAlias.bar)
  })

  it('runs second test', function () {
    cy.log(this.myAlias.bar)
  })

})

My 2c, I'm having similar issues now with aliases not persisting. I'd also like a way to stage my data in the before hook and let the aliases persist across the whole test, please. Staging data in the beforeEach hook will add 10-15 seconds per test as I'd need to reset the data each time (delete and recreate) - just not feasible when all I want is to call on a created user's GUID!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

egucciar picture egucciar  Â·  3Comments

stormherz picture stormherz  Â·  3Comments

carloscheddar picture carloscheddar  Â·  3Comments

jennifer-shehane picture jennifer-shehane  Â·  3Comments

zbigniewkalinowski picture zbigniewkalinowski  Â·  3Comments