精通-React-测试驱动开发第二版-七-

107 阅读29分钟

精通 React 测试驱动开发第二版(七)

原文:zh.annas-archive.org/md5/5e6d20182dc7eee4198d982cf82680c0

译者:飞龙

协议:CC BY-NC-SA 4.0

第十八章:由 Cucumber 测试引导的特性

在上一章中,我们学习了编写 Cucumber 测试的基本元素以及如何使用 Puppeteer 操作我们的 UI。但我们还没有探讨这些技术如何融入更广泛的开发生成过程。在本章中,我们将实现一个新的应用程序功能,但首先从 Cucumber 测试开始。这些将作为验收测试,我们的(虚构的)产品负责人可以使用它来确定软件是否按要求工作。

验收测试

验收测试是一个产品负责人或客户可以使用来决定是否接受交付的软件的测试。如果它通过,他们接受软件。如果它失败,开发者必须回去调整他们的工作。

我们可以使用术语**验收测试驱动开发(ATDD)**来指代一个整个团队都可以参与的测试工作流程。将其视为类似于 TDD,但它是在更广泛的团队层面上进行的,产品负责人和客户都参与其中。使用 Cucumber 编写 BDD 测试是将 ATDD 引入团队的一种方式——但不是唯一的方式。

在本章中,我们将使用我们的 BDD 风格的 Cucumber 测试作为我们的验收测试。

想象一下,我们的产品负责人已经看到了我们构建Spec Logo所做的出色工作。他们注意到共享屏幕功能很好,但还可以添加一些功能:它应该给演讲者提供在开始共享之前重置其状态的选择,如下所示:

图 18.1 – 新的共享对话框

图 18.1 – 新的共享对话框

产品负责人为我们提供了一些目前为红色以供实施的 Cucumber 测试——包括步骤定义和生成代码。

本章涵盖了以下主题:

  • 为对话框添加 Cucumber 测试

  • 通过测试驱动生产代码修复 Cucumber 测试

  • 避免在测试代码中使用超时

到本章结束时,你将看到更多关于 Cucumber 测试的示例以及它们如何作为团队工作流程的一部分被使用。你还将了解到如何避免在代码中使用特定的超时设置。

技术要求

本章的代码文件可以在以下位置找到:

github.com/PacktPublishing/Mastering-React-Test-Driven-Development-Second-Edition/tree/main/Chapter18

为对话框添加 Cucumber 测试

在本节中,我们将添加一个新的 Cucumber 测试,它目前不会通过。

让我们先看看这个新功能:

  1. 打开features/sharing.feature文件,看看你被给出的第一个特性。阅读步骤并尝试理解我们的产品负责人在描述什么。测试覆盖了很多行为——与我们的单元测试不同。它讲述了一个完整的故事:

    Scenario: Presenter chooses to reset current state when sharing
      Given the presenter navigated to the application page
      And the presenter entered the following instructions at the prompt:
        | forward 10 |
        | right 90 |
      And the presenter clicked the button 'startSharing'
      When the presenter clicks the button 'reset'
      And the observer navigates to the presenter's sharing link
      Then the observer should see no lines
      And the presenter should see no lines
      And the observer should see the turtle at x = 0, y = 0, angle = 0
      And the presenter should see the turtle at x = 0, y = 0, angle = 0
    
  2. 第一个the presenter navigated to the application page已经工作,如果你运行npx cucumber-js,你可以验证这一点。

  3. 下一个步骤 the presenter entered the following instructions at the prompt 与上一章的一个步骤非常相似。我们本可以选择在这里提取共性,就像我们处理 browseToPageFor 函数那样;然而,我们将等待测试和实现完成后再进行重构。现在,我们只是复制代码。打开 features/support/sharing.steps.js 并在代码底部添加以下步骤定义:

    When(
      "the presenter entered the following instructions at the prompt:",
      async function(dataTable) {
        for (let instruction of dataTable.raw()) {
          await this.getPage("presenter").type(
           "textarea",
           `${instruction}\n`
          );
          await this.getPage(
            "presenter"
          ).waitForTimeout(3500);
        }
      }
    );
    
  4. 接下来是 the presenter clicked the button 'startSharing'。在这之后出现的行是第一个 npx cucumber-js,你将获得这个函数的模板代码。将模板代码复制并粘贴到你的步骤定义文件中,如下面的代码块所示:

    When(
      "the presenter clicks the button {string}",
      function (string) {
        // Write code here that turns the phrase above
        // into concrete actions
        return "pending";
      }
    );
    

两个 When 语句

这个场景有两个 When 语句,这是不寻常的。就像你在 Act 阶段的单元测试一样,你通常只想有一个 When 语句。然而,由于此时有两个用户一起工作,为这两个用户有一个单一的操作是有意义的,所以我们将在这个场合让我们的产品所有者免责。

  1. 这个步骤定义与我们之前编写的非常相似。按照以下代码块所示填写函数。这里有一个新的 waitForSelector 调用。这个调用在我们继续之前等待按钮出现在页面上,这给了对话框渲染的时间:

    When(
      "the presenter clicks the button {string}",
      async function (
        buttonId
      ) {
        await this.getPage(
          "presenter"
        ).waitForSelector(`button#${buttonId}`);
        await this.getPage(
          "presenter"
        ).click(`button#${buttonId}`);
      }
    );
    
  2. 第二个 Then 子句。第一个是 the observer should see no lines;运行 npx cucumber-js 并复制 Cucumber 提供的模板函数,如下面的代码块所示:

    Then("the observer should see no lines", function () {
      // Write code here that turns the phrase above
      // into concrete actions
      return "pending";
    });
    
  3. 对于这个步骤,我们想要断言页面上没有线元素:

    Then(
      "the observer should see no lines",
      async function () {
        const numLines = await this.getPage(
          "observer"
        ).$$eval("line", lines => lines.length);
        expect(numLines).toEqual(0);
      }
    );
    
  4. 运行 npx cucumber-js,你应该会看到这个步骤通过了,下一个步骤非常相似。复制你刚才编写的步骤定义,并修改它以适用于演示者,如下面的代码块所示。同样,我们稍后可以清理重复的部分:

    Then(
      "the presenter should see no lines",
      async function () {
        const numLines = await this.getPage(
          "presenter"
        ).$$eval("line", lines => lines.length);
        expect(numLines).toEqual(0);
      }
    );
    
  5. 现在运行 Cucumber,你会看到这个步骤失败了;这是我们遇到的第一次失败。它指向我们需要在代码库中做出的具体更改:

    And the presenter should see no lines
       Error: expect(received).toEqual(expected)
       Expected value to equal:
       0
       Received:
       1
    
  6. 由于我们已经遇到了一个红色步骤,我们现在可以回过头来开始编写代码,使其变为绿色。然而,因为我们只有两个几乎相同的子句需要完成,我将选择在继续之前完成这些定义。Cucumber 告诉我们应使用哪个模板函数,所以现在添加如下:

    Then(
      "the observer should see the turtle at x = {int}, y = {int}, angle = {int}",
      function (int, int2, int3) {
        // Write code here that turns the phrase above
        // into concrete actions
        return "pending";
    });
    
  7. 我们需要定义几个辅助函数,可以告诉我们海龟当前的 xy 和角度值。我们需要这样做,因为我们只有 SVG polygon 元素,它使用 points 字符串和 transform 字符串来定位海龟。我们的辅助函数将把这些字符串转换回数字。作为提醒,以下是海龟初始的位置:

    <polygon
      points="-5,5, 0,-7, 5,5"
      fill="green"
      stroke-width="2"
      stroke="black"
      transform="rotate(90, 0, 0)" />
    

