-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Warn when defining alias in before hook - as they are not reusable in following tests. #665
Comments
That's why moving this code into a |
Some of this is discussed more in this issue. #583 |
I really wouldn't worry about performance of a Cypress runs every command asynchronously automatically, and that in itself is vastly more time consuming than any single |
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 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 This things add up real quick. For me it makes a big difference if checking the appearance of form costs me |
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.
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 |
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 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 I'll be reopening this issue and changing the scope to warning when an alias is defined in a |
The documentation here: https://docs.cypress.io/guides/core-concepts/variables-and-aliases.html#Aliases is also mentioning using aliases with
|
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. |
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.
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. |
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'}).then(() => {
// Allows accessing alias value in before()
console.log(this.myAlias)
}
})
it('runs first test', function () {
// Alias accessible here
cy.log(this.myAlias.bar)
})
it('runs second test', function () {
// Alias accessible here
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! |
We have seen many use cases above for the need to have an alias with a wider scope. Ill add another one here. What I am trying to achieve is create a random email in the before hook and wrap it into an alias. Use that random email in various tests. I did a vague workaround by writing all the test under a single test. However I reached a blocker when I had to change the domain of the URL. Cypress dosent allow us to use different domains in the same test. So back to using multiple test, where I cannot use the same alias across tests. |
A lack of persisting aliases in any capacity is going to take much of the modular design that cypress encourages and throw it out the window.
1 will be a nightmare This all seems like a lot of work to bypass an inability to persist an alias |
Morgan, can you create an example test that you are trying to write please? Then we can see what you are trying to do and maybe how the test could be written in a more intuitive style
…Sent from my iPhone
On Jan 5, 2021, at 18:25, Morgan Heinemann ***@***.***> wrote:
A lack of persisting aliases in any capacity is going to take much of the modular design that cypress encourages and throw it out the window.
I need to visit a page and get a list of fields that will be used for the actual tests but I'm unable to expect those fields to be 100% static so i can't use a fixture.
Possible solutions:
cram ALL the test cases in to a single a single test and make it harder to diagnose individual regression breaks and be harder to maintain
declare some JS variables and then define them later on and hope nothing breaks
put the site visit and data collection in the before each
Make a 1-off custom command
1 will be a nightmare
2 will probably work but will take me time to debug and make consistent
3 just isn't a viable option
4 MIGHT work but clutters up the actions with things I'll only need for one specific test
This all seems like a lot of work to bypass an inability to persist an alias
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
some of this is my "working" code and some is "what I'd like to do" describe("create users", function() {
before(function(){ // this 'before' handling getting all my test data once would be ideal
cy.fixture('master_credentials').as('creds').then(function (creds) {
cy.adminLogin(creds);
cy.visit(`${this.creds.adminSite.url}${this.creds.adminSite.port}/ep-admin/domain/domain/`)
cy.get(".field-name").then(function(domains){
let domainClean = [];
for(let i = 0; i < domains.length; i++){
if(!baseDomains.includes(domains[i].innerText)){
domainClean.push(domains[i].innerText)
}
}
cy.wrap(domainClean).as('testDomains')
})
})
})
beforeEach(function () {
// just to keep my tests separate I would log in for each
cy.get('@creds').then(function (creds) {
cy.adminLogin(creds);
})
})
it("create users for each test domain", function(){
cy.visit('http://admin.localtest.me:7000/ep-admin/common/user/')
cy.get(".field-email").then(function(emailAddresses){
let existingAddresses = []
for(let i = 0; i < emailAddresses.length; i++){
existingAddresses.push(emailAddresses[i].innerText)
}
cy.get('@testDomains').then((val1)=>{
val1.forEach(function(val,ind){
let email = `${val.split('.',1)[0]}@testdomain.com`
expect(existingAddresses).to.not.include(email)
})
})
})
})
// more tests using domains as templates or existing emails to go here
}) |
@Ginsusamurai have you tried my suggestion above? #665 (comment) I've got aliases working fine with like 3 extra lines and a simple new convention. Yes, it could be more intuitive, but I don't think anyone is blocked if they're already in this issue reading :) Working exampleCommand setupCommand usage (in
|
Thanks @patcon , I must have missed that. edit: |
I think the major advantage of using |
Sorry, I'm not totally clear on the nature of your thoughtful push-back -- I use something comparable to this to pass around dynamic values derived from content of pages visited in before(); to be specific, pages visited during the command execution itself. It might be possible to do with js vars, but that's even more off-road from cypress best practices, as far as I understand |
I have the observation on Cypress v6.8.0, in my test scenario, I'd like to store the object to an alias in |
Like a lot of you, I also need to have variables across tests. For example, I created x number of users and want to store their data to be used and referenced across the other tests as I prefer to have small "it" blocks. I don't have a lot of experience with Cypress (in fact, I've only been using it for the past 24hrs). But here is my solution, hope it helps others. I added the following in the commands
and then I use it this way.
my preference is if the Cypress team would add a param to .alias() to set the scope as in |
So, being new to Cypress I've just faced this problem and was quite surprised there is no "clean solution" yet, especially one that would be endorsed or provided directly from the Cypress API. Anyway, here's what I ended up doing in order to re-use aliases, so I can keep my tests kinda easy to read. I've got a function that returns some alias getters and use it like this at the top of my test file. Then in each tests I just call the alias getter whenever I need it.
...etc. This pattern can even work for an alias getter that use another alias, you just need to call alias getter dependencies in the "final" alias getter. Here an example of how the alias getters function looks like, it could be in another utils file or something, in my case I just defined it at the bottom of the file since the function is hoisted.
|
I found this article very useful -> https://medium.com/quick-code/passing-variables-between-tests-in-cypress-3bea0eb821fb |
Well, might be slightly off-the-topic but I stumled upon this in my research of how to avoid repetitive tasks like navigating with ui to the same url b/w tests which might include 2-6 route actions which involve data fetching for all intermediate routes and takes time as well and came up with a function which can be executed in declare namespace Cypress {
interface Chainable {
/**
* Used in `beforeEach` to use web navigation only during first test execution, all subsequesnt tests use the location as product of the function passed as the first argument
* @param fn a function containing the navigation logic. The location after the function code is run will be reused in subsequent calls. This function is run only during first execution
*/
revisit(fn: () => void): Chainable<void>;
}
}
Cypress.Commands.add(
'revisit',
function (this: { lastVisitedUrl?: string }, fn: () => void) {
if (this.lastVisitedUrl) cy.visit(this.lastVisitedUrl);
else {
fn();
cy.location().its('href').as('lastVisitedUrl');
}
},
); Now in the suite only the first test will have the full user navigation of clicking the links, while the second and all subsequent tests of the suite will use the href and navigate there straight away as if by permalink. Mind it if you're using any contexts passed around by navigating to routes via router context as those will not be available using this method describe('Workshops', function () {
before(() => {
//auth logic
});
beforeEach(() => {
cy.revisit(() => {
//navigation execued only once
cy.visitMenu({ top: 'enterpriseSettings', side: 'productionStructure' }); //visit('/'), click top menu (url changes) and side menu (url changes)
cy.getTab('workshops').click(); // click tab on page (url changes)
cy.get('#addButton').click(); //click add button to go to add page (url changes)
});
cy.getScreen('workshopCreate').as('createScreen');
});
it('first add test', () => {
// came here by cicking through navigtion
});
it('second add test', () => {
// came here by full url
})
it('third add test', () => {
// came here by full url
})
// yep, there are more tests down below for the same add page because it has complex logic PS: using |
We've found that aliases set in a before block are persisted in some test files and not others. It turns out that tests within a Fails: const testUser = { id: "test" };
describe("My test", () => {
before(() => {
cy.wrap(testUser).as("user");
});
it("loads first time", function () {
cy.wrap(this.user).should("eq", testUser);
});
it("loads second time", function () {
// Fails
cy.wrap(this.user).should("eq", testUser);
});
}); Works: const testUser = { id: "test" };
describe("My test", () => {
before(() => {
cy.wrap(testUser).as("user");
});
describe("Keeping aliased state", () => {
it("loads first time", function () {
cy.wrap(this.user).should("eq", testUser);
});
it("loads second time", function () {
// Works
cy.wrap(this.user).should("eq", testUser);
});
});
}); |
deleted |
codeincorect's solution didn't work for me (v10.3.1) so I came up with another solution that instead involves dumping and restoring aliases. Here is a full example: describe("My Tests", () => {
const store = {};
before(() => {
cy.wrap("bar").as("foo");
cy.dumpAliases(store);
});
beforeEach(() => {
cy.restoreAliases(store);
});
it("test1", () => { cy.get("@foo").should("eq", "bar"); });
it("test2", () => { cy.get("@foo").should("eq", "bar"); });
it("test3", () => { cy.get("@foo").should("eq", "bar"); });
after(() => {
cy.restoreAliases(store);
cy.get("@foo").then((foo) => {
cy.log("Foo is ", foo);
});
});
});
/**
* Dumps all the aliases from the current context into the store.
* Typically used in `before(() => ...)`
*
* Workaround for aliases set in `before` being unavailable after the first test.
* See https://github.com/cypress-io/cypress/issues/665
*/
Cypress.Commands.add("dumpAliases", function (store: Record<string, any>, aliases?: string[]) {
if (aliases == null) {
const { currentTest, test, _runnable, ...rest } = this;
Object.assign(store, rest);
} else {
for (const alias of aliases) {
store[alias] = this[alias];
}
}
});
/**
* Restores the aliases from the given store
* Typically used in `beforeEach(() => ...)`
*
* Workaround for aliases set in `before` being unavailable after the first test
* See https://github.com/cypress-io/cypress/issues/665
*/
Cypress.Commands.add("restoreAliases", function (store: Record<string, any>, aliases?: string[]) {
if (aliases == null) {
for (const [alias, value] of Object.entries(store)) {
cy.wrap(value).as(alias);
}
} else {
for (const alias of aliases) {
cy.wrap(store[alias]).as(alias);
}
}
}); |
When separate In your const globalAliases = {};
module.exports = defineConfig({
e2e: {
setupNodeEvents(on: any, config: any) {
on("task", {
globalAlias(newValues?: Record<string, any>): Record<string, any> {
if (newValues) {
Object.assign(globalAliases, newValues);
}
return globalAliases;
},
});
return config;
},
}
}) We could use this task directly from the tests, but for convenience it is nice to create the following commands, that wrap the task calls: Cypress.Commands.add(
"asGlobal",
{
prevSubject: true,
},
(subject: any, alias: string) =>
cy.task("globalAlias", { [alias]: subject }).then(() => subject)
);
Cypress.Commands.add("getGlobal", (alias: string) => {
return cy
.task("globalAlias")
.then((globalAliases: any) => globalAliases[alias]);
});
declare global {
namespace Cypress {
interface cy {
getGlobal(alias: string): Chainable<any>;
}
interface Chainable<Subject> {
asGlobal(alias: string): Chainable<Subject>;
}
}
} We can then use the commands in our tests: it("should do something", () => {
// .. some logic
cy.title().asGlobal("title");
});
it("should do something else", () => {
cy.getGlobal("title").then(title => {
// Use title
});
}); |
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:
Additional Info (images, stack traces, etc)
The given Example will lead to the following behaviour:
it()
will succeedit()
will failbefore
in describe number imput into abeforeEach
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.
The text was updated successfully, but these errors were encountered: