We're back! Welcome once again. This is Day 4. As usual, if you're new, you should probably start on Day 0.
At the end of each session, you get to take with you a challenge that's designed to help solidify new concepts. Last session you were tasked with making an assertion on the response to POST /login
as well as making assertions on the Home page. If you were able to figure out how to circumvent the UI to authenticate, you get extra credit!
Asserting the contents of a request or its response are nearly the same.
To review, here's how to assert the request body:
cy.wait('@login')
.its('request.body')
.should('deep.equal', {
username: data.username,
password: data.password,
remember: true,
type: 'LOGIN'
})
Let's look at the response.body
yielded by cy.wait('@login')
by selecting the command in the Command Log.
{
"user": {
"id": "bDjUb4ir5O",
"uuid": "b4ebe114-141c-4397-9346-dd37d788f71b",
"firstName": "Kaylin",
"lastName": "Homenick",
"username": "Allie2",
"password": "$2a$10$5PXHGtcsckWtAprT5/JmluhR13f16BL8SIGhvAKNP.Dhxkt69FfzW",
"email": "Rebeca35@yahoo.com",
"phoneNumber": "072-208-4283",
"avatar": "https://api.adorable.io/avatars/128/bDjUb4ir5O.png",
"defaultPrivacyLevel": "private",
"balance": 164867,
"createdAt": "2019-09-15T04:44:05.536Z",
"modifiedAt": "2020-05-21T18:25:10.341Z"
}
}
Unlike the assertion for the request body, we just need to assert the username so we'll first get that and then assert that it equals the username that we set:
cy.wait('@login')
.its('response.body.user.username')
.should('equal', data.username)
The Take-home Challenge was different because you had to get creative about what should be tested on Home instead of just figuring out how to go about it. There's no right answer, but one way to approach is to split the interface - that is, everything we see on the /
route) - into a few large parts with smaller parts within them:
- the left navigation (side nav)
- name, handle, avatar
- balance
- navigation links
- logout - the top navigation (toolbar)
- nav toggle
- app title
- new transaction and notification buttons
- tabs - and the main content area with transactions
We could break down home.spec.js
thusly:
describe('home', () => {
describe('left navigation', () => {
it('name, handle, avatar', () => {
...
})
...
})
describe('toolbar and tabs', () => {
it('nav toggle', () => {
...
})
...
})
describe('transactions', () => {
...
})
})
Within those test (it
) blocks are the assertions like visibility (be.visible
), text content (contain.text
, have.text
), attributes (have.attr
), etc.
We discovered last week that signing in is required before each test. Doing so through the UI is the slowest and least reliable way to set the stage for our tests. To fix that, we first need to observe how our app authenticates.
We already know that our app makes the request, POST /login
to authenticate, but how does our app persist that authentication? If we insert a cy.pause()
after waiting for the request, we can start poking around.
Open Chrome DevTools and select the Application tab. Selecting our app's url, http://localhost:3000
, under Cookies reveals a connect.sid
cookie that wasn't there before we authenticated. We can safely assume that this is a session ID created by logging in.

We can come to the same conclusion by resuming the test and inspecting the headers in the response to POST /login
, where we'll find set-cookie
(which the browser uses to set the cookie for us) containing the same connect.sid
value.
While we're here, let's have a look at the response body:
{
"id": "dBjUb4ir5O",
"uuid": "a4ebe114-141d-4397-9347-dd37d788e71b",
"firstName": "Kaylin",
"lastName": "Homenick",
"username": "Allie2",
"password": "$2a$10$5PHXGtcsckWtAprT5/JmluhR13f16BL8SGIhvAKNP.Dhxkt96FfzW",
"email": "Rebeca35@yahoo.com",
"phoneNumber": "072-208-4283",
"avatar": "https://api.adorable.io/avatars/128/bDjUb4ir5O.png",
"defaultPrivacyLevel": "private",
"balance": 164867,
"createdAt": "2019-09-15T04:44:05.536Z",
"modifiedAt": "2020-05-21T18:25:10.341Z"
}
More on that in a bit...
Let's delete the cookie, click Bank Accounts
and check out the Command Log:

As expected the response statuses of subsequent requests is 401
(Not Authorized)
Back to the Application tab in DevTools, select our app's url under Local Storage. There we see authState

