原文链接:Testing React app with Cypress
两周(原文发布于2017/11/6)以前,Cypress 开源了并且适用于任何人。
Cypress 是一个工具,它使得你的端对端测试写起来更快。
对浏览器中运行的任何内容进行快速,简单和可靠的测试。
让我们来试一试,并验证这是真的!
我们将把 Cypress 与我们的项目之一--Eedi
集成在一起。 Eedi
是英国教师、学生及家长的绝佳教育平台。关键是,任何使用它的人,在浏览时都有愉快而流畅的体验,并且所有的功能都能按预期工作。
配置
在我们应用程序的根目录下,让我们添加 Cypress 作为 dev 依赖。
$ yarn add --dev cypress
调整 package.json
中的 "scripts":
"scripts": {
...
"cypress:open": "cypress open"
}
就像正常开发一样,在本地运行服务,然后在新的终端窗口中打开 Cypress:
$ yarn run cypress:open
过了一会儿,Cypress 应该打开了,我们应该看到一个窗口弹出。
在这里,我们可以访问我们所有的测试,甚至开箱即用。
Cypress 已创建新的文件夹cypress
与子文件夹 fixtures
, integration
和support
。它还添加了一个空配置文件cypress.json
。
由于我们经常访问我们的根路径,因此将它抽象为配置文件是一种很好的做法。打开cypress.json
文件,并添加一个带有键baseUrl
和 url 的新条目作为值:
{
"baseUrl": "http://localhost:3000"
}
在example_spec.js
文件中,我们可以看到'Kitchen Sink Tests',当我们想要浏览一些常见的测试场景时可以派上用场。但是让我们现在写我们自己的测试。
测试登录
登录是任何应用程序最重要的功能之一。如果做得不好,用户将无法看到我们其它的工作,并且再做其他事情就没有任何意义。
创建一个新文件login_spec.js
。在这里,我们将测试我们关于登录的所有逻辑。
让我们写下我们的第一个测试,让我们来检查一下 happy path 是否如预期一样工作:
describe('Log In', () => {
it('succesfully performs login action', () => {
// 访问 'baseUrl'
cy.visit('/');
// 断言我们是否处于好的位置 - 搜索'smarter world'
cy.contains('smarter world');
// 搜索带有 'Teachers' 的div, 并点击它
cy.get('a[data-testid="main-link-teachers"]').click();
// 检查url是否改变
cy.url().should('includes', 'teachers');
cy.contains('more time to teach');
// 找到Login按钮并点击它
cy.get('button[data-testid="menu-button-login"]').click();
// 检查url是否改变
cy.url().should('includes', '/login');
// 提交输入表单并点击提交按钮
cy.get('input[data-testid="login-form-username"]').type('test@email.com');
cy.get('input[data-testid="login-form-password"]').type('password');
cy.get('button[data-testid="login-form-submit"]').click();
// 验证是否被重定向
cy.url({ timeout: 3000 }).should('includes', '/c/');
});
});
现在,请转到 Cypress 应用程序并选择我们刚刚创建的测试。它应该在一个文件中运行所有的测试,我们可以看到它们的表现如何:
在测试运行器的左侧窗格中,我们可以看到 Cypress 执行的所有操作,查找到的元素以及浏览器重定向的元素。我们还可以使用漂亮的时间旅行功能,并检查我们测试的每一步。让我们停下来!修改测试的第12行:
cy.contains('Log In').click()
它失败了。这很好,我们已经确定 happy path 确实很 happy。Cypress 为我们提供了详细的堆栈跟踪 -- 发生了什么问题以及在哪里发生的问题。
添加更多的用例:
- 不成功的登录操作应该会产生错误消息
- 未经授权的用户应该无法访问受限制的网址
describe('Log In', () => {
it('succesfully performs login action', () => {
...
});
it('displays error message when login fails', () => {
// 直接转到登录路径
cy.visit('/login');
// 尝试使用不正确的凭证登录
cy.get('input[data-testid="login-form-username"]').type('test@email.com');
cy.get('input[data-testid="login-form-password"]').type('fail_password');
cy.get('button[data-testid="login-form-submit"]').click();
// 应该出现错误信息
cy.contains('Something went wrong');
});
it('redirects unauthorized users', () => {
// 转到受保护的路径
cy.visit('/c');
// 应该重定向到登录页面
cy.url().should('contains', '/login');
});
});
我们保存测试文件之后,Cypress应该重新运行所有的测试:
测试注销
下一个要覆盖的功能是注销操作。我们希望确定该用户可以正确地从我们的应用程序注销。听起来很简单,对吧?
但是,让我们再考虑一下...为了注销,我们需要先登录,对吧?我们是否应该重用先前测试的代码,然后再添加更多逻辑?听起来很傻,我们是开发者,我们可以做得更好!
Cypress 提供了另一个便利的功能 -- 命令。它允许我们创建可以在任何测试中重用的自定义操作。而且由于大多数场景应该为登录用户编写,因此此操作是自定义命令的完美候选。
打开位于support
文件夹中的commands.js
文件。 Cypress 为我们提供了一些示例,取消注释即可使用!
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
使用我们的自定义行为来增强此登录命令,但首先让我们考虑一下我们想要做什么。
我们已经测试了登录,不是吗?所以,我们接下来要写的每一个测试都是重复相同的步骤,是没有意义的。我们甚至可以阅读文档:
完全测试登录流程 - 但只有一次!
同样的:
在每次测试之前,请勿使用您的用户界面登录。
那我们能做什么呢?
我们可以使用cy.request()
直接向我们的后端服务请求登录,然后像往常一样继续。如下:
Cypress.Commands.add('login', (email, password) => {
// 向后端发出POST请求
// 我们正在使用GraphQL,因此我们正在通过转变:
cy
.request({
url: 'http://localhost:4000/graphql',
method: 'POST',
body: {
query:
'mutation login($email: String!, $password: String!) {loginUser(email: $email, password: $password)}',
variables: { email, password },
},
})
.then(resp => {
// 断言来自服务器的响应
expect(resp.status).to.eq(200);
expect(resp.body).to.have.property('data');
// 我们所有的private路径都会检查存在redux store上的auth token,所以让我们把它传递到那里
window.localStorage.setItem(
'reduxPersist:user',
JSON.stringify({ refreshToken: resp.body.data.loginUser })
);
// 到仪表盘
cy.visit('/c');
});
});
现在,在每个测试中,我们可以调用cy.login('username','password')
,并且它应该执行登录操作而不需要使用UI。
现在我们准备测试注销操作,创建logout_spec.js
并添加一些断言:
const baseUrlMatcher = new RegExp('localhost:3000/$');
describe('Log out user properly', () => {
// 在每次测试前登录:
beforeEach(() => {
cy.login('test@email.com', 'password');
});
it('can select dropdown and perform logout action', () => {
// 检查我们是否登录:
cy.url().should('contains', '/c/');
cy.get('div[data-testid="main-menu-settings"]').click();
cy
.get('.Popover-body ul li')
.first()
.click();
cy.url().should('match', baseUrlMatcher);
});
it('/logout url should work as well', () => {
cy.url().should('contains', '/c/');
cy.visit('/log-out');
cy.url().should('match', baseUrlMatcher);
});
it('should clear auth token from local storage', () => {
cy.url().should('contains', '/c/');
cy.visit('/logout');
cy.url().should('match', baseUrlMatcher);
const user = JSON.parse(window.localStorage.getItem('reduxPersist:user'));
assert.isUndefined(user.token, 'refreshToken is undefined');
});
});
观察它们失败:
然后修改第14行和第20行(将first()
更改为last()
,将cy.visit('log-out')
更改为cy.visit('logout')
并观察测试如何通过:
TL;DR
总之,用 Cypress 写测试真的很有趣。
正如所宣称的,配置几乎为零,编写断言很简单,感觉很自然,而且 GUI 非常棒!您可以进行时间旅行,调试所有步骤,并且因为它们都作为 Electron 应用程序启动,所以我们甚至可以访问开发者工具以了解每个动作发生了什么。
网络已经进化,测试依旧会如此。
让我们写一些测试吧,愿原力与你同在!
关注微信公众号:创宇前端(KnownsecFED),码上获取更多优质干货!