Cypress.io
When testing is easy, developers build better things faster and with confidence.
@bahmutov
Questions via Sli.do #cygofundme
Recording at https://on.cypress.io/webinars
/in/twilliams15
GoFundMe Charity is a subscription-free fundraising platform with powerful tools to make fundraising easy for charities
5-hour overnight test runs (~1,000 tests)
Random 10% (~100) tests fail from flake
Brittle code
Poor coding/QA practices
Difficult to debug & update
Questions via Sli.do #cygofundme
https://www.atlassian.com/inside-atlassian/qa
While devs prepare for a new feature,
QA helps set up the tests.
Then we just fill in the blanks.
No problem, QA has you covered
QA helps create a habit by writing unfinished tests.
Red, Green, Refactor
Selenium isn't very fun
Questions via Sli.do #cygofundme
npm install cypress
describe('Sign up', () => {
it('Users can create a new account')
})
describe('Sign in', () => {
it('Users can log in as a charity')
it('Users can log in as a human')
})
Example
beforeEach(() => {
cy.visit('/signin/form')
})
describe('Sign up', () => {
it('Users can create a new account')
})
describe('Sign in', () => {
it('Users can log in as a charity')
it('Users can log in as a human')
})
beforeEach(() => {
cy.visit('/signin/form')
})
describe('Sign up', () => {
beforeEach(() => {
cy.contains('Sign up')
.click()
})
it('Users can create a new account')
})
describe('Sign in', () => {
it('Users can log in as a charity')
it('Users can log in as a human')
})
beforeEach(() => {
cy.visit('/signin/form')
})
describe('Sign up', () => {
beforeEach(() => {
cy.contains('Sign up')
.click()
})
it('Users can create a new account')
})
describe('Sign in', () => {
describe('Charity', () => {
let charityUser
before(() => {
cy.charitySetup()
.then(response => {
charityUser = response.body
})
})
it('Users can log in as a charity')
})
describe('Human', () => {
let humanUser
before(() => {
cy.humanSetup()
.then(response => {
humanUser = response.body
})
})
it('Users can log in as a human')
})
})
beforeEach(() => {
cy.visit('/signin/form')
})
describe('Sign up', () => {
beforeEach(() => {
cy.contains('Sign up')
.click()
})
it('Users can create a new account')
// enter email & password
// click submit
// assert 201 response from api
// assert success message in ui
})
describe('Sign in', () => {
describe('Charity', () => {
let charityUser
before(() => {
cy.charitySetup()
.then(response => {
charityUser = response.body
})
})
it('Users can log in as a charity')
// enter email & password
// click submit
// assert 200 response from api
// assert url is charity home screen
})
describe('Human', () => {
let humanUser
before(() => {
cy.humanSetup()
.then(response => {
humanUser = response.body
})
})
it('Users can log in as a human')
// enter email & password
// click submit
// assert 200 response from api
// assert url is human home screen
})
})
Questions via Sli.do #cygofundme
abstraction isn't necessary
it('uses page object model', () => {
login
.visit()
.enterEmail('tester@example.com')
.enterPassword('password')
.submit();
});
it('avoids page object model', () => {
cy.visit('/login');
cy.get('#email')
.type('tester@example.com')
cy.get('#password')
.type('password')
cy.get('#submit')
.click();
});
using POM
just use cy commands
$this->testUtility->enterText(
$this->donateLocators->first_name,
$this->donateEntity->getBillingFirstName()
);
$this->testUtility->enterText(
$this->donateLocators->last_name,
$this->donateEntity->getBillingLastName()
);
$this->testUtility->enterText(
$this->donateLocators->email,
$this->donateEntity->getUserEmail()
);
legacy codebase (php)
public function enterText($element, $text, $verifyText = true, $timeout = null)
{
$timeout = $this->setTimeout($timeout);
if (Driver::isMobileBrowser()) {
$foundElement = $this->waitForElementToAppear($element, $timeout);
} else {
$foundElement = $this->scrollToElement($element, $timeout);
}
$foundElement->clear();
$foundElement->sendKeys($text);
if ($verifyText && !(CONFIG['browser'] == 'FIREFOX' && CONFIG['useSaucelabs'])) {
$this->assertEquals($text, $foundElement->getAttribute("value"));
}
}
legacy codebase (php)
you can mock and stub!
$username = $entity_data['username'];
if ($entity_type == EntityTypes::PROJECT) {
$username .= '/' . $entity_data['project_username'];
} elseif ($entity_type == EntityTypes::FUNDRAISER) {
$entity_type = EntityTypes::PROJECT; // due to $entity_type being used in url below
}
$this->donateEntity->setAmount(100);
$this->donateEntity->setCharityId($entity_data['charity_id']);
$this->donateEntity->setFeeStructureType($entity_data['fee_structure_type']);
if ($this->donateEntity->getPaymentProcessor() == PaymentProcessors::PPGF) {
$this->donateEntity->setCardCvv('324');
} else {
$this->donateEntity->setCardNumber('4111111111111111');
}
if ($widget) {
$fees_data = $this->donateSupport->widgetFeeCalculations($entity_data['charity_id'], $entity_data['fee_id'], $this->donateEntity->getAmount());
} else {
$fees_data = $this->donateSupport->feeCalculations($entity_data['charity_id'], $this->donateEntity->getAmount());
}
it(`
Users can donate to a team:
* Logged out
* PPGF
* USD
* Includes Tip
* $25, one-time
`, () => {
cy.visit(`/donate/project/${fundraiser.team_slug}`);
// step 1
cy.contains('$25')
.click();
cy.contains('Continue')
.click();
// step 2: billing info
cy.get('#first_name')
.type('First');
cy.get('#last_name')
.type('Last');
cy.get('#email')
.type('testdonation@example.com');
cy.get('#address')
.type('123 Main St');
cy.get('#country')
.select('United States');
cy.get('#city')
.type('LA');
cy.get('#state')
.select('CA');
cy.get('#zip')
.type('12345');
// step 2: payment info
cy.get('#shown_card_number')
.type(getRandomPPGFTestCard());
cy.get('#exp_month')
.type('01');
cy.get('#exp_year')
.type('2030');
cy.get('#cvc')
.type('123');
cy.contains('DONATE')
.click();
// success
cy.url()
.should('include', '/share');
});
use before and beforeEach
and nest 'em!
build your own cy.getTestID
Questions via Sli.do #cygofundme
Cypress Best Practices
Cypress.Commands.add('getDataQA', (value) => {
return cy.get(`[data-qa="${value}"]`)
})
support/commands.js
it('works', () => {
cy.getDataQA('like-magic')
.click()
})
some.spec.js
use route aliases or assertions
cy.server()
.route({
method: 'GET',
url: '**/users/*',
})
.as('users')
// ...
cy.wait('@users')
// ...
cy.visit('/react-app')
.get('.loading')
.should('not.be.visible')
// ...
*
Questions via Sli.do #cygofundme
/in/twilliams15
By Cypress.io
GoFundMe Charity's subscription-free fundraising platform uses powerful tools, including donation buttons, data solutions, and custom campaigns to provide a world-class fundraising experience for charities. In our upcoming webcast, find out how their QA and developers use Cypress to ensure quality across multiple projects.
When testing is easy, developers build better things faster and with confidence.