The observant ones in the room will point out that the values of context.user
and event.data.user
are both equal to the response body (from POST /login
) that we saw earlier.
Poking around the Real World App codebase confirms that an authentication service sets authState
like this:
window.localStorage.setItem('authState', JSON.stringify(authState))
How authState
plays a role here is out of the scope of this workshop but I encourage you to peruse these relevant files:
src/machines/authMachine.ts
src/components/SignInForm.tsx
backend/auth.ts
To summarize what we know at this point, our Real World App authenticates by making a request, POST /login
with our credentials and uses the response to persist that authentication. The cookie storage is handled by the browser and the app's authentication service stores authState
in Local Storage.
How do we circumvent the UI to do all this? First, with cy.request
.
cy.request
is used to make HTTP requests – like the ones our app is making. We simply pass an object containing method
(GET
, POST
, PUT
, DELETE
, etc.), url
and body
. We have all these values (or know where to get them) so our request is straightforward:
cy.request({
method: 'POST'
url: '/login' // Cypress prepends `baseUrl` for you
body: {
username: data.username,
password: data.password,
remember: true,
type: 'LOGIN'
}
})
According to the Cypress docs, the cy.request
command yields the response which is an object containing body
. Let's use it in our spec and observe the command's console output. We should expect to see the same response when we make the request as when the app makes it, containing user
in body
.
We can also expect that the cookie is set, but not authState
. That's next.
To access the response yielded we need .then
but we only need the user
object:
cy.request({
...
})
.its('body.user')
.then(user => {
// logic using `user`
})
We discovered earlier that the app sets Local Storage like this:
window.localStorage.setItem('authState', JSON.stringify(authState))
where authState
contains the user
object in context
and event
. So if we put it all together in a beforeEach
hook:
beforeEach('sign in', () => {
cy.request({
method: 'POST',
url: '/login',
body: {
type: 'LOGIN',
username: data.username,
password: data.password,
remember: true
},
})
.its('body.user')
.then(user => {
const authState = {
value: 'authorized',
context: { user }, // `user` is shorthand for `user: user`
_event: {},
event: {
data: { user }
}
}
window.localStorage.setItem('authState', JSON.stringify(authState))
})
cy.visit('/')
})
Let's consider Home tested for now and move on to testing My Account. Clicking on My Account from Home takes the user to a different route (/user/settings
) so the feature deserves its own spec - appropriately named my-account.spec.js
.
We can continue to assume the role of Product Manager or Stakeholder and make up our own requirements based on our own observations of the UI - in this case it's a form with a Save button:
[image]
It seems a logical test to start with would involve filling out the form and clicking Save.
Remember from last time, that we need to authenticate first. We could copy over the beforeEach
hook, but in the typical workshop fashion (that you should be used to by now!) we're going to find a better way.
Universal programming paradigms would suggest avoiding code repetition and improve readability and maintainability through abstraction. In modern JavaScript, it means creating a module that exports a function that can be imported wherever it's needed.
In Cypress, it gets a bit more fancy with custom commands. By importing your module in cypress/support/commands.js
and adding it with Cypress.Commands.add(name, callbackFn)
it becomes part of the global cy
namespace with all other Cypress commands.
To spice things up even more, before a spec is loaded, Cypress first loads cypress/support/index.js
. This yields many benefits, the first being we can import commands.js
so we don't have to import them into each spec and second being we can declare hooks - like our beforeEach
hook for authenticating - that will apply to all specs.
Take-home Challenge
- Create a new spec,
my-account.spec.js
- In it, create a function,
signIn
that takesusername
andpassword
and signs in the specified user via the API and persists authentication by settingauthState
- Call/invoke
signIn
from abeforeEach
- Write a test to update the user's email address to
kaylin.homenick@gmail.com
by visiting the appropriate route, filling out the form, clicking Save, and asserting the new value in the request made to the/users
endpoint. - Extra Credit: add the
signIn
function tocommands.js
and useCypress.Commands.add(name, callbackFn)
to create the custom command,cy.signIn
, to use in your new test - Extra Extra Credit: figure out a way to use
cy.signIn
in a globalbeforeEach
hook, passing in theusername
andpassword
set in Cypress Environment Configuration