我们可以使用第一个points坐标来计算xy,通过将第一个数字加 5,从第二个数字减 5。角度可以通过将旋转的第一个参数减去 90 来计算。创建一个名为features/support/turtle.js的新文件,然后添加以下两个定义:

export const calculateTurtleXYFromPoints = points => {
  const firstComma = points.indexOf(",");
  const secondComma = points.indexOf(
    ",",
    firstComma + 1
  );
  return {
    x:
      parseFloat(
        points.substring(0, firstComma)
      ) + 5,
    y:
      parseFloat(
        points.substring(firstComma + 1, secondComma)
      ) - 5
  };
};
export const calculateTurtleAngleFromTransform = (
  transform 
) => {
  const firstParen = transform.indexOf("(");
  const firstComma = transform.indexOf(",");
  return (
    parseFloat(
      transform.substring(
        firstParen + 1, 
        firstComma
      )
    ) - 90
  );
}
  1. feature/sharing.steps.js中,更新步骤定义,如下面的代码块所示:

    Then(
      "the observer should see the turtle at x = {int}, y = {int}, angle = {int}",
      async function (
        expectedX, expectedY, expectedAngle
      ) {
        await this.getPage(
          "observer"
        ).waitForTimeout(4000);
        const turtle = await this.getPage(
          "observer"
        ).$eval(
          "polygon",
          polygon => ({
            points: polygon.getAttribute("points"),
            transform: polygon.getAttribute("transform")
          })
        );
        const position = calculateTurtleXYFromPoints(
          turtle.points
        );
        const angle = calculateTurtleAngleFromTransform(
          turtle.transform
        );
        expect(position.x).toBeCloseTo(expectedX);
        expect(position.y).toBeCloseTo(expectedY);
        expect(angle).toBeCloseTo(expectedAngle);
      }
    );
    
  2. 最后,按照以下方式为演示者重复此步骤定义:

    Then(
      "the presenter should see the turtle at x = {int}, y = {int}, angle = {int}",
      async function (
        expectedX, expectedY, expectedAngle
      ) {
        await this.getPage(
          "presenter"
        ).waitForTimeout(4000);
        const turtle = await this.getPage(
          "presenter"
        ).$eval(
          "polygon",
          polygon => ({
            points: polygon.getAttribute("points"),
            transform: polygon.getAttribute("transform")
          })
        );
        const position = calculateTurtleXYFromPoints(   
          turtle.points
        );
        const angle = calculateTurtleAngleFromTransform(
          turtle.transform
        );
        expect(position.x).toBeCloseTo(expectedX);
        expect(position.y).toBeCloseTo(expectedY);
        expect(angle).toBeCloseTo(expectedAngle);
      }
    );
    

那是第一个测试;现在,让我们继续到第二个场景:

  1. 我们第二个场景的大部分步骤定义已经实现;只有两个还没有:

      Then these lines should have been drawn for the observer:
        | x1 | y1 | x2 | y2 |
        | 0 | 0 | 10 | 0 |
      And these lines should have been drawn for the presenter:
        | x1 | y1 | x2 | y2 |
        | 0 | 0 | 10 | 0 |
    

我们已经在features/support/drawing.steps.js中有一个与这两个非常相似的步骤定义。让我们将这个逻辑提取到一个单独的模块中,这样我们就可以重用它。创建一个名为features/support/svg.js的新文件,然后从绘图步骤定义中复制以下代码:

import expect from "expect";
export const checkLinesFromDataTable = page =>
  return async function (dataTable) {
    await this.getPage(page).waitForTimeout(2000);
    const lines = await this.getPage(page).$$eval(
      "line",
      lines =>
        lines.map(line => ({
          x1: parseFloat(line.getAttribute("x1")),
          y1: parseFloat(line.getAttribute("y1")),
          x2: parseFloat(line.getAttribute("x2")),
          y2: parseFloat(line.getAttribute("y2"))
        }))
    );
    for (let i = 0; i < lines.length; ++i) {
      expect(lines[i].x1).toBeCloseTo(
        parseInt(dataTable.hashes()[i].x1)
      );
      expect(lines[i].y1).toBeCloseTo(
        parseInt(dataTable.hashes()[i].y1)
      );
      expect(lines[i].x2).toBeCloseTo(
        parseInt(dataTable.hashes()[i].x2)
      );
      expect(lines[i].y2).toBeCloseTo(
        parseInt(dataTable.hashes()[i].y2)
      );
    }
  };
  1. features/support/drawing.steps.js中,修改这些行应该已经被绘制步骤定义,使其现在使用此函数:

    import { checkLinesFromDataTable } from "./svg";
    Then(
      "these lines should have been drawn:",
      checkLinesFromDataTable("user")
    );
    
  2. 我们最新共享场景的两个新步骤定义现在很简单。在features/support/sharing.steps.js中,添加以下import语句和步骤定义:

    import { checkLinesFromDataTable } from "./svg";
    Then(
      "these lines should have been drawn for the presenter:",
      checkLinesFromDataTable("presenter")
    );
    Then(
      "these lines should have been drawn for the observer:",
      checkLinesFromDataTable("observer")
    );
    

您现在已经看到了如何编写较长的步骤定义以及如何将公共功能提取到支持函数中。

步骤定义完成后,是时候让这两个场景都通过了。

通过测试驱动生产代码来修复 Cucumber 测试

在本节中,我们将先进行一些初步设计,然后编写单元测试,以覆盖 Cucumber 测试的功能,然后使用这些测试来构建新的实现。

