更新
在我们从本课程开始之前,让我们为代码做一些更新:
1:在package.json
文件中,编辑运行命令为:
"cypress-headed": "cypress run --headed -b chrome"
这样做的原因是添加了一个头标志,因此我们可以在运行此终端命令时看到测试执行UI。
2:在包含书店应用中所有书籍的固定装置中添加新的固定文件books.json
:
{
"collection1": {
"Git": "Git Pocket Guide",
"DesignPatternsJS": "Learning JavaScript Design Patterns",
"API": "Designing Evolvable Web APIs with ASP.NET",
"SpeakingJS": "Speaking JavaScript",
"Don'tKnowJS": "You Don't Know JS",
"JSApps": "Programming JavaScript Applications",
"JSSecond": "Eloquent JavaScript, Second Edition",
"ECMA": "Understanding ECMAScript 6"
}
}
3:我为书店应用程序创建了一个用户,我们将用于今天的课程登录,所以让我们将第二个用户登录信息添加到我们的users.json
固定装置中。最终的users.json
固定装置现在应该看起来像:
{
"user1": {
"firstName": "Harvey",
"lastName": "Specter",
"age": "40",
"userEmail": "specter@example.com",
"salary": "700000",
"department": "legal"
},
"user2": {
"username": "repartner",
"password": "Test123456!"
}
}
命令
好,所以让我们首先从柏树中的命令开始。什么是命令?
柏树自定义命令由用户描述,而不是柏树的默认命令。这些自定义命令用于创建在自动化流中重复的测试步骤。
我们可以添加并覆盖已经存在的命令。它们应放在柏树项目中存在的支持文件夹中的commands.js
文件中。
因此,从本质上讲,此柏树功能使我们可以为可重复使用的代码创建自定义的柏树命令。我们想为在整个项目中全局重复的所有功能编写自定义命令。
目前,我提出了三个自定义命令,我们可以在测试中到处编写和使用,因此这些是全局的一般命令来处理我们将在几个地方重复的某些操作,但与任何特定页面并不严格相关我们的应用程序。
¹¸了解有关柏树自定义命令的更多信息:commands
1:打开commands.js
文件,然后向其添加这3个命令:
Cypress.Commands.add('verifyWindowAlertText', (alertText) => {
cy.once('window:alert', (str) => {
expect(str).to.equal(alertText);
});
});
Cypress.Commands.add('elementVisible', (locator) => {
cy.wrap(locator).each((index) => {
cy.get(index).then((el) => {
cy.get(el).should('be.visible');
});
});
});
Cypress.Commands.add('textExists', (text) => {
cy.wrap(text).each((index) => {
cy.contains(index).should('exist');
});
});
代码说明:
verifyWindowAlertText
命令将帮助我们在某些页面上验证警报文本。如果在测试中调用它,则将执行一次以获取单个警报。如果我们想在同一测试中验证其他一些警报,我们将再次称呼它。
elementVisible
命令将帮助我们存在多个元素的资产,因此我们不必分别为每个元素编写断言代码。我们将提供一系列元素,并调用此命令以检查所有元素。
textExists
与以前的命令类似,这将断言页面上存在一系列文本。
页面对象模式
页面对象模型是自动化世界中的设计模式,该模式以其测试维护方法而闻名并避免了代码重复。页面对象是代表Web应用程序中页面的类。在此模型下,总体Web应用程序分解为逻辑页面。 Web应用程序的每个页面通常对应于页面对象中的一个类,但甚至可以根据页面的分类来映射到多个类。此页面类将包含该网页的Web元素的所有定位器,还将包含可以在这些Web元素上执行操作的方法。
使用页面对象模式有什么好处?
我们已经看到,页面对象模式通过将代码分开为不同的类,还可以使测试脚本与定位器分开。考虑到这一点,页面对象模型的重要好处是:
- 代码可重用性 - 可以在不同的测试中使用同一页面类,并且所有定位器及其方法都可以在各种测试用例中重新使用。
- 代码可维护性 - 测试代码之间存在干净的分离,这可能是我们的功能场景和页面特定的代码,例如定位器和方法。因此,如果网页上发生了一些结构性更改,它将仅影响页面对象,并且不会对测试脚本产生任何影响。
ðâ€从企业项目的个人经验中,我可以给您一个建议 - 有时您必须测试非常复杂的Web应用程序的页面,具有多个大型功能,并且需要大量的测试用例。在这种情况下,您不会仅创建一个页面对象类来存储该页面的所有页面对象和方法。使代码更加可维护的方法是通过组件(功能)将页面对象类分开,而不是整个页面上充满复杂功能。这也是一种有效的策略,主要用于企业项目。
ð有趣的事实:这也是开发人员通常使用组件(前端)或微服务(后端)构建其代码的方式。这意味着将企业应用分解为具有大量较小功能/组件/服务等的较小项目。
所以,让我们弄脏双手,看看如何在演示应用中实施此策略。
我们将在Demoqa应用程序中使用书店应用程序。您可以在这里找到它:
在此书店应用程序中,我们有登录/注册页面,书店页面,您可以在其中找到所有书籍和个人资料页面,您可以在其中查看您的个人集合并进行注销。
因此,在现实生活中,在您开始进行任何自动化之前,您首先需要提出测试方案并为其编写文档。出于本课程的目的,我提出了一些情况:
如您所见,我们有一些重复的步骤,例如导航到登录页面,登录单,单击登录等。 P>
1:第一个,让我们称其为auth.js
,让我们写在那里的每个页面对象方法我们需要进行身份验证。
export class Auth {
login(user_name, password) {
cy.get('#userName').type(user_name);
cy.get('#password').type(password);
cy.get('#login').click();
}
}
export const auth = new Auth();
代码说明:
我们创建了一个类验证,以存储与登录,注册等相关的所有页面对象/方法,并且我们将该类导出为一个常数,因此我们可以在其他测试中使用它。
在类中,我们创建了一个正在将登录存储在步骤中的函数login
,因此我们可以在测试中以稍后调用此功能,而无需在测试本身中写入所有这些元素和操作。 login
功能正在使用参数,用户名和密码。
2:第二页对象文件,它可以是关于导航到不同页面的。这是一个组件心态的一个示例,有时您会在逻辑上创建一个页面对象,而与某个页面无关。让我们命名navigation.js
并在内部写下以下代码:
export class NavigateTo {
login() {
cy.visit('/login');
}
}
export const navigateTo = new NavigateTo();
代码说明:
我们创建了一个NavigateTo
类,以将所有导航操作存储在一个地方。对于第一组登录测试用例,我们只需要一个可以将我们导航到登录页面的功能。
现在我们拥有了验证和导航页面对象文件,并且我们还可以从之前添加的固定装置中使用现有用户(请参阅本课程的顶部),让S上述测试并将其翻译成自动化代码。
3:在E2E文件夹下创建书店文件夹,并创建名为login.cy.js
的文件。将以下代码放在其中:
/// <reference types="Cypress" />
import { auth } from '../../support/bookstore_page_objects/auth';
import { navigateTo } from '../../support/bookstore_page_objects/navigation';
describe('Auth: Login user', () => {
// Navigate to login page
beforeEach('Navigate to Login page', () => {
navigateTo.login();
});
it('Check valid user credentials', () => {
// Load users fixture
cy.fixture('users').then((users) => {
// Perform login
auth.login(users.user2.username, users.user2.password);
});
// Verify that user is redirected to profile page (user is logged in)
cy.url().should('contain', '/profile');
});
it('Check invalid user credentials', () => {
// Perform login
auth.login('invalid345', 'invalid345');
// Verify that user is still on login page (user is not logged in)
cy.url().should('contain', '/login');
// Verify that error message is displayed
cy.get('#output').should('contain', 'Invalid username or password!');
});
it('Check login with invalid username and valid password', () => {
// Load users fixture
cy.fixture('users').then((users) => {
// Perform login
auth.login('invalid345', users.user2.password);
});
// Verify that user is still on login page (user is not logged in)
cy.url().should('contain', '/login');
// Verify that error message is displayed
cy.get('#output').should('contain', 'Invalid username or password!');
});
it('Check login with valid username and invalid password', () => {
// Load users fixture
cy.fixture('users').then((users) => {
// Perform login
auth.login(users.user2.username, 'invalid345');
});
// Verify that user is still on login page (user is not logged in)
cy.url().should('contain', '/login');
// Verify that error message is displayed
cy.get('#output').should('contain', 'Invalid username or password!');
});
});
代码说明:
- 第3-4行:为了使用我们的页面对象和方法,我们需要在测试文件中导入它们。
- 第8-10行:在每次测试之前,我们将通过调用navigateto'页面对象类Constant的登录函数来导航到登录页面。
- 第12-20行:我们正在编写第一种情况的核心,以使用有效的凭据登录用户。为了使用我们的用户凭据,我们需要加载固定装置文件 - 行:14。然后,我们调用auth page对象和方法'登录并提供参数有效的用户名和有效密码固定装置文件。然后,第19行我们断言用户现在在个人资料页面上。
- 行22-57:我们本质上只是涵盖用户以与上述相同的方式输入有效凭据的其他方案,并且我们正在编写断言以验证用户仍在登录页面上,并且错误消息是显示。
ð - 非常重要:让您断言对象文件及其方法。如上所述,仅在测试文件中写断言。
因此,对于此测试案例,您可以看到我们有一些新的导航操作,我们在书店页面和个人资料页面上也有一些操作。
因此,我们需要将此操作添加到现有和新页面对象中。
1:首先,让我们添加导航方法到现有的navigation.js
页面对象:
export class NavigateTo {
login() {
cy.visit('/login');
}
profile() {
cy.visit('/profile');
}
bookStoreFromProfile() {
cy.get('#gotoStore').click();
}
}
export const navigateTo = new NavigateTo();
代码解释:
- 第6行:我们添加了新功能,该功能将通过URL导航到配置文件页面。
- 第10行:我们添加了新功能,可以通过单击按钮将用户导航到书店 - 转到书店
2:然后,让我们添加book_store.js
页面对象文件,从书店存储操作:
export class BookActions {
addBookToCollection(book_name) {
cy.contains(book_name).click();
cy.get('.text-right > #addNewRecordButton').click();
}
}
export const bookActions = new BookActions();
代码说明:
我们创建上一页对象的方式相同,对于此页面,我们正在创建一个带有函数addBookToCollection
的新版本,该函数将以书名作为参数,并将执行单击表中的书籍的动作,然后单击按钮以添加按钮预订收集。
3:然后,我们还需要与个人资料操作相关的页面对象文件,例如从个人收藏中删除书籍,所以让S创建profile.js
文件以存储其页面对象:
export class ProfileActions {
deleteBookFromTable(book_name, dialog_option) {
// Find delete button for certain book name and delete book from table
cy.get('.rt-tbody')
.contains('.rt-tr-group', book_name)
.then((row) => {
cy.wrap(row).find('#delete-record-undefined').click();
cy.get(`#closeSmallModal-${dialog_option}`).click();
});
}
}
export const profileActions = new ProfileActions();
代码说明:
与以前相同,我们为配置文件创建了新的页面对象类,我们创建了一个函数deleteBookFromTable
,该函数将从profile的用户表中处理删除book的删除(它将按书名搜索表,并且在该行中会找到n beac删除图标,然后单击它。然后,它将单击对话框选项删除/取消删除。
您可以看到此功能需要我们需要的两个参数,这是书籍名称和对话框选项('ok'/'cancel'),以作为确认对话框的元素ID的一部分提供。我们以这种方式可以重复使用删除书籍测试。
4:现在我们已经准备好所有页面对象和操作了,现在我们可以编写测试用例。在名为addBookToProfile.cy.js
的书店文件夹下创建新的测试用例,并在内部写下以下代码:
/// <reference types="Cypress" />
import { auth } from '../../support/bookstore_page_objects/auth';
import { bookActions } from '../../support/bookstore_page_objects/book_store';
import { profileActions } from '../../support/bookstore_page_objects/profile';
import { navigateTo } from '../../support/bookstore_page_objects/navigation';
describe('Collections: Add Book To Collection', () => {
// Perform login
beforeEach('Perform login', () => {
navigateTo.login();
cy.fixture('users').then((users) => {
auth.login(users.user2.username, users.user2.password);
});
});
// Delete the book from collection
afterEach('Delete book from profile collection', () => {
cy.fixture('books').then((books) => {
profileActions.deleteBookFromTable(books.collection1.Git, 'ok');
cy.verifyWindowAlertText(`Book deleted.`);
cy.get('.rt-tbody').should('not.contain', books.collection1.Git);
cy.get('.rt-noData').should('contain', 'No rows found').should('be.visible');
});
});
it('Check adding book to profile collection', () => {
// Navigate to book store
navigateTo.bookStoreFromProfile();
// Load books fixture
cy.fixture('books').then((books) => {
// Add first books to collection
bookActions.addBookToCollection(books.collection1.Git);
// Handle alert and verify alert message
cy.verifyWindowAlertText(`Book added to your collection.`);
// Navigate to user profile and verify that book is in collection table
navigateTo.profile();
cy.get('.rt-tbody').find('.rt-tr-group').first().should('contain', books.collection1.Git);
});
});
});
代码说明:
- 第3-6行:我们正在导入所有需要在此处使用的页面对象类。
- 第10-15行:先决条件:我们正在导航到登录页面并执行登录。请注意,我们如何使用页面对象来做到这一点,我们没有在测试案例中编写动作。
- 第18-25行:清理:我们正在从用户表中删除书籍并验证其已删除。每个测试案例后将执行AfterEach左右的清理工作,但通常是这样写的 - 在挂钩之前。然后,在定义前提条件和清理状态之后,我们正在编写实际的测试用例。
- 第27-40行:实际测试用例。我们正在从个人资料中导航到书店,我们正在加载固定装置,从收藏中添加一本书,我们正在验证警报是否使用适当的文本调用(使用以前写的自定义命令)该书添加到用户表中。
对于此测试案例,我们不需要其他页面对象,我们有现有的对象。因此,让我们将此测试用例转换为自动化案例:
1:创建名为deleteBookFromProfile.cy.js
的新测试文件,并在内部写下以下代码:
/// <reference types="Cypress" />
import { auth } from '../../support/bookstore_page_objects/auth';
import { bookActions } from '../../support/bookstore_page_objects/book_store';
import { profileActions } from '../../support/bookstore_page_objects/profile';
import { navigateTo } from '../../support/bookstore_page_objects/navigation';
describe('Collections: Delete Book From Collection', () => {
// Perform login
beforeEach('Perform login', () => {
navigateTo.login();
cy.fixture('users').then((users) => {
auth.login(users.user2.username, users.user2.password);
});
});
// Add book to collection
beforeEach('Add book to profile collection', () => {
navigateTo.bookStoreFromProfile();
cy.fixture('books').then((books) => {
bookActions.addBookToCollection(books.collection1.SpeakingJS);
cy.verifyWindowAlertText(`Book added to your collection.`);
});
});
// Delete the book from collection
after('Delete book from profile collection', () => {
cy.fixture('books').then((books) => {
profileActions.deleteBookFromTable(books.collection1.SpeakingJS, 'ok');
cy.verifyWindowAlertText(`Book deleted.`);
});
});
it('Check deleting book from profile collection - confirm deletion', () => {
cy.fixture('books').then((books) => {
// Navigate to user profile
navigateTo.profile();
// Check if book is in the collection table
cy.get('.rt-tbody')
.find('.rt-tr-group')
.first()
.should('contain', books.collection1.SpeakingJS);
// Delete book from table - confirm deletion
profileActions.deleteBookFromTable(books.collection1.SpeakingJS, 'ok');
// Handle delete alert and verify message
cy.verifyWindowAlertText(`Book deleted.`);
// Verify that book is no longer in collection table and that table is empty
cy.get('.rt-tbody').should('not.contain', books.collection1.SpeakingJS);
cy.get('.rt-noData').should('contain', 'No rows found').should('be.visible');
});
});
it('Check deleting book from profile collection - decline deletion', () => {
cy.fixture('books').then((books) => {
// Navigate to user profile
navigateTo.profile();
// Check if book is in the collection table
cy.get('.rt-tbody')
.find('.rt-tr-group')
.first()
.should('contain', books.collection1.SpeakingJS);
// Cancel book deletion
profileActions.deleteBookFromTable(books.collection1.SpeakingJS, 'cancel');
// Verify that book is still in the table
cy.get('.rt-tbody').should('contain', books.collection1.SpeakingJS);
});
});
});
代码说明:
- 第3-6行:我们正在导入所有需要的页面对象类
- 第10-15行:我们正在导航到登录页面并执行登录
- 第18-24行:我们正在收集中添加书籍(这也是前提条件)
- 第27-32行:在上次测试之后,我们将从Collection删除剩余的书
- 第34-51行:第一个测试用例,我们正在导航到配置文件,验证该书在表格中,我们正在单击删除图标并确认删除,我们正在检查警报,该书不再在表中。
- 第53-68行:第二个测试案例,类似于第一个,除了第55行,我们提供了取消参数以取消删除对话框,并且在我们验证书籍仍在表中。
1:对于此测试案例,我们需要在配置文件页面对象文件中添加一个操作(要单击用户表收集和打开详细信息中的书),因此现在看起来像这样:
export class ProfileActions {
deleteBookFromTable(book_name, dialog_option) {
// Find delete button for certain book name and delete book from table
cy.get('.rt-tbody')
.contains('.rt-tr-group', book_name)
.then((row) => {
cy.wrap(row).find('#delete-record-undefined').click();
cy.get(`#closeSmallModal-${dialog_option}`).click();
});
}
checkBookData(book_name) {
// Navigate to book info (open book from table)
cy.get('.rt-tbody').find('.rt-tr-group').first().contains(book_name).click();
}
}
export const profileActions = new ProfileActions();
代码说明:
您可以在第12行中看到,我们添加了新功能checkBookData
,该功能将以参数为参数,并且将执行用户表中的名称查找书并单击它的动作,以打开书籍详细信息。
2:有了这一切,我们可以将测试用例转换为自动化案例。创建一个新的测试文件并将其称为checkBookInfo.cy.js
.在内部编写以下代码:
/// <reference types="Cypress" />
import { auth } from '../../support/bookstore_page_objects/auth';
import { bookActions } from '../../support/bookstore_page_objects/book_store';
import { profileActions } from '../../support/bookstore_page_objects/profile';
import { navigateTo } from '../../support/bookstore_page_objects/navigation';
describe('Collections: Check Book Info', () => {
// Perform login
beforeEach('Perform login', () => {
navigateTo.login();
cy.fixture('users').then((users) => {
auth.login(users.user2.username, users.user2.password);
});
});
// Add book to book collection
beforeEach('Add book to profile collection', () => {
navigateTo.bookStoreFromProfile();
cy.fixture('books').then((books) => {
bookActions.addBookToCollection(books.collection1.DesignPatternsJS);
cy.verifyWindowAlertText(`Book added to your collection.`);
});
});
// Delete book from collection
afterEach('Delete book from profile collection', () => {
navigateTo.profile();
cy.fixture('books').then((books) => {
profileActions.deleteBookFromTable(books.collection1.DesignPatternsJS, 'ok');
cy.verifyWindowAlertText(`Book deleted.`);
});
});
it('Check book info from profile table', () => {
// Navigate to user profile
navigateTo.profile();
// Load books fixture
cy.fixture('books').then((books) => {
// Click on book in collection to open book info
profileActions.checkBookData(books.collection1.DesignPatternsJS);
});
// Define book info elements
const bookDataElements = [
'#ISBN-label',
'#title-label',
'#subtitle-label',
'#author-label',
'#publisher-label',
'#pages-label',
'#description-label',
'#website-label',
];
// Check book info elements
cy.elementVisible(bookDataElements);
// Define data about the book
const bookData = [
'9781449331818',
'Learning JavaScript Design Patterns',
`A JavaScript and jQuery Developer's Guide`,
'Addy Osmani',
`O'Reilly Media`,
'254',
];
// Check data about the book
cy.textExists(bookData);
});
});
代码说明:
- 第3-6行:我们正在导入此测试所需的所有页面对象类
- 第10-15行:前提条件 - 登录
- 第18-24行:前提条件 - 将书添加到用户集合
- 行27-33:清理/后条件 - 从用户表删除书
- 行35-67:我们的测试用例 - 我们正在导航到用户配置文件,并使用先前创建的方法打开书籍,我们正在单击表格中的书籍以打开详细信息。然后,我们从书籍详细信息和所有文本中存储了所有感兴趣的书籍详细信息中的所有元素,这些元素将我们介绍为两个数组,我们正在调用我们之前创建的自定义命令,以验证页面上是否存在所有元素和文本。
1:对于此测试用例,我们需要在book_store.js
页面对象内添加一个搜索书籍的功能。编写其他功能,以便现在您有内部:
export class BookActions {
addBookToCollection(book_name) {
cy.contains(book_name).click();
cy.get('.text-right > #addNewRecordButton').click();
}
searchCollection(book_name) {
cy.get('#searchBox').type(book_name);
}
}
export const bookActions = new BookActions();
代码说明:
在第7行上,我们添加了新功能searchCollection
,该功能将在书店的搜索框中输入我们在测试中提供的书名。
2:这样,我们可以创建测试用例searchBookstore.cy.js
并在其中编写此代码:
/// <reference types="Cypress" />
import { auth } from '../../support/bookstore_page_objects/auth';
import { bookActions } from '../../support/bookstore_page_objects/book_store';
import { navigateTo } from '../../support/bookstore_page_objects/navigation';
describe('Bookstore: Search For Book', () => {
// Perform login
beforeEach('Perform login', () => {
navigateTo.login();
cy.fixture('users').then((users) => {
auth.login(users.user2.username, users.user2.password);
});
});
it('Check searching for existing book in book store', () => {
// Navigate to bookstore
navigateTo.bookStoreFromProfile();
// Load books fixture
cy.fixture('books').then((books) => {
// Perform book search
bookActions.searchCollection(books.collection1.DesignPatternsJS);
// Verify that there is a book in filtered table (in search result)
cy.get('.rt-tbody')
.find('.rt-tr-group')
.first()
.should('contain', books.collection1.DesignPatternsJS);
});
});
it('Check searching for non-existing book in book store', () => {
// Define invalid book name
const invalid_book_name = 'Game of Thrones';
// Navigate to bookstore
navigateTo.bookStoreFromProfile();
// Perform book search
bookActions.searchCollection(invalid_book_name);
// Assert that there are no search results (no book in the table and table is empty)
cy.get('.rt-tbody').should('not.contain', invalid_book_name);
cy.get('.rt-noData').should('contain', 'No rows found').should('be.visible');
});
});
代码说明:
- 第3-5行:我们正在导入页面对象
- 第9-14行:我们正在做登录的前提
- 第16-29行:第一个测试案例,使用页面对象我们在书店的搜索框中键入,搜索书籍并断言我们找到了它
- 行31-41:第二个测试用例,与上述相同,但在这种情况下,我们定义了不存在的标题,并且正在检查该表不包含它。
1:对于此测试案例,我们将在Auth Page对象中添加新操作,以使我们可以注销。现在auth.js
页面对象文件应该看起来像这样:
export class Auth {
login(user_name, password) {
cy.get('#userName').type(user_name);
cy.get('#password').type(password);
cy.get('#login').click();
}
logout() {
cy.get('#submit').should('contain', 'Log out').click();
}
}
export const auth = new Auth();
代码说明:
在第7行上,我们添加了logout
功能,该功能将单击用户配置文件上的登录按钮,然后预成登录。
2:现在我们可以写出测试用例logout.cy.js
:
/// <reference types="Cypress" />
import { auth } from '../../support/bookstore_page_objects/auth';
import { navigateTo } from '../../support/bookstore_page_objects/navigation';
describe('Auth: Log out user', () => {
// Perform login
beforeEach('Perform login', () => {
navigateTo.login();
cy.fixture('users').then((users) => {
auth.login(users.user2.username, users.user2.password);
});
});
it('Check logging out user', () => {
// Assert that user is on profile page
cy.url().should('contain', '/profile');
// Perform log out
auth.logout();
// Assert that user is on login page
cy.url().should('contain', '/login');
});
});
代码说明:
与以前的测试用例相同,我们正在导入页面对象,编写先决条件,然后在测试中编写核心 - 在这种情况下,我们使用auth Page对象的注销操作来执行注销。当然,我们正在添加主张来验证用户是否实际记录。
家庭作业:
对于家庭作业,您可以在上述结构之后为书店添加更多方案。还可以从Internet来源中阅读有关页面对象的更多信息,有很多ð
不要忘记推动您今天在github-记得git命令上所做的一切吗?
git add .
git commit -am "add: reusability, book store tests"
git push
在第10课!
中见Completed code for this lesson
如果您学到了新知识,请随时购买咖啡来支持我的工作。