Testing is an integral part of software development, and it is important that all developers learn best testing practices. Jordan Powell, DX engineer at Cypress and Angular Google Developer Expert, has some tips to share with developers on how to write better tests using Cypress 10.
In this article, we will take a look at Powell’s tips for writing end-to-end tests, component tests, and advanced testing patterns in Cypress. If you want to learn more from Jordan Powell, please check out his Advanced Cypress JS Drop training.
Table of Contents
- Why is Testing important?
- Types of Testing
- A new take on the testing pyramid
- Differences between end-to-end testing and component testing
- Jordan Powell’s best practices for E2E testing
- Jordan Powell’s Best Practices for Component Testing
- Jordan Powell’s Advanced Cypress Patterns
- Conclusion
Why is Testing important?
Software testing identifies issues within the application, and helps ensure that only high quality products are shipped to the user.
Here are Jordan Powell’s reasons on why testing is important, and how it helps developers:
Documentation: Good testing coverage will result in developers creating stronger test plans and better QA testing for new releases.
Confidence: Writing good tests allows developers to build new features in confidence because the applications are working as intended.
Safe Refactoring: Good test coverage leads to less refactoring down the road, and provides developers more time to work on new features.
Improved UX: Good test coverage provides better UX (User Experience) for the end user because the application is working as intended.
Types of Testing
E2E, Integration, and Unit testing are three common methods for testing software applications.
Unit Test: Unit testing serves as the foundation of the test pyramid. If you're working in a functional language, a unit will most likely be a single function. Your unit tests will call a function with different parameters and ensure that it returns the expected values. In an object-oriented language, a unit can range from a single method to an entire class.
Integration test: Integration testing is where individual components of your application are tested as a group.
End-to-end testing: E2E Testing is a technique that tests your app from the web browser through to the back end of your application, as well as testing integrations with third-party APIs and services. These types of tests are great at making sure your entire app is functioning as a cohesive whole.
A new take on the testing pyramid
In Jordan Powell’s Advanced Cypress JS Drop training, he challenges developers to rethink the traditional testing pyramid, and believes that component testing should also be included.
Component Testing with Cypress 10 allows developers to test individual components quickly, regardless of its complexity. Component tests differ from end-to-end tests in that instead of visiting a URL to pull up an entire app, a component can be "mounted" and tested on its own.
Differences between end-to-end testing and component testing
Here are a few key differences between End-to-end and component testing:
End-to-end testing | Component testing |
---|---|
The entire application and all of its layers are tested | Only the independent components are tested |
Testing can be done by developers and QA Teams | Testing is done by the developers |
Often requires a complex setup | No extra configuration for CI(Continuous Integration) environments needed |
Initialization command: cy.visit(url) | Initialization command: cy.mount(<MyComponent>) |
Jordan Powell’s best practices for E2E testing
Don’t use HTML Native selectors
It is not good practice to use element selectors, id attributes, or class attributes when writing End-to-end tests. Using HTML Native selectors can lead to team members refactoring tests later on, or changing attributes which could affect the tests.
Jordan Powell recommends using data attributes that can inform the team that this is a test attribute. When other team members need to change anything, they will be aware of the function of the attribute and what it may affect.
Use Closures
Jordan warns against assigned return values for Cypress assertions, and recommends using closures to access the Commands yield like this:
cy.get('button').then((btn) => {
// store the button's
const txt = btn.text()
// submit a form
cy.get('form').submit()
// compare the two button's text
// and make sure they are different
cy.get('button').should((btn2) => {
expect(btn2.text()).not.to.eq(txt)
})
})
// these commands run after all of the
// other previous commands have finished
cy.get(...).find(...).should(...)
Independent Test
It is not good practice to combine related tests. Jordan Powell suggests that tests should be run independently from one another without sharing or relying on other test states. If test suites are related in any way, run the related properties or methods before each of the test runs, like in the code below:
describe('my form', () => {
beforeEach(() => {
cy.visit('/users/new')
cy.get('#firstname').type('Hassan')
cy.get('#lastname').type('Sani')
})
it('display form validation', () => {
// clear out the first name
cy.get('#firstname').clear()
cy.get('form').submit()
cy.get('#errors').should('contain', 'First name is required’)
})
it('can submit a valid form', () => {
cy.get('form').submit()
})
})
Use Route Aliases
Another recommended practice is to use route aliases to guard Cypress against proceeding until an explicit condition is met.
// route aliases
cy.intercept('**/api/users').as('usersRequest')
cy.wait('@usersRequest')
// assertions
cy.get('button').click()
cy.wait(4000) // <- this is unnecessary
cy.get('p').contains('hello world')
Setting a Global baseUrl
It is bad practice to use hardcoded URL strings for the baseUrl because it will eventually fail when the environment changes. Instead, Powell recommends using a config file that holds the environment baseUrl.
Jordan Powell’s Best Practices for Component Testing
All of the code examples in this next section will be for Angular, but Cypress component testing works for all major Frontend JavaScript frameworks.
Default Config Mount
Component testing takes two parameters: the component to mount, and the object for configuration.
A Custom Mount Config can boost flexibility. It ships a default mount config which you can use, but to manage multiple modules and import without adding too many boilerplate codes, it is recommended to have a custom mount config for specific instances.
import { HttpClientModule } from '@angular/common/http';
import { SharedModule } from 'libs/shared/shared';
import { mount } from 'cypress/angular';
type MountParams = Parameters<typeof mount>
Cypress.Commands.add('mount', (component: MountParams[0], config: MountParams[1] = {}) => {
return mount(component, {
...config,
imports: [
...config.imports,
HttpClientModule,
SharedModule
]
})
})
Cypress Intercept
Component testing handles the individual components you are testing at a particular time, to access external API it is recommended to use cy.intercept
. According to the Doc Cy.intercept can be used to passively listen for matching routes without manipulating the request or its response in any way.
it('should show bad login message when credentials are invalid', () => {
cy.intercept('POST', '/auth', {
statusCode: 401,
});
cy.mount('<app-login-form></app-login-form>');
cy.get('button').contains('Sign in').click();
cy.get('input[type=email]').type('bad@email.com');
cy.get('input[type=password]').type('badpass');
cy.get('button').contains('Sign in').click();
cy.contains('Invalid username or password');
});
Use createOutputSpy
When working with eventEmitter for components that may contain forms or other elements with events, createOutputSpy
will automatically create an EventEmitter
and set up the spy on it's .emit()
method.
it('should show bad login message when credentials are invalid', () => {
cy.intercept('POST', '/auth', {
statusCode: 401,
});
cy.mount('<app-login-form></app-login-form>',
componentProperties: {
onLogin: createOutputSpy('onLoginSpy'),
}
);
cy.get('button').contains('Sign in').click();
cy.get('input[type=email]').type('bad@email.com');
cy.get('input[type=password]').type('badpass');
cy.get('button').contains('Sign in').click();
cy.contains('Invalid username or password');
});
Jordan Powell’s Advanced Cypress Patterns
Session
When testing for routes or components that require authentication Cypress provides cy.session
which eliminates creating functions in your test to check for sessions on every call to the route or component. cy.session
caches the sessions, and it will skip the login process for testing components that need the sessions on a second call.
Cypress.Command.add("login", (username, password) => {
cy.session(
[username, password],
() => {
cy.request({
method: 'POST',
url: '/login',
body: {username, password},
}).then(({body}) => {
window.localStorage.setItem('authToken', body.token)
})
}
{
validate() {
cy.request('/whoami').its('status').should('eq', 200)
}
}
)
})
Origin
Running tests for projects that require a visit to multiple different domains could get CORS, or a limitation of visits, due to browser environment. Cypress provides Origin to help bypass some of the browser limitations.
const sentArgs = { username: 'username', password: 'password@254' }
cy.origin(
'supersecuredsite.com',
// Send the args here...
{ args: sentArgs },
// ...and receive them at the other end here!
({username, password}) => {
cy.visit('/login')
cy.get('input#username').type(username)
cy.get('input#password').type(password)
cy.contains('button', 'Login').click()
}
)
Conclusion
In this article, we looked at Jordan Powell’s tips for writing End-to-end tests, component tests, and advanced testing patterns in Cypress.
I recommend watching the full video for Jordan Powell’s Advanced Cypress JS Drop training.
Are you excited about the Cypress Component Testing? Let us know on Twitter.