让我们进行一些初步设计:

  • 当用户点击开始共享时,应该弹出一个带有重置按钮的对话框。

  • 如果用户选择重置,Redux 存储将发送一个带有新reset属性设置为trueSTART_SHARING动作:

    { type: "START_SHARING", reset: true }
    
  • 如果用户选择共享他们的现有命令,则START_SHARING动作将带有reset设置为false发送:

    { type: "START_SHARING", reset: false }
    
  • 当用户点击RESET动作应该发送到 Redux 存储。

  • RESET动作发生之后,才应该开始共享。

那就是我们所需要的所有初步设计。让我们继续集成Dialog组件。

添加对话框

既然我们知道我们要构建什么,那就让我们开始吧!为此,执行以下步骤:

  1. 打开test/MenuButtons.test.js并跳过标题为当点击开始共享时触发 START_SHARING 动作的测试。我们暂时将这个连接断开。但我们会回来修复它:

    it.skip("dispatches an action of START_SHARING when start sharing is clicked", () => {
      ...
    });
    
  2. 在同一文件中,添加一个新的import语句用于Dialog组件,并使用jest.mock进行模拟。Dialog组件已经在代码库中存在,但直到现在还没有被使用:

    import { Dialog } from "../src/Dialog";
    jest.mock("../src/Dialog", () => ({
      Dialog: jest.fn(() => <div id="Dialog" />),
    });
    
  3. 在你跳过的测试下面添加这个新测试。非常简单,它检查在点击适当的按钮时显示对话框:

    it("opens a dialog when start sharing is clicked", () => {
      renderWithStore(<MenuButtons />);
      click(buttonWithLabel("Start sharing"));
      expect(Dialog).toBeCalled();
    });
    
  4. src/MenuButtons.js中,向 JSX 添加一个新的Dialog元素,包括文件顶部的import语句。新组件应放置在返回的 JSX 的底部。然后测试应该通过:

    import { Dialog } from "./Dialog";
    export const MenuButtons = () => {
      ...
      return (
        <>
          ...
         <Dialog />
        </>
      );
    };
    
  5. 接下来,让我们设置message属性以对用户更有用。将此添加到你的测试套件中:

    it("prints a useful message in the sharing dialog", () => {
      renderWithStore(<MenuButtons />);
      click(buttonWithLabel("Start sharing"));
      expect(propsOf(Dialog).message).toEqual(
        "Do you want to share your previous commands, or would you like to reset to a blank script?"
      );
    });
    
  6. 为了使这个测试通过,向你的实现添加message属性:

    <Dialog
      message="Do you want to share your previous commands, or would you like to reset to a blank script?"
    />
    
  7. 现在,我们需要确保在点击分享按钮之前不显示对话框;添加以下测试:

    it("does not initially show the dialog", () => {
      renderWithStore(<MenuButtons />);
      expect(Dialog).not.toBeCalled();
    });
    
  8. 通过添加一个新的状态变量isSharingDialogOpen来使这个测试通过。分享按钮在点击时将此设置为true。你需要在文件顶部添加useStateimport语句:

    import React, { useState } from "react";
    export const MenuButtons = () => {
      const [
        isSharingDialogOpen, setIsSharingDialogOpen
      ] = useState(false);
      const openSharingDialog = () =>
       setIsSharingDialogOpen(true);
      ...
      return (
        <>
          ...
          {environment.isSharing ? (
            <button
              id="stopSharing"
              onClick={() => dispatch(stopSharing())}
            >
              Stop sharing
            </button>
          ) : (
            <button
              id="startSharing"
              onClick={openSharingDialog}
            >
              Start sharing
            </button>
          )}
          {isSharingDialogOpen ? (
            <Dialog
              message="..."
            />
          ) : null}
        </>
      );
    };
    
  9. 现在,让我们添加一个测试来添加按钮到对话框。这是通过在Dialog组件上指定buttons属性来完成的:

    it("passes Share and Reset buttons to the dialog", () => {
      renderWithStore(<MenuButtons />);
      click(buttonWithLabel("Start sharing"));
      expect(propsOf(Dialog).buttons).toEqual([
        { id: "keep", text: "Share previous" },
        { id: "reset", text: "Reset" }
      ]);
    });
    
  10. 通过向Dialog组件添加buttons属性来使这个测试通过,如下所示:

    {isSharingDialogOpen ? (
      <Dialog
        message="..."
        buttons={[
         { id: "keep", text: "Share previous" },
         { id: "reset", text: "Reset" }
        ]}
      />
    ) : null}
    
  11. 对于下一个测试,我们将测试对话框是否关闭。首先在你的测试套件中定义一个新的closeDialog辅助函数:

    const closeDialog = () =>
      act(() => propsOf(Dialog).onClose());
    
  12. 添加下一个测试,检查一旦对话框调用了onClose属性,Dialog组件就会消失:

    it("closes the dialog when the onClose prop is called", () => {
      renderWithStore(<MenuButtons />);
      click(buttonWithLabel("Start sharing"));
      closeDialog();
      expect(element("#dialog")).toBeNull();
    });
    
  13. 通过在Dialog JSX 中添加以下行来使这个测试通过:

    <Dialog
      onClose={() => setIsSharingDialogOpen(false)}
      ...
    />
    
  14. 现在回到你跳过的测试,并修改它,使其与以下代码块相同。我们将修改START_SHARING Redux 动作以接受一个新的reset布尔变量:

    const makeDialogChoice = button =>
      act(() => propsOf(Dialog).onChoose(button));
    it("dispatches an action of START_SHARING when dialog onChoose prop is invoked with reset", () => {
      renderWithStore(<MenuButtons />);
      click(buttonWithLabel("Start sharing"));
      makeDialogChoice("reset");
      return expectRedux(store)
        .toDispatchAnAction()
        .matching({ type: "START_SHARING", reset: true });
    });
    
  15. 为了使这个测试通过,转到src/MenuButtons.js并修改startSharing函数,为创建的 Redux 动作添加一个reset属性。注意我们目前将值硬编码为true——我们将在即将到来的测试中进行三角测量:

    const startSharing = () => ({
      type: "START_SHARING",
      reset: true,
    });
    

测试中的三角测量

查看第一章**,使用测试驱动开发的第一步,以了解三角测量的提醒以及为什么我们要这样做。

  1. MenuButtons组件中,设置Dialog组件的onChoose属性:

    return (
      <>
        ...
        {isSharingDialogOpen ? (
          <Dialog
            onClose={() => setIsSharingDialogOpen(false)}
            onChoose={() => dispatch(startSharing())}
            ...
          />
        ) : null}
      </>
    );
    
  2. 最后,我们需要添加一个新的测试,用于发送false值给reset动作属性:

    it("dispatches an action of START_SHARING when dialog onChoose prop is invoked with share", () => {
      renderWithStore(<MenuButtons />);
      click(buttonWithLabel("Start sharing"));
      makeDialogChoice("share");
      return expectRedux(store)
        .toDispatchAnAction()
        .matching({
          type: "START_SHARING",
          reset: false
        });
    });
    
  3. 为了使这个测试通过,修改startSharing以接受一个button参数,然后使用它来设置reset属性:

    const startSharing = (button) => ({
      type: "START_SHARING",
      reset: button === "reset",
    });
    
  4. 然后,最后,在MenuButtons组件 JSX 中,设置Dialog元素的onChoose属性:

    onChoose={(button) => dispatch(startSharing(button))}
    

你现在已经完成了 Cucumber 测试中指定的第一个新功能。有一个对话框正在显示,并且一个reset布尔标志正在通过 Redux 存储发送。我们正在逐步接近一个可工作的解决方案。

更新 sagas 到重置或回放状态

现在,我们需要更新分享 saga 以处理新的重置标志:

  1. 打开test/middleware/sharingSagas.test.js,并在START_SHARING嵌套describe块的末尾添加以下测试:

    it("puts an action of RESET if reset is true", async () => {
      store.dispatch({
        type: "START_SHARING",
        reset: true,
      });
      await notifySocketOpened();
      await sendSocketMessage({
        type: "UNKNOWN",
        id: 123,
      });
      return expectRedux(store)
        .toDispatchAnAction()
        .matching({ type: "RESET" });
    });
    
  2. src/middleware/sharingSagas.js中修改startSharing,使其与以下代码块相同。别忘了将新的action参数添加到第一行:

    function* startSharing(action) {
      ...
      if (action.reset) {
        yield put({ type: "RESET" });
      }
    }
    
  3. 现在是棘手的第二个测试。如果resetfalse,我们希望重新播放所有当前的动作:

    it("shares all existing actions if reset is false", async () => {
      const forward10 = {
        type: "SUBMIT_EDIT_LINE",
        text: "forward 10",
      };
      const right90 = {
        type: "SUBMIT_EDIT_LINE",
        text: "right 90"
      };
      store.dispatch(forward10);
      store.dispatch(right90);
      store.dispatch({
        type: "START_SHARING",
        reset: false,
      });
      await notifySocketOpened();
      await sendSocketMessage({
        type: "UNKNOWN",
        id: 123,
      });
      expect(sendSpy).toBeCalledWith(
        JSON.stringify({
          type: "NEW_ACTION",
          innerAction: forward10,
        })
      );
      expect(sendSpy).toBeCalledWith(
        JSON.stringify({
          type: "NEW_ACTION",
          innerAction: right90
        })
      );
    });
    
  4. 要使这通过,我们可以使用export命名空间中的toInstructions函数。我们还需要使用两个新的redux-saga函数:selectallselect函数用于检索状态,而all函数与yield一起使用,以确保在继续之前等待所有传递的调用完成。现在将那些import语句添加到src/middleware/sharingSagas.js中:

    import {
      call,
      put,
      takeLatest,
      take,
      all,
      select
    } from "redux-saga/effects";
    import { eventChannel, END } from "redux-saga";
    import { toInstructions } from "../language/export";
    
  5. 现在,通过在条件语句后面添加一个else块来修改startSharing函数。

    if (action.reset) {
      yield put({ type: "RESET" });
    } else {
      const state = yield select(state => state.script);
      const instructions = toInstructions(state);
      yield all(
        instructions.map(instruction =>
          call(shareNewAction, {
            innerAction: {
              type: "SUBMIT_EDIT_LINE",
              text: instruction
            }
          })
        )
      );
    }
    
  6. 如果你现在运行测试,你会注意到有几个无关的失败。我们可以通过在我们的测试中的startSharing辅助方法中为reset属性添加一个默认值来修复这些问题:

    const startSharing = async () => {
      store.dispatch({
        type: "START_SHARING",
        reset: true
      });
      ...
    };
    

这样就完成了功能;单元测试和 Cucumber 测试都应该通过。现在手动尝试一下也是个不错的选择。

在下一节中,我们将专注于重构我们的 Cucumber 测试,使它们运行得更快。

避免测试代码中的超时

在本节中,我们将通过用waitForSelector调用替换waitForTimeout调用,来提高我们的 Cucumber 测试运行的速度。

我们的大多数步骤定义都包含等待,在等待动画完成的同时暂停我们的测试脚本与浏览器的交互。以下是我们测试中的一个示例,它等待了 3 秒钟:

await this.getPage("user").waitForTimeout(3000);

不仅这个超时会减慢测试套件,这种等待方式也很脆弱,因为可能存在超时稍微太短而动画尚未完成的情况。在这种情况下,测试将间歇性失败。相反,等待期实际上相当长。随着更多测试的添加,超时累积,测试运行突然变得非常慢。

避免超时

无论自动化测试的类型如何,避免在测试代码中使用超时都是一个好主意。超时将显著增加运行测试套件所需的时间。几乎总是有方法可以避免使用它们,就像本节中突出显示的那样。

我们可以做的替代方案是修改我们的生产代码,在元素动画时通知我们,通过设置一个isAnimating类。然后我们使用 Puppeteer 的waitForSelector函数来检查这个类值的改变,完全替换waitForTimeout

添加 HTML 类以标记动画状态

我们这样做是通过在动画运行时给 viewport 的div元素添加一个isAnimating类。

让我们从在Drawing元素准备好动画一个新 Logo 命令时添加isAnimating类开始:

  1. test/Drawing.test.js中,在主Display上下文中的重置上下文下方添加一个新的嵌套describe块。然后,添加以下测试:

    describe("isAnimating", () => {
      it("adds isAnimating class to viewport when animation begins", () => {
        renderWithStore(<Drawing />, {
          script: { drawCommands: [horizontalLine] }
        });
        triggerRequestAnimationFrame(0);
        expect(
          element("#viewport")
        ).toHaveClass("isAnimating");
      });
    });
    
  2. src/Drawing.js中,更新 JSX 以在viewport元素上包含这个类名:

    return (
      <div
        id="viewport"
        className="isAnimating"
      >
        ...
      </div>
    );
    
  3. 让我们进行三角测量,以便将这个状态变量放在合适的位置。为此,添加以下测试:

    it("initially does not have the isAnimating class set", () => {
      renderWithStore(<Drawing />, {
        script: { drawCommands: [] }
      });
      expect(
        element("#viewport")
      ).not.toHaveClass("isAnimating");
    });
    
  4. 为了使这个测试通过,将className更新为仅在commandToAnimate不为 null 时设置isAnimating

    className={commandToAnimate ? "isAnimating" : ""}>
    
  5. 作为最后的点缀,我们将添加一个可能不必要的测试。我们想要在动画完成后小心地移除isAnimating类。然而,我们的实现已经处理了这个问题,因为当发生这种情况时,commandToAnimate将被设置为undefined。换句话说,我们不需要为此进行显式的测试,这个添加就完成了。然而,为了完整性,你可以添加这个测试:

    it("removes isAnimating class when animation is finished", () => {
      renderWithStore(<Drawing />, {
        script: { drawCommands: [horizontalLine] },
      });
      triggerAnimationSequence([0, 500]);
      expect(element("#viewport")).not.toHaveClass(
        "isAnimating"
      );
    });
    

完成了添加isAnimating类功能。现在我们可以使用这个类作为替换waitForTimeout调用的手段。

更新步骤定义以使用 waitForSelector

我们已经准备好在我们的步骤定义中使用这种新行为,引入一个新的waitForSelector调用,等待元素上的isAnimating类出现(或消失):

  1. features/support/world.js中,向World类添加以下两个方法。第一个方法等待isAnimating选择器在 DOM 中出现,第二个方法等待它消失:

    waitForAnimationToBegin(page) {
      return this.getPage(page).waitForSelector(
        ".isAnimating"
      );
    }
    waitForAnimationToEnd(page) {
      return this.getPage(page).waitForSelector(
        ".isAnimating",
       { hidden: true }
      );
    }
    
  2. features/support/drawing.steps.js中,搜索这个文件中的单个waitForTimeout调用,并将其替换为以下代码块:

    When(
      "the user enters the following instructions at the prompt:",
      async function (dataTable) {
        for (let instruction of dataTable.raw()) {
          await this.getPage("user").type(
            "textarea",
            `${instruction}\n`
          );
          await this.waitForAnimationToEnd("user");
        }
      }
    );
    

注意类转换

我们在每个指令输入后等待动画。这很重要,因为它反映了isAnimating类将在应用程序中添加和删除的方式。如果我们只有一个waitForAnimationToEnd函数作为页面上的最后一个指令,那么如果在一系列指令的中间捕获到isAnimating类的移除,而不是捕获最后一个,我们可能会提前退出步骤定义。

  1. 现在,打开features/support/sharing.steps.js;这个文件中有一个与上一个类似的步骤,所以现在以相同的方式更新它:

    When(
      "the presenter entered the following instructions at the prompt:",
      async function(dataTable) {
        for (let instruction of dataTable.raw()) {
          await this.getPage("presenter").type(
            "textarea",
            `${instruction}\n`
          );
          await this.waitForAnimationToEnd("presenter");
        }
      }
    );
    
  2. 在文件底部,更新检查海龟位置的两个步骤定义:

    Then(
      "the observer should see the turtle at x = {int}, y = {int}, angle = {int}",
      async function (
        expectedX, expectedY, expectedAngle
      ) {
        await this.waitForAnimationToEnd("observer");
        ...
      }
    );
    Then(
      "the presenter should see the turtle at x = {int}, y = {int}, angle = {int}",
      async function (
        expectedX, expectedY, expectedAngle
      ) {
        await this.waitForAnimationToEnd("presenter");
        ...
      }
    );
    
  3. 打开features/support/svg.js并更新其中的函数,如下所示:

    export const checkLinesFromDataTable = page => {
      return async function (dataTable) {
        await this.waitForAnimationToEnd(page);
        ...
      }
    };
    
  4. 如果你现在运行npx cucumber-js,你会看到我们有一个测试失败,这与观察者的屏幕输出有关。它表明我们需要在加载观察者页面时等待动画。在这种情况下,我们需要在等待动画开始之前等待动画结束。我们可以通过向功能添加一个新的步骤来修复这个问题。打开features/sharing.feature并修改最后一个测试,在When部分包含一个第三个条目:

    When the presenter clicks the button 'keep'
    And the observer navigates to the presenter's sharing link
    And the observer waits for animations to finish
    

封装多个 When 子句

如果您对有三个When子句不满意,那么您总是可以将它们合并为一个单独的步骤。

  1. features/support/sharing.steps.js中,在其他的When步骤定义之下添加这个新的步骤定义:

    When(
      "the observer waits for animations to finish",
      async function () {
        await this.waitForAnimationToBegin("observer");
        await this.waitForAnimationToEnd("observer");
      }
    );
    

您的测试现在应该通过了,并且它们应该运行得更快。在我的机器上,它们现在只需要之前四分之一的时间。

摘要

在本章中,我们探讨了如何将 Cucumber 集成到您团队的日常工作流程中。

您看到了一些 Cucumber 测试与单元测试不同的方式。您还学习了如何避免使用超时来保持测试套件快速运行。

我们现在已经完成了对Spec Logo世界的探索。

在本书的最后一章,我们将探讨 TDD 与其他开发者流程的比较。

练习

尽可能地从您的步骤定义中移除重复内容。

第十九章:在更广泛的测试领域中理解 TDD

除了测试驱动开发的机制之外,本书还涉及了一些关于 TDD 实践者心态的想法:何时何地“作弊”,系统重构,严格的 TDD 等等。

一些开发团队喜欢采用“快速行动,打破事物”的口号。TDD 则相反:放慢速度,深思熟虑。为了理解在实践中这意味着什么,我们可以将 TDD 与各种其他流行的测试技术进行比较。

本章将涵盖以下主题:

  • 测试驱动开发作为一种测试技术

  • 手动测试

  • 自动化测试

  • 完全不进行测试

到本章结束时,你应该对为什么以及如何与其他编程实践相比,我们实践 TDD(测试驱动开发)有一个很好的理解。

测试驱动开发作为一种测试技术

TDD 实践者有时喜欢说,TDD 不是关于测试;而是关于设计、行为或规范,而我们最终拥有的自动化测试只是一个额外的好处。

是的,TDD 是关于设计的,但 TDD 当然也是关于测试的。TDD 实践者关心他们的软件具有高质量,这与测试人员关心的是同一件事。

有时,人们会质疑 TDD 的命名,因为他们觉得“测试”这个概念混淆了实际的过程。原因在于开发者误解了构建“测试”的含义。典型的单元测试工具实际上几乎不提供如何编写良好测试的指导。结果证明,将测试重新构造成规范和示例是向开发者介绍测试的好方法。

所有自动化测试都很难编写。有时,我们会忘记编写重要的测试,或者构建脆弱的测试,编写宽松的期望,过度复杂化解决方案,忘记重构,等等。

不仅新手会遇到这个问题——每个人都会,包括专家。人们经常一团糟。这也是乐趣的一部分。发现 TDD 的乐趣需要一定的谦卑,并接受你大多数时候不会编写完美的测试套件。完美的测试套件确实非常罕见。

如果你很幸运,你的团队里有测试人员,你可能会认为 TDD 侵犯了他们的工作,甚至可能让他们失业。然而,如果你询问他们的意见,你无疑会发现他们非常希望开发者对他们的工作质量感兴趣。有了 TDD,你可以自己捕捉到所有那些微不足道的逻辑错误,而不需要依赖他人的手动测试。然后测试人员可以更好地利用他们的时间,专注于测试复杂用例和寻找遗漏的需求。

单元测试的最佳实践

以下是一些优秀的单元测试:

  • 独立:每个测试应该只测试一件事,并只调用一个单元。我们可以采用许多技术来实现这一目标。仅举两个例子,协作者通常(但不总是)被模拟,示例数据应该是正确描述测试所需的最小数据集。

经典主义者与模拟主义者 TDD

你可能听说过伟大的 TDD 辩论,即经典主义者模拟主义者的 TDD。其想法是,经典主义者不会使用模拟和存根,而模拟主义者会模拟所有协作者。在现实中,这两种技术都很重要。你在本书中已经看到了它们的使用。我鼓励你不要局限于单一的方法,而是实验并学会对两者都感到舒适。

  • 简短,高度抽象:测试描述应该简洁。测试代码应突出显示对测试重要的所有代码片段,并隐藏任何所需但不相关的设备。

  • 快速运行:使用测试替身而不是与系统资源(文件、网络连接等)或其他进程交互。不要在代码中使用超时,或依赖时间的流逝。

  • 专注于可观察的行为:系统对外部世界的影响才是有趣的,而不是它如何做到这一点。在 React 的情况下,我们关注 DOM 交互。

  • 分为三部分:这些部分是安排行动断言,也称为AAA模式。每个测试都应该遵循这个结构。

  • 不要重复自己DRY):始终花时间重构和清理你的测试,目标是可读性。

  • 设计工具:优秀的测试帮助你弄清楚如何设计你的系统。这并不是说前置设计不重要。在本书的几乎每一章中,我们在开始测试之前都进行了一些设计。做一些思考,这样你就有了一个大致的方向。只是不要试图计划得太远,并且准备好在前进过程中完全放弃你的设计。

