06月20, 2016

【译】Spying on the DOM

原文:http://www.zcfy.cc/article/613

我一直是对 TDD(测试驱动开发)和代码测试持怀疑态度的。我发现它很难做到而且很耗时间。尽管如此,有比测试难得多而且更耗时间的事情。你可能已经猜到了,改 bug!。当代码库变大了,开发团队来了新人,你会发现晚上越来越难睡个安稳觉,因为你不知道一周前写的代码在明天的需求变更中还能不能存活下来。我不想讨论单元测试的好处,因为这个话题可以单独写一整篇文章,而如果你快速 google 一下,你一定会找到关于单元测试这个话题的高质量的内容

所以让我们直奔主题。我正在做一个 Backbone 项目。在此处我不关心用什么框架,我更关心代码库将如何增长以及代码库将如何保持稳定。我并不是说框架不重要。不管怎样,有一件事情是肯定的,如果有人不理解所使用的库或框架构建的基础——不论这个库或框架是什么,一切都会变得一团糟。

所以像我说的,我关心代码的质量。因为我想要在晚上睡个安稳觉,我开始写测试。如你们大多数人所知的,使用 Backbone 工作你将迟早引入 jQuery。更进一步,大多数开发者倾向于滥用 jQuery 的能力,不论他们用的是什么框架。因此,我发现遵守如下一些规则对保持代码尽可能可测试很有用:

  • 尽可能少操作 DOM(不论用什么框架)
  • 即使你觉得有必要用 jQuery,在局部使用。
  • DOM 作为表现层而不是作为逻辑层。

我发现这些规则有用的原因是测试 DOM 自身会很麻烦。而且,我确实不认为 view 层的测试有用。在我看来,表现层测试与代码质量没有任何关系。

由于这个原因,我决定不使用任何衍生的浏览器去测试应用,这意味着不用 Phantomjs,不用 Chrome 及其他。我只用如下三个库:

这些工具对于单元测试来说很棒。然而,如果你想要测试任何调用了 jQuery 的部分,你将受阻于一个小错误,它提示你 jQuery 需要一个 window 对象才能工作。所以...完蛋了?还没有。我们依然可以搞定它,而不需要一个浏览器。

介绍我们的秘密武器:jsdom

有了这个“坏男孩”,我们可以模拟整个 DOM,从而仍然可以在 nodejs 下执行 jQuery 代码。所以有了它,我们的 nodeJquery 只需要几行代码就能搞定:

const fakeDOM = require("jsdom").jsdom(); 
nodeJquery = require("jquery")(fakeDom.defaultView);

你瞧,现在我们可以在 mocha 下安全地单元测试我们依赖于 jQuery 的代码了,不会有任何错误。

这一切都好,但我们仍然需要确保我们现有的代码不会出错。对于这个问题,我们可以写一个工厂方法叫做 getJquery(),它返回一个 nodeJquery 单例对象:

let nodeJquery = null;

export function getJquery(spy = false) {
   if (!nodeJquery) {
      const sinon = require("sinon");
      const fakeDOM = require("jsdom").jsdom();
      nodeJquery = require("jquery")(fakeDOM.defaultView);
    }

    return nodeJquery;
}

好的,现在让我们写一些我们可以真正测试的代码。我们假设我们有一个简单的 validator 对象:

import $ from "jquery";

export const isEmpty = value => !value.length;

export default function getValidator(rule, inputSelector, valueGetter) {
  return {
    validate() {
      const $input = $(inputSelector);
      const value = valueGetter();

      if (rule(value)) {
        $input.addClass("error");
        return false;
      }
      else {
        $input.removeClass("error");
        return true;
      }
    }
  }
}

