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

2,246 阅读6分钟

原文链接:Testing React app with Cypress

作者:Adam Trzciński

两周(原文发布于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, 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),码上获取更多优质干货!