TDD 不是优秀设计的替代品。要成为一名优秀的 TDD 实践者,你还应该了解并练习软件设计。关于软件设计有许多书籍。不要局限于关于 JavaScript 或 TypeScript 的书籍;优秀的设计超越语言。

提高你的技术

以下是一些改进的一般性建议:

  • 与他人合作:除了阅读这本书之外,提高 TDD 水平的最佳方式是与专家合作。由于 TDD 非常适合结对和团队编程,它可以给不同经验水平的团队提供结构。经验丰富的开发者可以使用小型测试的粒度来帮助提高经验不足的开发者的工作。

  • 实验设计:TDD 为你提供了一个安全网,让你可以实验程序的风格和形状。利用这个安全网来了解更多关于设计的信息。你的测试会保护你。

  • 学会放慢速度:TDD 需要大量的个人自律。不幸的是,没有余地可以马虎。你绝对不能走捷径;相反,要抓住每一个机会进行重构。一旦测试通过,就坐下来审视你的代码。在继续下一个测试之前,仔细看看你的当前解决方案,并认真思考它是否是最好的。

  • 不要害怕推迟设计决策:有时,我们面临几个设计选择,知道选择哪个选项可能很棘手。即使是命名变量这样的简单行为也可能很困难。拥有设计感的一部分是知道何时推迟你的思考。如果你处于重构阶段,并发现自己正在权衡两个或更多选项,那就继续前进,添加另一个测试,然后回过头来审视你的设计。你通常会发现自己有更多的设计知识,并且更接近正确答案。

  • 每天解决一个 kata:kata 是一种短期的练习,旨在反复练习以教授你某种技术。两个基本的 kata 是 硬币兑换器罗马数字。更复杂的 kata 包括保龄球 kata、银行 kata 和康威的 生命游戏。钻石 kata 是我最喜欢的,还有排序算法。

  • 参加编码 retreat编码 retreat 涉及一天的对偶编程和 TDD,围绕 生命游戏 kata 展开。全球编码 retreat 日 在 11 月举行。来自世界各地的团队聚集在一起解决这个问题。这不仅有趣,而且是扩展你的 TDD 视野的好方法。