这个模块简单提供一个 validate() 函数,它有三个参数:

  • rule(一个返回布尔结果的函数,用来评估输入字符串
  • inputSelector(我们将使用它来给被校验的 input 添加 error class
  • valueGetter(一个将返回当前被校验 input 对象的值的函数

为了测试这个模块,我们需要 sinonjs 的帮助,或者更确切说,我们需要使用 spy 函数。一个 spy 函数简单让你知道一个函数什么时候被调用、被调用了几次,以及用哪些参数调用。为了更好地测试我们的 jQuery 代码,我们将需要在所有 jQuery 函数上创建 spy,例如:hide、show、append,等等

为了达成这个目标,我们将添加一个新的函数到我们的 jquery-getter.js 模块:

const sinon = require("sinon");
const fakeDOM = require("jsdom").jsdom();

let nodeJquery = null;
let jquerySpies = null;

export function getJquery() {
   if (!nodeJquery) {
      nodeJquery = require("jquery")(fakeDOM.defaultView);
    }

    return nodeJquery;
}

export function getJquerySpies() {
  if (!jquerySpies) {
    jquerySpies = {};

    Object.keys(nodeJquery.prototype).forEach(method => {
      if (typeof nodeJquery.prototype[method] === "function") {
        jquerySpies[method] = sinon.spy(nodeJquery.prototype, method);
      }
    });
  }

  return jquerySpies;
}

有了 getJquerySpies() 的帮助,我们改变我们的 nodeJquery 单例版本,在 jQuery 提供的所有公开的函数上创建 spies。所以现在我们有了两个“主力球员”,它们将帮助我们测试 validator 模块:

  • getJquery()
  • getJquerySpies()

尽管如此,validator 模块仍然加载原版 jQuery 库。因此我们将要把 jQuery 切换成我们修改过的 nodeJquery。为了做到这个,我们将使用这个极好的模块叫做 proxyquire(https://github.com/thlorenz/proxyquire)。有了 proxyquire 的帮助,我们可以创建一个中间人,当模块尝试加载 jQuery 函数的时候,它将实际加载我们修改过的 nodeJquery 版本。偷梁换柱,对吗?

因此让我们看一下最终版的单元测试:

import { assert } from "chai";
import proxyquire from "proxyquire";
import { getJquerySpies, getJquery } from "../src/jquery-getter";

const $ = getJquery();
const getValidator = proxyquire("../src/validator", { jquery : $ }).default;
const isEmpty = proxyquire("../src/validator", { jquery : $ }).isEmpty;

const addClassFn = getJquerySpies().addClass;

describe("validator", () => {
  it("should add the error class when validation fails", () => {
    let value = "";
    const validator = getValidator(isEmpty, "input", () => value);

    validator.validate();
    assert.isTrue(addClassFn.called);
    assert.isTrue(addClassFn.getCall(0).calledWith("error"));
    addClassFn.reset();
  });
});

就是这样,我们现在可以单元测试依赖 jQuery 的代码而不需要引入 Phantomjs、Chrome 或者任何别的浏览器。通过这么做我们得到一个主要的好处:速度。你的单元测试将会运行得超级快。而且,你会被迫不滥用 jQuery 以保证你的代码仍然能被测试(因为如果用到大量 jQuery + css,那就很难测了,jsdom 还只能支持一个简单的 css 子集——译者注)。

这可能不是最佳解决方案,但是它的确解决了我的问题。你也应该记住 jQuery 代码越复杂,单元测试越难做。这就是我为什么在前面提到我尽可能少操作 DOM

结论

我学到了如果你一开始就做,测试你的代码并没有那么难。而写测试代码为你的代码库带来的长期好处是显而易见的。我也学到了测试你的前端代码很难,你得去实现各种模拟和侵入代码才能让测试正常工作。然而,在我看来这么做是值得的。我是一个不断努力寻找途径让我的代码尽可能易于被单元测试的人。

英文原文:https://blog.hellojs.org/spying-on-the-dom-d9c3d2beb2f8#.sk6nbd9os

本文链接:https://www.h5jun.com/post/spying-on-the-dom.html

-- EOF --

Comments