Hola, Khabrovites. Para los futuros estudiantes del curso JavaScript QA Engineer, preparamos una traducción de material útil.
También invitamos a todos a participar en el seminario web abierto sobre el tema "Lo que un evaluador debe saber sobre JS". La lección considerará las características de JS que debe tener en cuenta todo el tiempo al escribir pruebas.
— . -, page objects, . , page objects — , . test runner Cypress.io, .
Page objects
Page Objects 1, 2 , . ad-hoc , , . , Selenium Wiki.
public class LoginPage {
private final WebDriver driver;
public LoginPage(WebDriver driver) {
this.driver = driver;
// Check that we're on the right page.
if (!"Login".equals(driver.getTitle())) {
// Alternatively, we could navigate to the
// login page, perhaps logging out first
throw new IllegalStateException("This is not the login page");
}
}
// The login page contains several HTML elements
// that will be represented as WebElements.
// The locators for these elements should only be defined once.
By usernameLocator = By.id("username");
By passwordLocator = By.id("password");
By loginButtonLocator = By.id("login");
// The login page allows the user to type their
// username into the username field
public LoginPage typeUsername(String username) {
// This is the only place that "knows" how to enter a username
driver.findElement(usernameLocator).sendKeys(username);
// Return the current page object as this action doesn't
// navigate to a page represented by another PageObject
return this;
}
// other methods
// - typePassword
// - submitLogin
// - submitLoginExpectingFailure
// - loginAs
}
Page Objects :
Page Objects:
public void testLogin() {
LoginPage login = new LoginPage(driver);
login.typeUsername('username')
login.typePassword('username')
login.submitLogin()
}
PageObjects API HTML. PageObjects HTML.
Tests
-----------------
Page Objects
~ ~ ~ ~ ~ ~ ~ ~ ~
HTML UI
-----------------
Application code
.
1. HTML .
2. HTML page objects .
3. PO
HTML UI , HTML- DOM — render - DOM one to one. HTML.
Page objects HTML , , ~ ~. , , - . , DOM , .
, , HTML , .
, page objects — , .
Page objects Cypress
Page Objects Cypress. “Deep diving PageObject pattern and using it with Cypress”. PageObject SignInPage LoginPage Selenium, .
class SignInPage {
visit() {
cy.visit('/signin');
}
getEmailError() {
return cy.get(`[data-testid=SignInEmailError]`);
}
getPasswordError() {
return cy.get(`[data-testid=SignInPasswordError]`);
}
fillEmail(value) {
const field = cy.get(`[data-testid=SignInEmailField]`);
field.clear();
field.type(value);
return this;
}
fillPassword(value) {
const field = cy.get(`[data-testid=SignInPasswordField]`);
field.clear();
field.type(value);
return this;
}
submit() {
const button = cy.get(`[data-testid=SignInSubmitButton]`);
button.click();
}
}
export default SignInPage;
“Home page”, SignInPage
Page Object.
import Header from './Headers';
import SignInPage from './SignIn';
class HomePage {
constructor() {
this.header = new Header();
}
visit() {
cy.visit('/');
}
getUserAvatar() {
return cy.get(`[data-testid=UserAvatar]`);
}
goToSignIn() {
const link = this.header.getSignInLink();
link.click();
const signIn = new SignInPage();
return signIn;
}
}
export default HomePage;
— PageObject
, , , object-oriented . :
import HomePage from '../elements/pages/HomePage';
describe('Sign In', () => {
it('should show an error message on empty input', () => {
const home = new HomePage();
home.visit();
const signIn = home.goToSignIn();
signIn.submit();
signIn.getEmailError()
.should('exist')
.contains('Email is required');
signIn
.getPasswordError()
.should('exist')
.contains('Password is required');
});
// more tests
});
Cypress JavaScript, , .
object-oriented PageObject. Cypress Custom Commands, . , “login”.
// in cypress/support/commands.js
Cypress.Commands.add('login', (username, password) => {
cy.get('#login-username').type(username)
cy.get('#login-password').type(password)
cy.get('#login').submit()
})
, , built-in
.
// cypress/integration/spec.js
it('logs in', () => {
cy.visit('/login')
cy.login('username', 'password')
})
, , JavaScript ( , check step .
// cypress/integration/util.js
export const login = (username, password) => {
cy.get('#login-username').type(username)
cy.get('#login-password').type(password)
cy.get('#login').submit()
}
// cypress/integration/spec.js
import { login } from './util'
it('logs in', () => {
cy.visit('/login')
login('username', 'password')
})
Page Objects
, PageObject
, .
! Page Objects, "App Actions", . , App Actions , .
TodoMVC. , todos. Cypress — , .
describe('TodoMVC', function () {
// set up these constants to match what TodoMVC does
let TODO_ITEM_ONE = 'buy some cheese'
let TODO_ITEM_TWO = 'feed the cat'
let TODO_ITEM_THREE = 'book a doctors appointment'
beforeEach(function () {
cy.visit('/')
})
context('New Todo', function () {
it('should allow me to add todo items', function () {
cy.get('.new-todo').type(TODO_ITEM_ONE).type('{enter}')
cy.get('.todo-list li').eq(0).find('label').should('contain', TODO_ITEM_ONE)
cy.get('.new-todo').type(TODO_ITEM_TWO).type('{enter}')
cy.get('.todo-list li').eq(1).find('label').should('contain', TODO_ITEM_TWO)
})
// more tests for adding items
// - adds items
// - should clear text input field when an item is added
// - should append new items to the bottom of the list
// - should trim text input
// - should show #main and #footer when items added
})
})
“New Todo” , <input class="new-todo" />
shortcuts. , .
“buy some cheese” ( ), , .
" " (“Mark all as completed”). , .
<input
className='toggle-all'
type='checkbox'
onChange={this.toggleAll}
checked={activeTodoCount === 0} />
— todo .toggle-all
? cy.createDefaultTodos().as('todos')
, UI , , .
// cypress/support/commands.js
const TODO_ITEM_ONE = 'buy some cheese'
const TODO_ITEM_TWO = 'feed the cat'
const TODO_ITEM_THREE = 'book a doctors appointment'
Cypress.Commands.add('createDefaultTodos', function () {
cy.get('.new-todo')
.type(`${TODO_ITEM_ONE}{enter}`)
.type(`${TODO_ITEM_TWO}{enter}`)
.type(`${TODO_ITEM_THREE}{enter}`)
.get('.todo-list li')
})
createDefaultTodos
.
// cypress/integration/spec.js
context('Mark all as completed', function () {
beforeEach(function () {
cy.createDefaultTodos().as('todos')
})
it('should allow me to mark all items as completed', function () {
// complete all todos
// we use 'check' instead of 'click'
// because that indicates our intention much clearer
cy.get('.toggle-all').check()
// get each todo li and ensure its class is 'completed'
cy.get('@todos').eq(0).should('have.class', 'completed')
cy.get('@todos').eq(1).should('have.class', 'completed')
cy.get('@todos').eq(2).should('have.class', 'completed')
})
// more tests
// - should allow me to clear the complete state of all items
// - complete all checkbox should update state when items are completed / cleared
})
:
UI — , "New Todo".
, .
— - . " " (“Mark all as completed”) 4 5 .
App Actions
Application Actions. , , Page Object App Actions repo bahmutov/test-todomvc-using-app-actions.
, , , . Cypress , . , , , , , window.
// app.jsx code
var model = new app.TodoModel('react-todos');
if (window.Cypress) {
window.model = model
}
model window model.addTodo
, js/todoModel.js
.
// js/todoModel.js
// Model: keeps all todos and has methods to act on them
app.TodoModel = function (key) {
this.key = key
this.todos = Utils.store(key)
this.onChanges = []
}
app.TodoModel.prototype.addTodo = function (title) {
this.todos = this.todos.concat({
id: Utils.uuid(),
title: title,
completed: false
});
this.inform();
};
app.TodoModel.prototype.inform = ...
app.TodoModel.prototype.toggleAll = ...
// other methods
, Page Object, todos
cy.createDefaultTodos().as('todos')
model.addTodo
, , “api” . cy.window() , , model
.invoke() , addTodo
.
beforeEach(function () {
cy.window().its('model').invoke('addTodo', TODO_ITEM_ONE)
cy.window().its('model').invoke('addTodo', TODO_ITEM_TWO)
cy.window().its('model').invoke('addTodo', TODO_ITEM_THREE)
cy.get('.todo-list li').as('todos')
})
— 1 , 3 , . , — Cypress , . TodoModel.prototype.addTodo
, .
// js/todoModel.js
app.TodoModel.prototype.addTodo = function (...titles) {
titles.forEach(title => {
this.todos = this.todos.concat({
id: Utils.uuid(),
title: title,
completed: false
});
})
this.inform();
};
// cypress/integration/spec.js
beforeEach(function () {
cy.window().its('model').invoke('addTodo',
TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE)
cy.get('.todo-list li').as('todos')
})
, ? , . , ! , , .
App Actions DevTools , "Your app", . .
app actions , . , cy
.
const addDefaultTodos = () => {
cy.window().its('model').invoke('addTodo',
TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE)
cy.get('.todo-list li').as('todos')
}
beforeEach(addDefaultTodos)
Cypress , addDefaultTodos
require import , spec-. addDefaultTodos
, JSDoc, .
// utils.js
const TODO_ITEM_ONE = 'buy some cheese'
const TODO_ITEM_TWO = 'feed the cat'
const TODO_ITEM_THREE = 'book a doctors appointment'
/**
* Creates default todo items using application action.
* @example
* import { addDefaultTodos } from './utils'
* beforeEach(addDefaultTodos)
*/
export const addDefaultTodos = () => {
cy.window().its('model').invoke('addTodo',
TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE)
cy.get('.todo-list li').as('todos')
}
app actions — JavaScript-, — .
TodoMVC, . , , . . Cypress .
context('Persistence', function () {
it('should persist its data', function () {
// mimicking TodoMVC tests
// by writing out this function
function testState () {
cy.get('@firstTodo').should('contain', TODO_ITEM_ONE)
.and('have.class', 'completed')
cy.get('@secondTodo').should('contain', TODO_ITEM_TWO)
.and('not.have.class', 'completed')
}
cy.createTodo(TODO_ITEM_ONE).as('firstTodo')
cy.createTodo(TODO_ITEM_TWO).as('secondTodo')
cy.get('@firstTodo').find('.toggle').check()
.then(testState)
.reload()
.then(testState)
})
})
testState
— , — . .
, , ? , ! , . Item — should allow me to mark items as complete
, :
context('Item', function () {
it('should allow me to mark items as complete', function () {
cy.createTodo(TODO_ITEM_ONE).as('firstTodo')
cy.createTodo(TODO_ITEM_TWO).as('secondTodo')
cy.get('@firstTodo').find('.toggle').check()
cy.get('@firstTodo').should('have.class', 'completed')
cy.get('@secondTodo').should('not.have.class', 'completed')
cy.get('@secondTodo').find('.toggle').check()
cy.get('@firstTodo').should('have.class', 'completed')
cy.get('@secondTodo').should('have.class', 'completed')
})
})
. , , , cypress-testing-library - , .
.
cy.createTodo(TODO_ITEM_ONE).as('firstTodo')
cy.createTodo(TODO_ITEM_TWO).as('secondTodo')
cy.get('@firstTodo').find('.toggle').check()
, , app actions. addTodo
, class="toggle"
, , «».
// spec.js
import { addTodos } from './utils';
addTodos(TODO_ITEM_ONE, TODO_ITEM_TWO)
cy.get('.todo-list li').eq(0).find('.toggle').check()
todoModel.js
, todo
.
app.TodoModel.prototype.toggle = function (todoToToggle) {
this.todos = this.todos.map(function (todo) {
return todo !== todoToToggle ?
todo :
Utils.extend({}, todo, {completed: !todo.completed});
});
this.inform();
};
model.toggle
, completed
? Cypress , DevTools. , , DevTools test runner
, “Your App” . , , model.toggle(model.todos[0])
«».
app actions toggle. , , , .
/**
* Toggle given todo item. Returns chain so you can attach more Cypress commands
* @param {number} k index of the todo item to toggle, 0 - first item
* @example
import { addTodos, toggle } from './utils'
it('completes an item', () => {
addTodos('first')
toggle(0)
})
*/
export const toggle = (k = 0) =>
cy.window().its('model')
.then(model => {
expect(k, 'check item index').to.be.lessThan(model.todos.length)
model.toggle(model.todos[k])
})
toggle , . , "" ?
, .
context('Persistence', function () {
// mimicking TodoMVC tests
// by writing out this function
function testState () {
cy.get('.todo-list li').eq(0)
.should('contain', TODO_ITEM_ONE).and('have.class', 'completed')
cy.get('.todo-list li').eq(1)
.should('contain', TODO_ITEM_TWO).and('not.have.class', 'completed')
}
it('should persist its data', function () {
addTodos(TODO_ITEM_ONE, TODO_ITEM_TWO)
toggle(0)
.then(testState)
.reload()
.then(testState)
})
})
cy.get('.todo-list li').eq(k).find('.toggle').check() toggle(k)
. .
, , , app actions. , — , "Active" !
context('Routing', function () {
beforeEach(addDefaultTodos) // app action
it('should allow me to display active items', function () {
toggle(1) // app action
// the UI feature we are actually testing - the "Active" link
cy.get('.filters').contains('Active').click()
cy.get('@todos').eq(0).should('contain', TODO_ITEM_ONE)
cy.get('@todos').eq(1).should('contain', TODO_ITEM_THREE)
})
// more tests
})
, , utility , , toggle, , , , , , !
// hmm, maybe we need to add a `model.toggleIndex()` method?
export const toggle = (k = 0) =>
cy.window().its('model')
.then(model => {
expect(k, 'check item index').to.be.lessThan(model.todos.length)
model.toggle(model.todos[k])
})
model.toggleIndex
, , , . .
DRY
. app actions. , , . . app actions . NEWTODO
TOGGLEALL
.
describe('TodoMVC', function () {
// testing item input
context('New Todo', function () {
// selector to enter new todo item is private to these tests
const NEW_TODO = '.new-todo'
it('should allow me to add todo items', function () {
cy.get(NEW_TODO)
.type(TODO_ITEM_ONE)
.type('{enter}')
// more commands
})
// more tests that use NEW_TODO selector
})
// testing toggling all items
context('Mark all as completed', function () {
// selector to toggle all items is private to these tests
const TOGGLE_ALL = '.toggle-all'
beforeEach(addDefaultTodos)
it('should allow me to mark all items as completed', function () {
cy.get(TOGGLE_ALL).check()
// more commands
})
// more tests that use TOGGLE_ALL selector
})
})
. , const NEWTODO = '.new-todo'
"New Todo", const TOGGLEALL = '.toggle-all'
"Mark all as completed". , - «» — app actions .
. , , Todo , . page objects
ALL_ITEMS
.
describe('TodoMVC', function () {
// common selector used across many tests
const ALL_ITEMS = '.todo-list li'
context('New Todo', function () {
const NEW_TODO = '.new-todo'
it('should allow me to add todo items', function () {
cy.get(NEW_TODO)
.type(TODO_ITEM_ONE)
.type('{enter}')
cy.get(ALL_ITEMS)
.eq(0)
.find('label')
.should('contain', TODO_ITEM_ONE)
})
// more tests
})
context('Mark all as completed', function () {
const TOGGLE_ALL = '.toggle-all'
beforeEach(addDefaultTodos)
it('should allow me to mark all items as completed', function () {
cy.get(TOGGLE_ALL).check()
cy.get(ALL_ITEMS)
.eq(0)
.should('have.class', 'completed')
})
// more tests
})
})
const ALL_ITEMS = '.todo-list li'
. utility allItems
, , .
describe('TodoMVC', function () {
const ALL_ITEMS = '.todo-list li'
/**
* Returns all todo items
*/
const allItems = () => cy.get(ALL_ITEMS)
context('New Todo', function () {
const NEW_TODO = '.new-todo'
it('should allow me to add todo items', function () {
cy.get(NEW_TODO)
.type(TODO_ITEM_ONE)
.type('{enter}')
allItems()
.eq(0)
.find('label')
.should('contain', TODO_ITEM_ONE)
})
// more tests
})
context('Mark all as completed', function () {
const TOGGLE_ALL = '.toggle-all'
beforeEach(addDefaultTodos)
it('should allow me to mark all items as completed', function () {
cy.get(TOGGLE_ALL).check()
allItems()
.eq(0)
.should('have.class', 'completed')
})
// more tests
})
})
utility allItems ALL_ITEMS allItems
specs , .
// cypress/integration/utils.js
const ALL_ITEMS = '.todo-list li'
/**
* Returns all todo items
* @example
import {allItems} from './utils'
allItems().should('not.exist')
*/
export const allItems = () => cy.get(ALL_ITEMS)
// cypress/integration/spec.js
import { allItems } from './utils'
describe('TodoMVC', function () {
context('New Todo', function () {
const NEW_TODO = '.new-todo'
it('should allow me to add todo items', function () {
cy.get(NEW_TODO)
.type(TODO_ITEM_ONE)
.type('{enter}')
allItems()
.eq(0)
.find('label')
.should('contain', TODO_ITEM_ONE)
})
// more tests
})
context('Mark all as completed', function () {
const TOGGLE_ALL = '.toggle-all'
beforeEach(addDefaultTodos)
it('should allow me to mark all items as completed', function () {
cy.get(TOGGLE_ALL).check()
allItems()
.eq(0)
.should('have.class', 'completed')
})
// more tests
})
})
, , - .
app actions . , . :
if (todos.length) {
main = (
<section className='main'>
<input
className='toggle-all'
type='checkbox'
onChange={this.toggleAll}
checked={activeTodoCount === 0}
/>
<ul className='todo-list'>{todoItems}</ul>
</section>
)
}
<input className='toggle-all' … />
, « » (“Mark all as completed”) .
, .
, Todo
. .
<input
className="toggle"
type="checkbox"
checked={this.props.todo.completed}
onChange={this.props.onToggle}
/>
onChange={this.props.onToggle}
:
<input
className='toggle'
type='checkbox'
checked={this.props.todo.completed}
// onChange={this.props.onToggle}
/>
.
, . «- UI — ».
TypeScript , . , , .
App Actions
app actions , . , todos , «».
// model
app.TodoModel.prototype.addTodo = function (...todos) {
// make XHR to the server to save todos
ajax({
method: 'POST',
url: '/todos',
data: todos
}).then(() =>
then update local state
this.saveTodos(todos)
).then(() =>
// this triggers DOM render
this.inform()
)
}
// spec.js
it('completes all items', () => {
addDefaultTodos()
toggle(1) // marks item completed
// click on "Completed" link
// assert there is 1 completed item
})
completes all items, , , , . , , .
, todos
addTodo
, toggle action
, todo 1. , , todo
— . — 1 .
, , . , . , , toggle(1)
. .
it('completes all items', () => {
addDefaultTodos()
allItems().should('have.length', 3)
toggle(1) // marks item completed
// click on "Completed" link
// assert there is 1 completed item
})
, — app action , app action, . , Cypress DOM , .
DOM — . , POST /todos XHR
, , toggle(1)
.
it('completes all items', () => {
cy.server()
cy.route('POST', '/todos').as('save')
addDefaultTodos()
cy.wait('@save') // waits for XHR POST /todos before test continues
toggle(1) // marks item completed
// click on "Completed" link
// assert there is 1 completed item
})
— ! model.inform
, , , app action.
it('completes all items', () => {
cy.window()
.its('model')
.then(model => {
cy.spy(model, 'inform').as('inform')
})
addDefaultTodos()
// wait until the spy is called once
cy.get('@inform').should('have.been.calledOnce')
toggle(1) // marks item completed
// click on "Completed" link
// assert there is 1 completed item
})
Cypress UI , , .
: app actions , . , - . , . :
DOM, .
XHR.
, .
-
. , Cypress Best Practices Brian Mann :
, ,
, , (, cy.request()), .
, cy.request
. , cy.request()
, app actions. — .
Page Objects, , App Actions, API , .
. TodoMVC, Cypress Electron, 34 , 17 App Actions — 50%.
. , .
. .
, , App Actions .
export const addTodos = (...todos) => {
cy.window().its('model').invoke('addTodo', ...todos)
}
( page objects), — , DevTools.
Application Actions. , Page Object App Actions repo bahmutov/test-todomvc-using-app-actions.
App Actions, . :
-
Dispatch Vuex actions DOM updates
Page Objects, App Actions .
«JavaScript QA Engineer».
« JS ».