这涵盖了关于 TDD 的一般建议。接下来,让我们看看手动测试技术。

手动测试

如你所猜到的,手动测试意味着启动你的应用程序并实际使用它。

由于你的软件是你的创造性作品,自然地,你很想知道它的表现如何。你当然应该花时间做这件事,但把它视为休息和放松的机会,而不是你开发过程的一个正式部分。

使用 软件相比,开发 软件的缺点是使用它需要花费大量时间。听起来很傻,但指向、点击和输入都占用了宝贵的时间。此外,设置测试环境并准备好相关测试数据也需要时间。

因此,尽可能避免手动测试是很重要的。然而,在某些情况下,它是必要的,正如我们将在本节中发现的那样。

总是会有一种诱惑,在每一个特性完成后手动测试软件,只是为了验证它是否工作。如果你发现自己经常这样做,考虑一下你对单元测试的信心有多大。

如果你声称,“我对我的单元测试有 100% 的信心”,那你为什么还需要 使用 你的软件来证明它呢?

让我们看看一些具体的手动测试类型,从展示软件开始。

展示软件

至少有两个重要场合你应该始终手动测试:当你向客户和用户展示你的软件时,以及当你准备展示你的软件时。

准备意味着写下一份演示脚本,列出你想要执行的所有操作。在实际演示之前,至少练习你的脚本两遍。很多时候,排练会带来对脚本的修改,这就是为什么排练如此重要的原因。在正式演示之前,一定要确保你已经至少进行了一次不需要修改的完整演练。

