四、异步测试——AJAX

不可避免地,在每个 JavaScript 应用中都有需要测试异步代码的时候。

异步意味着你不能以线性方式处理它——一个函数可能在执行后立即返回,但是结果会在稍后出现,通常是通过回调。

这是处理 AJAX 请求时非常常见的模式,例如通过 jQuery:

$.ajax('http://0.0.0.0/data.json', {
  success: function (data) {
    // handle the result
  }
});

为了演示 Jasmine 对异步测试的支持,我们将实现以下验收标准:

"股票回笼时,应更新其股价."

通过使用我们到目前为止向您展示的技术,您可以在spec/StockSpec.js文件中编写如下验收标准:

describe("when fetched", function() {
  beforeEach(function() {
    stock.fetch();
  });

  it("should update its share price", function() {
    expect(stock.sharePrice).toEqual(23.67);
  });
});

这将导致在src/Stock.js文件中实现fetch功能:

Stock.prototype.fetch = function() {
  var that = this;
  var url = 'http://0.0.0.0:8000/stocks/'+that.symbol;

  $.getJSON(url, function (data) {
    that.sharePrice = data.sharePrice;
  });
};

重要的部分是$.getJSON调用,这是一个 AJAX 请求,期待包含更新股价的 JSON 响应:

{
  "sharePrice": "23.67"
}

到现在,你可以看到我们被卡住了;为了运行这个规范,我们需要一个运行的服务器。

设置场景

由于这本书完全是关于 JavaScript 的,我们将创建一个非常简单的 Node.js 服务器供规范使用。Node.js 是一个允许使用 JavaScript 开发网络应用的平台,比如 web 服务器。

第 6 章光速单元测试中,我们将看到在不需要服务器的情况下测试 AJAX 请求的替代解决方案。

安装 Node.js

如果已经安装了 Node.js,可以跳到下一节。

有适用于 Windows 和 Mac OS X 的安装程序:

  • 前往 Node.js 网站http://nodejs.org/
  • 点击安装按钮
  • 下载完成后,执行安装程序并按照步骤操作

要查看其他安装方法和如何在 Linux 发行版上安装的说明,请查看https://github . com/joyent/node/wiki/Installing-node . js-via-package-manager上的官方文档。

完成后,您的命令行上应该有一个node命令。

对服务器进行编码

为了学习如何编写异步规范,我们将创建一个返回一些假数据的服务器。在项目的根文件夹中创建一个名为server.js的新文件,并添加其内容:

var express = require('express');
var app = express();

app.get('/stocks/:symbol', function (req, res) {
  res.setHeader('Content-Type', 'application/json');
  res.send({ sharePrice: 20.18 });
});

app.use(express.static(__dirname));

app.listen(8000);

为了处理 HTTP 请求,我们使用了快速,一个 Node.js 网络应用框架。通过阅读代码,可以看到它定义了一个到/stocks/:symbol的路由,所以它接受http://0.0.0.0:8000/stocks/AOUE等请求,并用 JSON 数据进行响应。

我们还使用express.static模块为http://0.0.0.0:8000/SpecRunner.html的规范转轮提供服务。

有规避 同产地政策的要求。出于安全原因,这是一个规定 AJAX 请求不允许在不同于应用的域上执行的策略。

这个问题是在第三章测试前端代码中使用带有 Chrome 浏览器的 HTML 装置时首次演示的,因为在处理file://网址时,有些浏览器的要求比其他浏览器更严格。

使用 Chrome 浏览器检查器,你可以在打开带有file://协议的SpecRunner.html文件时看到控制台中的错误(基本上是你一直在做的事情):

Coding the server

同源策略错误

通过服务于 runner,以及同一基本 URL 下的所有应用和测试代码,我们防止了这个问题的发生,并且能够在任何浏览器上运行规范。

运行服务器

要运行服务器,首先需要使用节点的包管理器安装其依赖项(快速)。在应用根文件夹中,运行npm命令:

$ npm install express

该命令将下载 Express,并将其放入项目文件夹中名为node_modules的新文件夹中。

现在,您应该能够通过调用node命令来运行服务器:

$ node server.js

要检查它是否工作,请在浏览器上点击http://0.0.0.0:8000/stocks/AOUE,您应该会收到 JSON 响应:

{
  "sharePrice": "23.66"
}

现在我们已经使我们的服务器依赖性工作了,我们可以继续编写规范了。

编写规范

在服务器运行的情况下,在http://0.0.0.0:8000/SpecRunner.html打开浏览器,查看我们的规范结果。

您可以看到,即使服务器正在运行,并且规范看起来是正确的,但它还是失败了。这是因为stock.fetch()是异步的。对stock.fetch()的调用立即返回,允许 Jasmine 在 AJAX 请求完成之前运行期望:

