【翻译】基于 Cypress 测试 React 应用

原文连接:Testing React app with Cypresshtml

做者:Adam Trzciński前端

两周(原文发布于2017/11/6)之前,Cypress 开源了而且适用于任何人。react

Cypress 是一个工具,它使得你的端对端测试写起来更快。json

对浏览器中运行的任何内容进行快速,简单和可靠的测试。redux

让咱们来试一试,并验证这是真的!后端

咱们将把 Cypress 与咱们的项目之一--Eedi 集成在一块儿。 Eedi 是英国教师、学生及家长的绝佳教育平台。关键是,任何使用它的人,在浏览时都有愉快而流畅的体验,而且全部的功能都能按预期工做。浏览器

配置

在咱们应用程序的根目录下,让咱们添加 Cypress 做为 dev 依赖。bash

$ yarn add --dev cypress
复制代码

调整 package.json 中的 "scripts":服务器

"scripts": {
   ...
   "cypress:open": "cypress open"
}
复制代码

就像正常开发同样,在本地运行服务,而后在新的终端窗口中打开 Cypress:微信

$ yarn run cypress:open
复制代码

过了一下子,Cypress 应该打开了,咱们应该看到一个窗口弹出。

在这里,咱们能够访问咱们全部的测试,甚至开箱即用。

Cypress 已建立新的文件夹cypress 与子文件夹 fixtures, integrationsupport。它还添加了一个空配置文件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),码上获取更多优质干货!

相关文章
相关标签/搜索