测试整个产品

前端开发包括很多移动部件,包括以下内容:

  • 需要支持的多个浏览器环境

  • CSS

  • 分布式组件,如代理和缓存

  • 认证机制

由于所有这些移动部件的交互,手动测试是必要的。我们需要检查所有部件是否能够很好地组合在一起。

或者,你可以使用端到端测试来达到相同的覆盖率;然而,这些测试的开发和维护成本也很高。

探索性测试

探索性测试是你希望你的 QA 团队做的事情。如果你没有与 QA 团队合作,你应该分配时间自己来做这件事。探索性测试涉及探索软件并寻找团队尚未考虑的缺失需求或复杂用例。

由于 TDD 在非常低的层面上工作,很容易错过或甚至误解需求。你的单元测试可能覆盖了 95%的情况,但你可能会不小心忘记剩下的 5%。当团队刚开始使用 TDD,或者由新手程序员组成时,这种情况经常发生。即使是经验丰富的 TDD 实践者,也会发生这种情况——即使是那些写 TDD 书籍的人!我们都会时不时犯错误。

一个非常常见的错误场景涉及到模拟。当一个类或函数签名发生变化时,该类或函数的任何模拟也必须更新。这一步经常被遗忘;单元测试仍然通过,错误只有在实际运行应用程序时才会被发现。

