有效重构,第3部分:测试的重要作用


这是关于有效重构的由4部分组成的系列文章的第三部分。

如果您不喜欢测试您的产品,很可能您的客户也不会喜欢测试它。

阅读任何一本关于软件测试的书或文章,他们中的大多数人都会在某种程度上提到,开发人员并不热衷于编写测试。我不打算争论这一说法的准确性,但我想起了拉尔夫·沃尔多·爱默生(Ralph Waldo Emerson)的一句话:

你的行动太大声了,我听不见你在说什么。

如果您编写测试并认识到它们的价值,您的应用程序将有很多简单明了的测试。如果您的重构项目的目标是严重失败,那么不要编写任何测试。

没有测试。

测试将缓解重构的主要压力之一:破坏应用程序的可能性。让我们面对现实吧,没有开发人员是十全十美的,也没有工具是完全不出错的。在重写代码的整个过程中,您可能会意外删除一行或有意删除一些看似不必要的逻辑。如果您正在对文件进行大量更改,这些破坏性更改可能不会被注意到。测试总是会注意到的。

在本系列的第3部分中,我将向您介绍设置有效的测试基础设施的过程。我在用JestEnzyme因为启动和运行相对较快。我推荐阅读以下文章,了解如何在Reaction/Redux应用程序中配置这些库。

出于本文的目的,我不想太深入地研究设置和配置。建立坚实的测试基础的第一步之一是建立可靠的模拟数据源。我们开始吧!

设置模拟数据

如果您使用的是现有应用程序,那么您的优势在于了解现有数据的形状和使用方式。在大多数情况下,API是经过改进的,在添加新功能时可能只需要稍作调整。对于我重构的应用程序,我创建了两个包含数据的文件:一个包含API响应,另一个包含填充的Redux状态。创建这些文件可能需要一些跑腿工作和大量的复制和粘贴,但只需要做一次。

拥有这些数据文件有两个重要目的。第一个也是显而易见的一点是,您将测试的许多元素将以某种方式显示或操作数据。第二个是能够快速引用数据的形状,以确定要编写的最有效的测试以及它们可能失败的原因。

如果数据敏感,则存储状态和API响应可能并不总是可行的。如果是这样的话,您可以使用像这样的库json-schema-fakerfaker或者chance以生成随机数据。这些库允许您使用种子重复生成相同的数据,但我建议您只生成一次数据并将生成的数据保存在repo中,而不是每次运行测试时都使用种子生成数据。我把我的文件存储在__fixtures__ 我的Redux文件(动作、减速器、选择器)旁边的目录。文件夹结构如下所示:

/src
  /components
  /constants
  /containers
  /redux
    /__fixtures__
      /state.js
      /responses.js
    /app
      /appActions.js
      /appReducer.js
      /appSelectors.js
  /...

获取Redux状态的整个形状的最简单方法是使用Redux DevTools扩展,从状态视图中选择Raw选项卡,复制所有内容,然后使用module.exports 声明。我建议只从状态和API响应中提取一小部分记录,以减小Jest快照的大小。在确定您认为适合保留的记录数量时,请使用最佳判断力。如果其中一个API响应返回一个包含400条记录的数组,您肯定可以消除其中很大一部分记录,并且仍然可以编写有效的测试。

拥有有效的数据是防止回归错误的必要条件。如果数据不能代表应用程序使用的数据,那么再多的测试也无法确保重构的成功。

现在您已经准备好了数据,是时候进入下一步了:为测试建立标准格式和样式指南。

使您的测试标准化

在重构项目过程中,您将最终编写大量测试。当我第一次开始编写测试时,一般来说,我对测试还比较陌生。我的测试是杂乱无章的,文件之间的措辞不同,关于以下方面的测试的结构describe 即使在测试两个非常相似的组件时,块也会有所不同。

我发现,建立测试标准可以减少确保编写质量测试所需的认知负担,从而使编写测试变得容易得多。标准的存在和遵守比微小的细节更重要。你可以自由地把最适合你的东西组合在一起,但是有几条你应该遵循的指导原则。

确定测试文件的存放位置

有些人更喜欢镜像src/ 目录,并将他们的测试文件放在那里。也许您更喜欢将测试文件命名为.spec.js .test.js。不管你选择什么,都要始终如一。缺省的Jest配置指定您将测试放在__tests__ 目录和使用.test.js 作为文件扩展名,所以这就是我选择的路线。一旦你决定了一个标准,就把它添加到自述文件中,这样将来任何其他开发这款应用的人都会效仿。

建立一种格式

您应该为每个正在测试的上下文建立格式/结构(即Reaction组件、Redux选择器等)。例如,我创建的每个Reaction组件和容器测试文件都有一个setup 函数位于如下所示的文件顶部:

const setup = (propOverrides, renderFn = shallow) => {
  const props = {
    propA: 'Some Value',
    propB: false,
    onClick: jest.fn(),
    ...propOverrides,
  };

  const wrapper = renderFn(<AppComponent {...props} />);

  return { props, wrapper };
};

这使得测试我的组件变得更加容易,而不必编写很多额外的样板。我还设置了一个特定的describe 挡路结构,用于反应组件。

