Angular js unit test mock document

angularjs, document, javascript, testing, unit-testing

Solution

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.

Problem

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.

Original source