无 bug 的软件

TDD 可以给你更多的信心,但绝对没有保证 TDD 能保证无 bug 的软件。

随着时间和经验的积累,你将更擅长在它们到达 QA 团队之前发现所有那些讨厌的边缘情况。

探索性测试的替代方案是自动化验收测试,但就像端到端测试一样,这些测试的开发和维护成本很高,而且它们还要求有高水平的专业知识和团队纪律。

浏览器中的调试

调试总是耗时巨大。这可能是一种极其令人沮丧的经历,伴随着大量的焦虑。这就是我们进行测试驱动开发的一个主要原因:这样我们就永远不需要调试。我们的测试为我们做了调试。

相反,TDD 的一个缺点是,你的调试技能可能会退化。

对于 TDD 实践者来说,从理论上讲,调试应该是一个非常罕见的情况,或者至少是积极避免的情况。但总有需要调试的情况。

打印行调试是一种调试技术,其中代码库中充满了console.log语句,希望它们可以提供有关运行时错误的线索。我与许多程序员合作过,他们职业生涯的开始是 TDD;对于他们中的许多人来说,打印行调试是他们所知道的唯一调试形式。尽管这是一个简单的技术,但它也很耗时,涉及大量的尝试和错误,您完成工作后必须记得清理。有可能会忘记一个多余的console.log,然后它在生产环境中生效。

现代浏览器具有非常复杂的调试工具,直到最近,这些工具只能在“全功能”IDE(如 Visual Studio 或 IntelliJ)中想象得到。您应该抽出时间来了解所有标准的调试技术,包括设置断点(包括条件断点)、进入、退出和跳过、监视变量等等。

一个常见的反模式是使用调试技术来追踪一个错误,一旦发现,就修复它并继续下一个任务。您应该做的是编写一个失败的测试来证明错误的存在。就像魔法一样,测试已经为您完成了调试。然后,您可以修复错误,并且立即,测试会告诉您问题是否已修复,而无需您手动重新测试。想想您将节省多少时间!

查看进一步阅读部分,获取有关 Chrome 调试器的资源。

这涵盖了您将执行的主要手动测试类型。接下来,让我们看看自动化测试技术。

自动化测试

TDD 是一种自动化测试形式。本节列出了其他一些流行的自动化测试类型,以及它们与 TDD 的比较。

集成测试

这些测试检查两个或更多独立进程之间的交互。这些进程可以是同一台机器上的,也可以分布在网络中。然而,您的系统应该使用与生产环境中相同的通信机制,因此如果它向一个网络服务发出 HTTP 调用,那么它应该在您的集成测试中这样做,无论该网络服务在哪里运行。

集成测试应该使用与单元测试相同的单元测试框架编写,所有关于编写良好单元测试的规则都适用于集成测试。

集成测试中最棘手的部分是编排代码,这涉及到启动和停止进程,以及等待进程完成其工作。可靠地执行这些操作可能很困难。

如果你选择在单元测试中模拟对象,当你不模拟这些交互时,你需要至少一些对这些交互的覆盖,集成测试是这样做的一种方式。另一种方式是系统测试,如以下所述。

系统测试和端到端测试

这些是自动化测试,它们通过驱动 UI 来测试整个系统,通常(但不一定)是通过驱动 UI 来实现的。

当手动探索性测试开始占用过多时间时,它们是有用的。这种情况发生在代码库随着规模和年龄的增长而增长。

端到端测试的建设和维护成本很高。幸运的是,它们可以逐步引入,这样你就可以从小规模开始,证明它们的价值,然后再扩大其范围。

接受测试

接受测试是由客户或代表客户的代理(如产品负责人)编写的,其中“接受”指的是必须通过的质量关卡,以便发布的软件被视为完整。它们可能是自动化的,也可能不是,并且它们在系统级别指定行为。

客户应该如何编写这些测试?对于自动化测试,你通常可以使用系统测试工具,如 Cucumber 和 Cypress。我们在第十七章**,编写您的第一个 Cucumber 测试第十八章**,由 Cucumber 测试引导的功能添加中看到的 Gherkin 语法是这样做的一种方式。

接受测试可以用来在开发人员和产品利益相关者之间建立信任。如果客户不断测试你的软件以寻找错误,这表明开发团队与外界之间的信任水平很低。如果接受测试开始捕获那些客户可能发现的错误,它们可以帮助提高这种信任。然而,与此同时,你也应该问自己为什么 TDD 一开始就没有捕获所有这些错误,并考虑如何改进你的整体测试流程。

基于属性和生成式测试

在传统的 TDD 中,我们找到一小组规范或示例来测试我们的函数。基于属性的测试不同:它基于对函数输入定义的测试生成大量测试。测试框架负责生成输入数据和测试。

例如,如果我有一个将华氏度转换为摄氏度的函数,我就可以使用生成式测试框架来生成针对大量随机整数华氏度测量值的测试,并确保每个值都能正确转换为摄氏度值。

基于属性的测试与 TDD(测试驱动开发)一样困难。它不是万能的灵丹妙药。找到正确的属性进行断言是具有挑战性的,尤其是如果你旨在以测试驱动的方式构建它们。

这种测试不会取代 TDD,但它是任何 TDD 实践者的工具箱中的另一个工具。

快照测试

这是一种流行的 React 应用程序测试技术。React 组件树被序列化为 JSON 字符串并存储到磁盘上,然后在测试运行之间进行比较。

React 组件树在几个重要场景中非常有用,包括以下内容:

  • 当你的团队在 TDD 和一般程序设计方面经验不足,并且可以通过快照测试的安全网来增强信心时

  • 当你正在使用的软件在生产的测试覆盖率为零,并且你希望在做出任何更改之前快速获得一定程度的信心时

质量保证团队有时会对软件在版本之间的视觉变化感兴趣,但他们可能不会想在你的单元测试套件中编写测试;他们会有自己的专用工具来做这件事。

