React app with Cypress

By Adam Trzciński

Two weeks ago, Cypress was open source and available to everyone.

Cypress is a tool that makes writing your end-to-end tests faster.

Fast, easy, and reliable testing of anything running in your browser.

Let’s give it a try and see if it’s true!

We will integrate Cypress with one of our projects, Eedi. Eedi is the perfect education platform for teachers, students and parents in the UK. The point is that anyone who uses it has a pleasant and fluid experience while browsing, and all the features work as expected.

configuration

In the root of our application, let’s add Cypress as a dev dependency.

$ yarn add --dev cypress
Copy the code

Adjust “scripts” in package.json:

"scripts": {..."cypress:open": "cypress open"
}
Copy the code

Run the service locally, as normal, and then open Cypress in a new terminal window:

$ yarn run cypress:open
Copy the code

Here we have access to all of our tests, even out of the box.

Cypress has created new folders Cypress with subfolders Fixtures, Integration, and Support. It also adds an empty configuration file, cypress.json.

Since we often access our root path, it is a good practice to abstract it as a configuration file. Open the cypress.json file and add a new entry with the key baseUrl and url as the value:

{
  "baseUrl": "http://localhost:3000"
}
Copy the code

In the example_spec.js file, we can see ‘Kitchen Sink Tests’, which we can use when we want to browse through some common test scenarios. But let’s write our own test now.

Test the login

Login is one of the most important functions of any application. If we don’t do it right, our users won’t be able to see the rest of our work, and there’s no point in doing anything else.

Create a new file login_spec.js. Here, we will test all of our logins.

Let’s write down our first test, and let’s check that Happy Path works as expected:

describe('Log In', () => {
  it('succesfully performs login action', () = > {/ / access 'baseUrl'
    cy.visit('/');
    // Assert that we are in a better position - search 'smarter world'
    cy.contains('smarter world');
    // Search for div with 'Teachers' and click on it
    cy.get('a[data-testid="main-link-teachers"]').click();
    // Check whether the URL has changed
    cy.url().should('includes'.'teachers');
    cy.contains('more time to teach');
    // Find the Login button and click it
    cy.get('button[data-testid="menu-button-login"]').click();
    // Check whether the URL has changed
    cy.url().should('includes'.'/login');
    // Submit the input form and click the Submit button
    cy.get('input[data-testid="login-form-username"]').type('[email protected]');
    cy.get('input[data-testid="login-form-password"]').type('password');
    cy.get('button[data-testid="login-form-submit"]').click();
    // Verify that it is redirected
    cy.url({ timeout: 3000 }).should('includes'.'/c/');
  });
});
Copy the code

Now, go to the Cypress application and select the test we just created. It should run all the tests in one file and we can see how they perform:

Let’s stop! Modify line 12 of the test:

cy.contains('Log In').click()
Copy the code

Add more use cases:

  • An unsuccessful login operation should generate an error message
  • Unauthorized users should not be able to access restricted web sites
describe('Log In', () => {
  it('succesfully performs login action', () => {... }); it('displays error message when login fails', () = > {// Go directly to the login path
    cy.visit('/login');
    // Attempt to log in with incorrect credentials
    cy.get('input[data-testid="login-form-username"]').type('[email protected]');
    cy.get('input[data-testid="login-form-password"]').type('fail_password');
    cy.get('button[data-testid="login-form-submit"]').click();
    // There should be an error message
    cy.contains('Something went wrong');
  });
  it('redirects unauthorized users', () = > {// Go to the protected path
    cy.visit('/c');
    // You should redirect to the login page
    cy.url().should('contains'.'/login');
  });
});
Copy the code

After we save the test file, Cypress should rerun all the tests:

Test the cancellation

The next feature to cover is the logout operation. We want to be sure that the user can be logged out of our application correctly. Sounds simple enough, right?

But let’s think again… In order to log out, we need to log in first, right? Should we reuse the code we tested earlier and then add more logic? It sounds silly, we’re developers, we can do better!

Cypress provides another handy feature — commands. It allows us to create custom actions that can be reused in any test. And since most scenarios should be written for login users, this action is a perfect candidate for custom commands.

Open the commands.js file in the support folder. Cypress has provided us with some examples that can be uncommented to use!

// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
Copy the code

Enhance this login command with our custom behavior, but first let’s think about what we want to do.

We’ve tested login, haven’t we? So, it doesn’t make sense to repeat the same steps for every test we write next. We can even read documents:

Fully test the login process – but only once!

The same:

Do not log in using your user interface before each test.

So what can we do?

We can use cy.request() to request a login directly to our back-end service and continue as usual. As follows:

Cypress.Commands.add('login', (email, password) => {
  // The backend sends a POST request
  // We are using GraphQL, so we are using a transformation:
  cy
    .request({
      url: 'http://localhost:4000/graphql'.method: 'POST'.body: {
        query:
          'mutation login($email: String! , $password: String!) {loginUser(email: $email, password: $password)}'.variables: { email, password },
      },
    })
    .then(resp= > {
      // Assert the response from the server
      expect(resp.status).to.eq(200);
      expect(resp.body).to.have.property('data');
      // All of our private paths check for the Auth token that exists on the Redux store, so let's pass it there
      window.localStorage.setItem(
        'reduxPersist:user'.JSON.stringify({ refreshToken: resp.body.data.loginUser })
      );
      // Go to the dashboard
      cy.visit('/c');
    });
});
Copy the code

Now, in each test, we can call cy.login (‘username’, ‘password’), and it should perform the login operation without using the UI.

Now we are ready to test the logout operation, create logout_spec.js and add some assertions:

const baseUrlMatcher = new RegExp('localhost:3000/$');

describe('Log out user properly', () = > {// Log in before each test:
  beforeEach((a)= > {
    cy.login('[email protected]'.'password');
  });
  it('can select dropdown and perform logout action', () = > {// Check if we are logged in:
    cy.url().should('contains'.'/c/');
    cy.get('div[data-testid="main-menu-settings"]').click();
    cy
      .get('.Popover-body ul li')
      .first()
      .click();
    cy.url().should('match', baseUrlMatcher);
  });
  it('/logout url should work as well', () => {
    cy.url().should('contains'.'/c/');
    cy.visit('/log-out');
    cy.url().should('match', baseUrlMatcher);
  });
  it('should clear auth token from local storage', () => {
    cy.url().should('contains'.'/c/');
    cy.visit('/logout');
    cy.url().should('match', baseUrlMatcher);
    const user = JSON.parse(window.localStorage.getItem('reduxPersist:user'));
    assert.isUndefined(user.token, 'refreshToken is undefined');
  });
});
Copy the code

Watch them fail:

first()
last()
cy.visit('log-out')
cy.visit('logout')

TL; DR

All in all, writing tests with Cypress is really fun.

As claimed, configuration is almost zero, assertions are easy to write, feel natural, and the GUI is great! You can time travel, debug all the steps, and since they all start as the Electron application, we even have access to developer tools to see what happens with each action.

The web has evolved, and testing will continue to do so.

Let’s write some tests, and may the Force be with you!


Pay attention to wechat public number: KnownsecFED, code to get more quality dry goods!