Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular js unit test mock document

I am trying to test angular service which does some manipulations to DOM via $document service with jasmine. Let's say it simply appends some directive to the <body> element.

Such service could look like

(function(module) {
    module.service('myService', [
        '$document',
        function($document) {
            this.doTheJob = function() {
                $document.find('body').append('<my-directive></my directive>');
            };
        }
    ]);
})(angular.module('my-app'));

And I want to test it like this

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>');
    });
});

So the question is what would be the best way to create such mock?

Testing with real document will give us much trouble cleaning up after each test and does not look like a way to go with.

I've also tried to create a new real document instance before each test, yet ended up with different failures.

Creating an object like below and checking whatever variable works but looks very ugly

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

I feel that I'm missing something important here and doing something very wrong.

Any help is much appreciated.

like image 651
Alexander Nyrkov Avatar asked Dec 01 '13 15:12

Alexander Nyrkov


2 Answers

You don't need to mock the $document service in such a case. It's easier just to use its actual implementation:

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 here.

If you really need to mock it out, then I guess you'll have to do it the way you did:

$documentMock = { ... }

But that can break other things that rely on the $document service itself (such a directive that uses createElement, for instance).

UPDATE

If you need to restore the document back to a consistent state after each test, you can do something along these lines:

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

Plunker here (I had to use another container otherwise Jasmine results wouldn't be rendered).

As @AlexanderNyrkov pointed out in the comments, both Jasmine and Karma have their own stuff inside the body tag, and wiping them out by emptying the document body doesn't seem like a good idea.

UPDATE 2

I've managed to partially mock the $document service so you can use the actual page document and restore everything to a valid state:

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 here.

The idea is to replace the body tag with a new one that your SUT can freely manipulate and your test can safely clear at the end of every spec.

like image 114
Michael Benford Avatar answered Oct 30 '22 23:10

Michael Benford


You can create an empty test document using 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));
    });

    // ...
  });

  // ...
});

Because you're creating a new, empty document for each test, you won't interfere with the page running your tests and you won't have to explicitly clean up after your service between tests.

like image 21
Ian Lesperance Avatar answered Oct 30 '22 23:10

Ian Lesperance