快照测试当然是一个值得了解的有用工具,但要注意以下问题:

  • 快照不是描述性的。它们不会超出说“这个组件树看起来和之前一样”。这意味着如果它们崩溃了,不会立即清楚为什么它们崩溃了。

  • 如果快照在组件树的高层渲染,它们就会变得脆弱。脆弱的测试经常失败,因此需要花费大量时间来纠正。由于测试是在高层进行的,它们无法精确指出错误的位置,因此你将花费大量时间寻找失败的原因。

  • 快照测试可以在两种情况下通过:首先,当组件树与之前测试的版本相同,其次,当找不到之前测试运行中的快照工件。这意味着绿色测试并不能给你带来完全的信心——它可能只是因为之前的工件缺失而变绿。

当编写良好的测试(任何类型的测试)时,你希望以下关于任何测试失败都是真实的:

  • 非常快地确定失败是由于错误还是规范的变化

  • 在错误的情况下,非常快地确定问题和错误的位置

TDD 是一种社区已经学到了足够多的、知道如何编写良好测试的成熟技术。我们在快照测试方面还没有达到这个水平。如果你绝对必须在代码库中采用快照测试,请确保你衡量它为你和你的团队提供的价值。

金丝雀测试

金丝雀测试是在你将软件发布给一小部分用户并观察发生了什么。对于拥有大量用户的 Web 应用程序来说,这可能很有用。金丝雀测试的一种形式涉及将每个请求发送到两个系统:运行中的系统和测试系统。用户只能感知到运行中的系统,但测试系统的结果由你记录和分析。然后可以观察到功能和性能的差异,而你的用户永远不会受到测试软件的影响。

金丝雀测试很有吸引力,因为从表面上看,它似乎非常具有成本效益,而且几乎不需要程序员进行任何思考

与 TDD 不同,金丝雀测试无法帮助你设计软件,而且你可能需要一段时间才能得到任何反馈。

这就完成了我们对自动化测试领域的考察。我们本章开始时探讨了手动测试技术。现在,让我们以一个最终技术来结束本章:完全不进行测试!

完全不进行测试

有一种观点认为,TDD 不适用于某些它确实适用的场景——例如,如果你的代码是废弃的,或者一旦部署就被认为永远不需要修改。相信这一点几乎可以确保相反的情况是真实的。代码,尤其是没有测试的代码,往往会在其预期寿命之外继续存在。

删除代码的恐惧

除了减少更改代码的恐惧之外,测试还可以减少删除代码的恐惧。没有测试,你可能会阅读一些代码并想“也许有人用这段代码来达到我不太记得的目的。”有了测试,这就不会成为问题。你会阅读测试,看到由于需求的变化,测试不再适用,然后删除测试及其相应的生产代码。

然而,有一些情况下不编写测试是可以接受的。其中最重要的两个如下。

当质量不重要时

不幸的是,在许多环境中,质量并不重要。我们中的许多人都能理解这一点。我们为那些积极忽视质量的雇主工作过。这些人赚得足够的利润,以至于他们不需要或不想关心。关心质量,不幸的是,是一个个人的选择。如果你在一个不重视质量的团队中,将很难说服他们 TDD 是值得的。

如果你处于这种情况,并且你迫切希望使用 TDD,那么你有几个选择。首先,你可以花时间说服你的同事这是一个好主意。这从来都不是一件容易的事情。你也可以玩 TDD-by-stealth 游戏,在你开始之前不征求任何人的同意。如果这些选项都失败了,一些程序员可能会足够幸运,能够找到一家确实重视质量的替代雇主。

编写和删除代码

Spike 意味着不进行测试的编码。我们在未知领域时进行 spike。我们需要找到解决我们以前从未解决过的问题的可行方法,这很可能会涉及大量的尝试和错误,以及大量的回溯。在找到可行方法之前,找到不可行方法的可能性很高。在这种情况下编写测试没有太多意义,因为许多在过程中编写的测试最终都会被废弃。

假设,例如,我正在构建一个 WebSocket 服务器和客户端,但这是我第一次使用 WebSocket。这将是一个很好的 spike 候选者——我可以安全地探索 WebSocket API,直到我对将其集成到我的应用程序中感到舒适。

当你觉得你已经找到了一个可行的方案时,重要的是要停止 spiking。你不需要一个完整的解决方案,只需要一个能教会你足够知识,让你走上正确道路的方案。

在 TDD 的纯粹主义视角中,spiking 必须随后进行删除。如果你要进行 spike,你必须习惯于删除你的工作。不幸的是,说起来容易做起来难;很难清除创造性输出。你必须摆脱你的代码是神圣的信念。乐于将其丢弃。

在 TDD 的实用主义视角中,spiking 常常可以随后编写围绕 spike 代码的测试。我经常使用这个技巧。如果你是 TDD 的新手,在你自信能够想出一套测试序列,覆盖 spike 代码中所有必需的功能之前,避免使用这个特定的作弊技巧可能更明智。

纯粹主义者可能会说,你的 spike 代码可以包含冗余代码,并且它可能不是最简单的解决方案,因为测试并没有驱动实现。这个论点有一定的道理。

Spiking 和测试最后开发

Spiking 与测试最后的实践有关,但存在细微的差别。围绕 spike 编写代码是 TDD 的一个作弊行为,因为你希望你的最终测试看起来就像你一开始就使用了 TDD 一样。任何在你之后到来的人都不应该知道你作弊了。

测试最后,然而,是一种更宽松的测试方式,你先编写所有生产代码,然后编写一些单元测试来证明一些更重要的用例。以这种方式编写测试给你提供了一定程度的回归覆盖率,但没有 TDD 的其他好处。

摘要

成为 TDD 的优秀实践者需要极大的努力。这需要练习、经验、决心和纪律。

许多人尝试过 TDD 并失败了。其中一些人可能会得出结论说 TDD 是有缺陷的。但我并不认为它是有缺陷的。只是需要努力和耐心才能做对。

但究竟什么是做对的呢?

所有软件开发技术都是主观的。本书中的所有内容都是主观的;它不是正确的方式。这是一系列我喜欢使用并且发现成功的技巧集合。其他人也用其他技巧取得了成功。

TDD 的激动人心之处不在于过程的黑白、严格形式;而是在于灰色地带,我们可以定义(并完善)一个适合我们和同事的开发过程。TDD 循环给我们提供了刚好足够的结构,我们可以从中找到乐趣,用我们的规则和教条来充实它。

我希望你觉得这本书有价值且有趣。测试驱动 React 应用程序的方法有很多,我希望这能成为你发展测试实践的平台。

进一步阅读

要了解更多关于本章所涉及的主题,请查看以下资源:

  • 有用的 Kata 资源:

codingdojo.org/kata/

codekata.com

kata-log.rocks

github.com/sandromancuso/Bank-kata

www.natpryce.com/articles/000807.xhtml