英文原文:http://emberjs.com/guides/testing/testing-xhr/

在Ember中测试异步调用和承诺,一开始可能觉得有点复杂,不过通过一些说明便能清晰起来。

承诺,Ember和运行循环

为了能完整的解释测试承诺和异步代码,对Ember的运行循环有一个清晰的了解非常重要。如果还不了解,请看看承诺理解Ember运行循环指南

现在对运行循环有了基本的理解,回想之前阅读的测试Ember应用的基础知识,发现在测试模式下,运行循环是被暂停的。这有助于保证代码的步骤和围绕代码编写的测试。注意在测试承诺和异步代码中,是一块块有效的“逐句通过”应用的。

当一个承诺运行时,其计划了履行和违背承诺让运行循环执行,因此为了使承诺工作,运行循环必须启用。简而言之,没有运行循环就没有承诺的履行和违背。

要获取承诺的结果需要使用then方法。在一个已知的承诺上调用then函数。

  1. // let's call the existing promise promise1, so you'd write:
  2. promise1.then(fulfillmentCallback, rejectionCallback);
  3. function fulfillmentCallback(successfulResults) {
  4. // do something wonderful with the results
  5. }
  6. function rejectionCallback(failureResults) {
  7. // tell someone important about the failure
  8. }

在本例中,promise1成功时,那么fulfillmentCallback函数会被调用,并将promise1的成功结果作为其参数。如果承诺被否决了(例如失败了),那么rejectionCallback将被调用,失败的原因将作为其参数。

如果将一个函数传给then,则会将该函数转换为一个承诺,并返回这个承诺。这个承诺的结果将是这个函数的返回值。

  1. // let's call the existing promise promise1 and will have the result `3`, so you'd write:
  2. var promise2 = promise1.then(function(results){
  3. return results + 2;
  4. });
  5. // the results of this promise would be 10
  6. var promise3a = promise2.then(function(results){
  7. return results + 5;
  8. });
  9. // the results of this promise would be 6
  10. var promise3b = promise2.then(function(results){
  11. return results + 1;
  12. });
  13. // or we can chain without the intermediary variables like so,
  14. var promise4 = promise1.then(function(results){
  15. return results + 2;
  16. }).then(function(results){
  17. return results + 5;
  18. }).then(function(results){
  19. return results + 90;
  20. }).then(function(results){
  21. alert(results); // this will alert `100`
  22. });

如果将一个承诺传递给then,其将返回这个承诺的结果。

  1. // let's call the existing promises promise1 and promise2, so you'd write:
  2. var promise3 = promise1.then(promise2);
  3. promise3.then(function(result){
  4. // this will be the results from promise2
  5. // this callback won't be called until promise1 and promise2 have fulfilled
  6. alert(result);
  7. });

如果运行循环没有运行,这些都不会工作。因为这些回调或者链式承诺都被计划在运行循环中执行。

运行循环和承诺的交点在哪

承诺处理

  1. var promise = new Ember.RSVP.Promise(function(resolve){
  2. // calling resolve will schedule an action to fulfill the promise
  3. // and call observers/chained promises.
  4. resolve('hello world'); // Run loop needs to be on here
  5. });

链接/观察承诺

  1. // once the above promise has been resolved it will then notify
  2. // the observers/chained promises to.
  3. promise.then(function(result){ // Run loop might* need to be on here
  4. alert(result);
  5. });
  • 调用then(观察/链接)只需要将其隐性的包裹在一个运行调用语句中(例如Ember.run(..)),这样有可能能够在承诺履行后对其进行链接/观察。下面的示例解释了不同的场景。
承诺履行之前观察/链接示例
  1. 运行循环是关闭的(测试模式)
  2. 代码:创建Promise1(new Ember.RSVP.Promise….)
  3. 代码:观察Promise1(promise.then(…))
  4. 代码:启动运行循环(将在运行循环完成所有的计划项目后停止)
  5. 代码:履行Promise1(将计划一个任务到运行循环来履行承诺)
  6. 运行循环:运行“履行承诺”任务(包括将履行通知所有链接的承诺/观察器)
  7. 运行循环停止,因为所有任务已经完成
  1. new Ember.RSVP.Promise(function(resolve){
  2. // resolve will run ~10 ms after the then has been called and is observing
  3. Ember.run.later(this, resolve, 'hello', 10);
  4. }).then(function(result){
  5. alert(result);
  6. });
承诺履行之后观察/链接示例
  1. 运行循环是关闭的(测试模式)
  2. 代码:创建Promise1
  3. 代码:启动运行循环(将在运行循环完成所有的计划项目后停止)
  4. 代码:履行Promise1(将计划一个任务到运行循环来履行承诺)
  5. 运行循环:运行“履行承诺”任务(包括将履行通知所有链接的承诺/观察器)
  6. 运行循环停止,因为所有任务已经完成
  7. 代码:观察Promise1(因为承诺已经履行,计划一个异步任务来通知观察器)
  8. 未捕获的错误:断言失败:开启了测试模式,这将禁止运行循环自动运行。需要将所有异步的代码包裹在Ember.run()中。
  1. var promise = new Ember.RSVP.Promise(function(resolve){
  2. // this will run before the then has happened below
  3. // and finish the triggered run loop
  4. Ember.run(this, resolve, 'hello');
  5. });
  6. // incorrect the run loop isn't on any more
  7. promise.then(function(result){
  8. alert(result);
  9. });
  10. // correct, start the run loop again
  11. Ember.run(function(){
  12. promise.then(function(result){
  13. alert(result);
  14. });
  15. });

测试承诺和运行循环

