AngularJS单元测试模拟文档

18
我将尝试测试Angular服务,该服务通过$document服务对DOM进行一些操作,并使用Jasmine进行测试。 简单来说,它会将某些指令附加到<body>元素上。
这样的服务可能如下所示:
(function(module) {
    module.service('myService', [
        '$document',
        function($document) {
            this.doTheJob = function() {
                $document.find('body').append('<my-directive></my directive>');
            };
        }
    ]);
})(angular.module('my-app'));

我想这样测试它

describe('Sample test' function() {
    var myService;

    var mockDoc;

    beforeEach(function() {
        module('my-app');

        // Initialize mock somehow. Below won't work indeed, it just shows the intent
        mockDoc = angular.element('<html><head></head><body></body></html>');

        module(function($provide) {
            $provide.value('$document', mockDoc);
        });
    });

    beforeEach(inject(function(_myService_) {
        myService = _myService_;
    }));

    it('should append my-directive to body element', function() {
        myService.doTheJob();
        // Check mock's body to contain target directive
        expect(mockDoc.find('body').html()).toContain('<my-directive></my-directive>');
    });
});

那么问题是,创建这样的模拟的最佳方法是什么?

使用真实的document进行测试会给我们带来很多麻烦,每次测试后都需要清理,并且看起来不是正确的方法。

我也尝试在每个测试之前创建一个新的真实文档实例,但最终遇到了不同的失败。

创建以下对象并检查whatever变量可以工作,但看起来非常丑陋。

var whatever = [];
var fakeDoc = {
    find: function(tag) {
              if (tag == 'body') {
                  return function() {
                      var self = this;
                      this.append = function(content) {
                          whatever.add(content);
                          return self;
                      };
                  };
              } 
          }
}

我感觉我在这里缺少重要的东西,做了一些非常错误的事情。

非常感谢任何帮助。

2个回答

17

在这种情况下,您不需要嘲笑$document服务。更容易的方法是直接使用其实际实现:

您不需要嘲笑$document服务,在这种情况下使用它的实际实现会更简单。

describe('Sample test', function() {
    var myService;
    var $document;

    beforeEach(function() {
        module('plunker');
    });

    beforeEach(inject(function(_myService_, _$document_) {
        myService = _myService_;
        $document = _$document_;
    }));

    it('should append my-directive to body element', function() {
        myService.doTheJob();
        expect($document.find('body').html()).toContain('<my-directive></my-directive>');
    });
});

Plunker 在这里

如果您确实需要进行模拟,那么我想您将不得不按照您所做的方式进行:

$documentMock = { ... }

但是这可能会破坏依赖于$document服务本身的其他东西(例如使用createElement的指令)。

更新

如果您需要在每个测试后将文档恢复到一致的状态,您可以按照以下方式操作:

afterEach(function() {
    $document.find('body').html(''); // or $document.find('body').empty()
                                     // if jQuery is available
});

这里是Plunker 链接(我不得不使用另一个容器,否则Jasmine的结果将无法呈现)。

正如@AlexanderNyrkov在评论中指出的那样,Jasmine和Karma都有它们自己的东西在body标签内部,通过清空文档内容来清除它们似乎不是一个好主意。

更新2

我已经部分模拟了$document服务,所以您可以使用实际的页面文档并将一切恢复到有效状态:

beforeEach(function() {
    module('plunker');

    $document = angular.element(document); // This is exactly what Angular does
    $document.find('body').append('<content></content>');

    var originalFind = $document.find;
    $document.find = function(selector) {
      if (selector === 'body') {
        return originalFind.call($document, 'body').find('content');
      } else {
        return originalFind.call($document, selector);
      }
    }

    module(function($provide) {
      $provide.value('$document', $document);
    });        
});

afterEach(function() {
    $document.find('body').html('');
});

在这里查看 Plunker 链接

这个想法是用一个新的标签来替换原有的标签,使你的被测系统可以自由地操纵它,并且在每个规范结束时安全地清除测试。


谢谢您的回复。这种方法会在测试之间创建依赖关系。假设我们的服务在每次调用doTheJob时向document添加一些内容,然后通过调用cleanup方法删除添加的元素。如果某个清理操作失败,则所有依赖于文档内容的其他测试都可能失败。恢复每个测试后文档的原始状态可能是可能的,但这可能与原始问题一样棘手。 - Alexander Nyrkov
假设您的测试将向文档“body”添加内容,那么在每个“afterEach”块中执行$document.find('body').html('')(如果有jQuery,则为$document.find('body').empty())即可将所有内容恢复到原始状态。我会更新答案以说明这一点。 - Michael Benford
@AlexanderNyrkov 这很奇怪。在回答你的第一条评论之前,我已经使用了我的一个测试套件(其中包括Jasmine和Karma),一切都运行良好。:\ 我会再次检查以确保。 - Michael Benford
@AlexanderNyrkov 我想我明白了。实际上,正如你所说,Karma将他的东西放在body标签内,但它只是带有所有必要内容的<script>标签。由于这些脚本已经在第一个测试运行时加载完毕,从body中删除它们似乎不会影响其余的测试。但我同意你的观点,干涉不属于自己的事情并不是一个好主意。 - Michael Benford
这是一个不错的想法,我也在考虑类似的东西,但具体实现方式没有想到。对于简单情况,直接使用清理函数可能更好,但这似乎是目前最好的解决方案,并且可以帮助测试棘手的问题。将其标记为答案。谢谢Michael。 - Alexander Nyrkov
显示剩余2条评论

6

您可以使用DOMImplementation#createHTMLDocument()创建一个空的测试文档:

describe('myService', function() {
  var $body;

  beforeEach(function() {
    var doc;

    // Create an empty test document based on the current document.
    doc = document.implementation.createHTMLDocument();

    // Save a reference to the test document's body, for asserting
    // changes to it in our tests.
    $body = $(doc.body);

    // Load our app module and a custom, anonymous module.
    module('myApp', function($provide) {
      // Declare that this anonymous module provides a service
      // called $document that will supersede the built-in $document
      // service, injecting our empty test document instead.
      $provide.value('$document', $(doc));
    });

    // ...
  });

  // ...
});

因为您每次测试都会创建一个新的空文档,所以不会干扰页面运行测试,并且您不必在测试之间显式地清理服务。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接