it("should update its share price", function() {
  expect(stock.sharePrice).toEqual(23.67);
});

为了解决这个问题,我们需要接受stock.fetch()函数的异步性,并指示 Jasmine 在运行期望之前等待它的执行。

等待时间()函数

要告诉 Jasmine 等待异步调用,我们需要使用它的另一个全局函数waitsFor()

在深入了解它的工作原理之前,让我们先来修改一下之前的测试代码,以使用这个新函数:

describe("when fetched", function() {
  var fetched = false;

  beforeEach(function() {
    stock.fetch({
      success: function () {
        fetched = true;
      }
    });

    waitsFor(function (argument) {
      return fetched;
    }, 'Timeout fetching stock data', 2000);
  });

  it("should update its share price", function() {
    expect(stock.sharePrice).toEqual(23.67);
  });
});

您首先会注意到,我们在stock.fetch()函数中添加了一个success回调,在获取完成后将fetched变量设置为true:

stock.fetch({
  success: function () {
    fetched = true;
  }
});

其实施如下:

Stock.prototype.fetch = function(parameters) {
  var that = this;
  var params = parameters || {};
 var success = params.success || function () {};
 var url = 'http://0.0.0.0:8000/stocks/'+that.symbol;

  $.getJSON(url, function (data) {
    that.sharePrice = data.sharePrice;
 success(that);
  });
};

然后我们使用waitsFor()功能保持it块的执行,直到fetched变量为true:

waitsFor(function (argument) {
  return fetched;
}, 'Timeout fetching stock data', 2000);

如果股票没有在 2000 毫秒内被取出,它会抛出一个错误,使规范失败。

让我们回顾一下,waitsFor()功能接受三个参数:

  • A function that Jasmine will poll, until it gets a truth result:

    function (argument) { return fetched; }

  • An error message to show if the waiting times out:

    "提取股票数据超时"

  • 超时前等待的时间(毫秒):2000

因此,每当您有任何依赖于异步调用结果的期望时,您可以通过使用beforeEach块中的waitsFor()函数来保持它的执行。

接下来,我们将看到如何直接在it块内部使用waitsFor()功能。

运行()功能

我们已经看到我们可以使用beforeEach中的waitsFor()函数,但是如果我们需要编写一个在it块中有异步调用的测试代码呢?

作为练习,让我们重写之前的规范,不将其嵌套在describe块中,而是将其作为单个it块:

it("should be able to update its share price", function() {
  var fetched = false;

  stock.fetch({
    success: function() {
      fetched = true;
    }
  });

  waitsFor(function (argument) {
    return fetched;
  }, 'Timeout fetching stock data', 2000);

  expect(stock.sharePrice).toEqual(23.67);
});

通过运行这个例子,你会发现同步问题又回来了。那是因为waitsFor()函数并没有阻止执行。

它以前是有效的,因为 Jasmine 等待运行it块,直到waitsFor()完成。

所以我们需要一种方法来安排这个期望代码在waitsFor()完成后运行。正如你可能已经猜到的,这将是通过另一个 Jasmine 全局函数,runs函数。

您所要做的就是在runs块中移动您想要尊重异步行为的代码:

it("should be able to update its share price", function() {
  var fetched = false;

  stock.fetch({
    success: function() {
      fetched = true;
    }
  });

  waitsFor(function (argument) {
    return fetched;
  }, 'Timeout fetching stock data', 2000);

  runs(function() {
    expect(stock.sharePrice).toEqual(23.67);
  });
});

这样,Jasmine 只在waitsFor()完成后运行该代码。

您甚至可以放置多个runs块,它们将按照声明的顺序运行:

it("should be able to update its share price", function() {
  var fetched = false;

  runs(function() {
    stock.fetch({
      success: function() {
        fetched = true;
      }
    });
  });

  waitsFor(function (argument) {
    return fetched;
  }, 'Timeout fetching stock data', 2000);

  runs(function() {
    expect(stock.sharePrice).toEqual(23.67);
  });
  runs(function() {
    expect(stock.sharePrice).not.toBeUndefined();
  });

  runs(function() {
    expect(stock.sharePrice).toBeGreaterThan(0);
  });
});

总结

在本章中,您已经看到了如何测试异步代码,这是测试服务器交互(AJAX)时常见的场景。我已经向您展示了如何使用不同的 Jasmine 全局函数,如waitsFor()runs来测试异步代码。

我还向您展示了 Node.js 平台,并使用它编写了一个简单的服务器代码,用作测试设备。

第 6 章光速单元测试中,我们将看到 AJAX 测试的不同解决方案,不需要服务器运行的解决方案。