当在通常情况下使用Ember时(例如不在测试模式),运行循环是处于活跃的执行状态的,也就是说不需要将异步代码包裹在Ember.run()中。在测试模式,运行循环是被动的,必须手动触发。测试没有包裹在Ember.run中的异步代码会导致出错:Uncaught Error: Assertion Failed: You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run.

一般性示例

这里创建了一个承诺,并且使用setTimeout来模拟从一个假的服务器延时返回一个响应。当这个假服务器返回时,需要将代码包裹在Ember.run中,手动调用运行循环。

  1. var promise = new Ember.RSVP.Promise(function(resolve){
  2. setTimeout(function(){
  3. Ember.run(this, resolve, 'hello world');
  4. }, 20);
  5. });

如果将上面的承诺传递给多个方法,并且这些方法观察/链接这个承诺,在某一时刻这个承诺可能已经履行了。这种情况下,需要将观察器和链接的承诺包裹在一个运行调用中。

  1. Ember.run(function(){
  2. promise.then(function(result){
  3. alert(result);
  4. });
  5. });

使用承诺的同步示例

如果使用承诺,但是其立即都履行了,那么可以简单的遵从上面的方法,将履行和观察器/链接的承诺包裹到一个运行调用中。本例中包裹了履行和观察器到一个运行调用中(因为承诺立即就被履行了)。

简单的承诺示例

使用承诺的异步示例

如果使用承诺,如果承诺在测试完成的时候才履行,那么就需要使用qunit的全局方法stopstart。这些方法可以告诉qunit在当前测试中停止测试(让qunit等待)或者在准备好时重新启动测试。本例中将执行延时,并将履行包裹在一个运行调用中。因为链接的承诺在承诺履行之前开始观察,所以不需要将链接的承诺包裹在一个运行循环中。

异步承诺示例

AJAX

AJAX请求是最典型的会创建承诺的用例。在测试的时候,可能希望模拟发送到服务器端的AJAX请求。下面给出一些ic-ajax的示例,不过有一点非常重要,就是Mockajax和其他的一些库并不知道运行循环,并不会将履行包裹在一个运行调用中。这可能导致承诺的履行不在一个运行循环中,从而发生错误。

ic-ajax

[ic-ajax]是一个Ember友好的jQuery-ajax封装,可以非常方便的用来构造夹具数据和在单元测试和集成测试中模拟ajax请求。最常见的承诺用例就是发送一个异步请求给一个服务器,ic-ajax可以不需要担心是否将resolve包裹在一个运行调用中。

ic-ajax简单示例

假设希望从服务器请求一个颜色列表。使用ic-ajax将采用如下的语法:

  1. var promise = ic.ajax.request('/colors');

这个异步调用将返回一个承诺。当承诺被履行时,其将包含颜色列表。ic-ajax的一个惯例是会将ajax调用的完成包裹到Ember.run中,这样就不需要担心这个异步行为。因此可以通过构造夹具数据来取代向一个模拟服务器发送请求来测试代码。

  1. ic.ajax.defineFixture('/colors', {
  2. response: [
  3. {
  4. id: 1,
  5. color: "red"
  6. },
  7. {
  8. id: 2,
  9. color: "green"
  10. },
  11. {
  12. id: 3,
  13. color: "blue"
  14. }
  15. ],
  16. jqXHR: {},
  17. textStatus: 'success'
  18. });

Using ic-ajax

使用Ember Data的ic-ajax简单示例

只需要将数据格式定义为Ember Data期望的格式,即可完成Ember Data的处理。

Using ic-ajax

使用ic-ajax和Ember Data的集成测试

在进行集成测试时,通常不希望向服务器发送真实的请求,因为这样应用的状态会出现不一致的情况。采用之前的模式,可以这是真实的ajax调用响应返回的数据,以此来独立测试代码。下面给出了一个简单的使用ic-ajax和Ember Data的测试示例。

Using ic-ajax

jquery-mockjax

jquery-mockjax是一个jQuery插件,用来模拟ajax请求。

jquery-mockjax简单示例

假设希望从服务器请求一个颜色列表。使用jQuery的方法如下所示:

  1. $.getJSON('/colors', function(response){ /* ... */ });

这个异步调用会将服务器的响应作为参数传给回调函数。与ic-ajax不同,这里需要将回调包裹到一个承诺中。

  1. var promise = new Ember.RSVP.Promise(function(resolve){
  2. $.getJSON('/colors', function(data){
  3. resolve(data.response);
  4. });
  5. });

下面将设置一些夹具数据,以便可以取代向一个模拟服务器发送ajax请求来获取数据,从而进行代码测试。

  1. $.mockjax({
  2. type: 'GET',
  3. url: '/colors',
  4. status: '200',
  5. dataType: 'json',
  6. responseText: {
  7. response: [
  8. {
  9. id: 1,
  10. color: "red"
  11. },
  12. {
  13. id: 2,
  14. color: "green"
  15. },
  16. {
  17. id: 3,
  18. color: "blue"
  19. }
  20. ]
  21. }
  22. });

由此可见,使用jquery-mockjax api可以非常的灵活。不仅可以设定url和响应,还可以设定方法、状态代码和数据类型。jquery-mockjax的完整api请查看文档

Using jquery-mockjax

Ember Data的jquery-mockjax简单示例

只需要将数据格式定义为Ember Data期望的格式,即可完成Ember Data的处理。

Using jquery-mockjax

使用jquery-mockjax和Ember Data的集成测试

在进行集成测试时,通常不希望向服务器发送真实的请求,因为这样应用的状态会出现不一致的情况。采用之前的模式,可以这是真实的ajax调用响应返回的数据,以此来独立测试代码。下面给出了一个简单的使用jquery-mockjax和Ember Data的测试示例。

Using jquery-mockjax