Day 2

If you're new here, check out Day 0 first to get caught up.

Welcome to Day 2 everybody!

I hope the Take-home Challenge on Day 1 was not too easy or too difficult – but just right; that you were stretched just enough to solidify new concepts. And if they were too difficult – that's OK! I design them to be just barely doable for most of you, especially if you're just starting out. Let's review...

You were tasked with testing the Sign Up feature, by creating a new user through the form, signing in as that new user and then asserting that a dialog is visible.

If we apply the concepts we learned testing Sign In, we get this:

describe('sign up', () => {
  it('sign up a new user', () => {
    cy.visit('signup')
    cy.get('#firstName')
      .type('Mike')
    cy.get('#lastName')
      .type('C')
    cy.get('#username')
      .type('MikeC')
    cy.get('#password')
      .type('password')
    cy.get('#confirmPassword')
      .type('password')
    cy.contains('button', 'Sign Up')
      .click()
  })
})

We could click on the Sign Up link on Sign In, but in general we should visit the page under test. We can test that link some other way. We should similarly keep Sign In and Sign Up tests separate by creating a sign-up.spec.js file.

That signs up a user, but we also have to login as that new user. That's even easier since we're repeating the steps in the Sign In test:

describe('sign up', () => {
  it('sign up and login as new user', () => {
    ...
    cy.get('#username')
      .type('MikeC')
    cy.get('#password')
      .type('password')
    cy.contains('button', 'Sign In')
      .click()
  })
})
...

Lastly, you were tasked with asserting the url and the visibility of the Get Started with Real World App dialog.

describe('sign up', () => {
  it('sign up, login as new user, see dialog', () => {
	...
    cy.url()
      .should('equal', 'http://localhost:3000/')
    cy.contains('[role=dialog]', 'Get Started with Real World App')
      .should('be.visible')
  })
})

Currently, you can run this test over and over again and it doesn't fail despite the username being reused. As extra credit, you were challenged to Sign Up with a unique username. I have a way to do so without importing any new libraries or adding additional complexity:

Date.now()

That's right! Using the JavaScript Date object we can get the current Unix time which - with each passing second - is guaranteed never to be the same:

describe('sign up', () => {
  it('sign up and login as new user', () => {
    ...
    cy.get('#username')
      .type(Date.now())
    cy.get('#password')
      .type('password')
    cy.contains('button', 'Sign In')
      .click()
  })
})

Before we move on, let's get into the right branch and start the app:

$ git checkout Day-2
$ npm install
$ npm start

As our test suite scales, managing data can get hairy. For example, what if we wanted to change the password that we submit during sign up from password to s3cret? Sure, we can update the test:

describe('sign up', () => {
  it('sign up, login as new user, see dialog', () => {
    cy.visit('signup')
    cy.get('#firstName')
      .type('Mike')
    cy.get('#lastName')
      .type('C')
    cy.get('#username')
      .type(Date.now())
    cy.get('#password')
      .type('s3cret')
    cy.get('#confirmPassword')
      .type('s3cret')
    cy.contains('button', 'Sign Up')
      .click()
    cy.get('#username')
      .type('MikeC')
    cy.get('#password')
      .type('password')
    cy.contains('button', 'Sign In')
      .click()
    cy.url()
      .should('equal', 'http://localhost:3000/')
    cy.contains('[role=dialog]', 'Get Started with Real World App')
      .should('be.visible')
  })
})

Do you see the problem in the code? And will it pass or fail?

It passed! But can you spot the problem now?

Earlier we updated the username and password used to sign up a new user but we didn't update those values to sign in that new user. This is a trap because the test passes but it's no longer doing what we initially intended. We could get the test to fail if we were to reset the database to the default set of users.

A better solution is to use what I call the test data object. It's where all the test data lives:

describe('sign up', () => {
  const data = {
    firstName: 'Mike',
    lastName: 'C',
    username: 'MikeC',
    password: 'password',
    baseUrl: 'http://localhost:3000'
  }
  it('sign up, login as new user, see dialog', () => {
    cy.visit('signup')
    cy.get('#firstName')
      .type(data.firstName)
    cy.get('#lastName')
      .type(data.lastName)
    cy.get('#username')
      .type(data.username)
    cy.get('#password')
      .type(data.password)
    cy.get('#confirmPassword')
      .type(data.password)
    cy.contains('button', 'Sign Up')
      .click()
    cy.get('#username')
      .type(data.username)
    cy.get('#password')
      .type(data.password)
    cy.contains('button', 'Sign In')
      .click()
    cy.url()
      .should('equal', data.baseUrl + '/')
    cy.contains('[role=dialog]', 'Get Started with Real World App')
      .should('be.visible')
  })
})

Now all we have to do is update the data object:

  const data = {
    firstName: 'Mike',
    lastName: 'C',
    username: Date.now(),
    password: 's3cret',
    baseUrl: Cypress.config('baseUrl')
  }

Easy peasy! Notice I also updated baseUrl since that's defined our Cypress configuration. When that baseUrl is a hosted environment (not localhost) the test will still work without having to update it.


In our Sign Up test, when we click the Sign In button, the app makes a series of XHR requests to the backend API we're serving up with our app. It's not until after the app gets responses from those requests that the user is able to see the dialog.

But what if the backend fails to respond in time for the assertion on that dialog? Or what if one of the requests fails? Many times, content that we're asserting or trying to interact with depends on responses to XHR requests. With Cypress, we can reduce flakiness by preventing further action until the app gets responses from the requisite XHRs. First we need to create define routes with cy.route() and alias them with .as()

For example, to create an aliased route for the POST /users request is pretty straightforward:

cy.route('POST', '/users').as('createUser')

Let's add all aliased routes to the Sign Up spec. To gain control over XHRs requires starting the "server" with cy.server().

...
it('sign up, login as new user, see dialog', () => {
    cy.server()
    cy.route('POST', '/users')
      .as('createUser')
    cy.route('POST', '/login')
      .as('login')
    cy.route('GET', '/bankAccounts')
      .as('getBankAccounts')
    cy.route('GET', '/transactions/public')
      .as('getPublicTransactions')
    cy.route('GET', '/notifications')
      .as('getNotifications')
    ...
})
...

Notice how the aliases are displayed on the XHRs in the Command Log:

To wait on a request is equally effortless by passing the alias into cy.wait(). Just prepend it with @ like so:

cy.wait('@createUser')

We want the cy.wait command to appear right where the XHR appears in the command log:

Take-home Challenge

  1. Create a data object for the Sign In test
  2. Assert that we're on the right page (path) using cy.location instead of cy.url
  3. Assert that the sign up link on the sign in page works as expected
  4. Wait for all XHRs in both of our tests
  5. Read the Network Requests guide in the Cypress docs

GO TOP

🎉 You've successfully subscribed to iheartjs!
OK