describe('Component A', () => {
  describe('Snapshot validation', () => {
    it('matches its snapshot with valid props', () => {
      const { wrapper } = setup();
      expect(wrapper).toMatchSnapshot();
    });
  });

  describe('Event validation', () => {
    it('fires props.onClick when button is clicked', () => {
      const { wrapper, props } = setup();
      wrapper.find('button').simulate('click');
      expect(props.onClick).toHaveBeenCalled();
    });
  });

  // Note: This is only for connected components.
  describe('Redux validation', () => {
    const store = {
      getState: () => state,
      dispatch: jest.fn(),
      subscribe: () => {},
    };

    it('renders when connected to Redux state', () => {
      const wrapper = shallow(<ComponentA store={store} />);
      expect(wrapper).toHaveLength(1);
    });
  });
});

我使用Redux操作、减法器和选择器执行类似的操作。我用WebStorm’s File Templates功能可以根据我正在测试的内容快速创建测试文件。您的编辑器很可能支持代码片断或文件模板,因此我建议您创建模板以确保您遵守标准。这有助于在自述文件中为后人提供这方面的简要概述。

现在是时候切入正题了。如果您不熟悉编写测试,找出最佳操作方案可能会令人望而生畏。你可能会问自己很多问题:

  • 我从哪里开始呢?

  • 我应该测试什么?

  • 我如何知道我是否已经编写了足够的测试来防止错误?

这些问题没有明确的“正确”答案,但我在整个项目过程中遵循的方法产生了成功的结果。

Part 2 of this series我强调了制定计划的重要性。如果您制定了一个行动方案,那么确定从哪里开始编写测试应该很简单。假设您正在开始一项任务,重构UI状态的Redux操作、Reducer和选择器。您已经准备好了状态数据,因此编写测试应该相对简单。你需要一个像这样的图书馆redux-mock-store模拟状态以测试动作。我非常依赖快照来测试减法器和选择器,即使选择器只返回字符串或布尔值。

在更改任何代码之前,请确保编写了所有测试。在重构代码之后,您的一些测试可能会失败。失败的测试将表明失败是由于故意更改还是意外更改。在这方面,快照是无价的。很容易遗漏一个字段或拼错对象键,在差异中看到这一点将使纠正问题变得微不足道。

只为重构的代码部分和任何直接受更改影响的代码编写测试。在开始重构之前尝试为整个应用程序编写测试将导致倦怠和沮丧。追查依赖项并编写覆盖所有基础的测试可能会令人沮丧,但会让您深入了解代码库,并提供更多重构机会。添加或更新您的项目以反映这些机会,否则,您可能会忘记它们并走上错误的道路。

我应该测试什么?

当您刚刚开始时,确定要测试什么的最简单和相对可靠的方法是代码覆盖率。Jest具有内置的代码覆盖率,您可以生成一个HTML报告,其中包含覆盖率百分比以及哪些代码部分当前未被测试覆盖。线路覆盖会让您自我感觉良好,但分支覆盖才是您想要关注的。如果您想了解Coverage类型之间的差异的详细说明,请查看this article by Jason Rudolph。在下一节中,我将讨论为什么代码覆盖率不是唯一的真理来源,但它是引导您走上正轨并保持动力的一个很好的工具。

我怎么知道我做得够多了?

这个问题更难回答。当我刚开始的时候,我把Jest的代码覆盖率当作福音。随着时间的推移和我经验的增长,我发现这不一定是最好的做法。覆盖率是评估代码的哪些部分正在测试(或没有测试)的优秀工具。如果您有一个if 语句,并且else 情况不在测试范围内,覆盖率报告将指出这一点。很高兴在报告或终端上看到很高的百分比和大量的绿色,但仅仅编写测试来进入绿色并不能防止错误。

关于写好测试的文章和书籍不胜枚举,而且各执己见。我喜欢检查一个函数几次,以确保我理解其逻辑,然后编写故意试图破坏它的测试。如果API响应中缺少字段怎么办?如果响应为空,会发生什么情况?

例如,假设有一个选择器,它汇总分配给特定地区每个销售员的预算。负责该地区的销售经理有全部可用预算。可用预算总额应始终高于已分配预算总额。当它不是的时候会发生什么?有没有if 这份声明涵盖了这一点吗?阅读代码和编写测试通常会让您想到这样的情况。代码覆盖率只会告诉您该函数已被覆盖。

结束

冒着听起来像打破记录的风险,我想重申测试对于在不破坏现有功能的情况下成功重构代码库有多么重要。有些东西会从裂缝中掉下来,但是一个漏水的水龙头比一个爆裂的管道更容易修理(也更便宜)。如果您从可靠的模拟数据、良好的标准开始,并在重构代码的同时编写测试,那么这个过程应该会相对顺利。在重写过程中您会遇到许多挑战,但是一个好的测试套件会灌输克服这些挑战所需的信心。

如果您有一个很好的测试框架设置,并且您为要重构的代码编写了测试,那么终于到了开始更改代码的时候了!在本系列的下一部分中,我将介绍与重写相关的提示、技巧和一些陷阱。