Unit test failing on Angular service with promises

130 views Asked by At

JSFiddle showing the problem:

http://jsfiddle.net/ggvfwoeL/3/

I have an Angular application that involves a service for generating threats. The threat generation is done using the nools rule engine. The service exposes a function generateForElement that returns a promise. The function looks like this (see the JSFiddle for full details):

var Element = function (element) { this.type = element.attributes.type; }
var Threats = function () { this.collection = []; }    

    function generateForElement(element) {
           var threats = new Threats();
           var el = new Element(element);
           flow.getSession(threats, el).match();
           return $q.when(threats.collection);
    }

The rule engine is set up so that when you call generateForElement with an element that is a tm.Process type, it generates 2 threats. Here is the code that defines the rules (obviously this is much simplified to make the question clearer):

function initialiseFlow() {
        return nools.flow('Element threat generation', function (flow) {

            flow.rule('Spoofing Threat Rule', [
                [Element, 'el', 'el.type == "tm.Process"'],
                [Threats, 'threats']
            ], function (facts) {
                facts.threats.collection.push('Spoofing');
            });

            flow.rule('Tampering Threat Rule', [
                [Element, 'el', 'el.type == "tm.Process"'],
                [Threats, 'threats']
            ], function (facts) {
                facts.threats.collection.push('Tampering');
            });
        });
    }

When I manually test this in my application I see the 2 threats being generated. But my unit test is failing on this line

expect(threats.length).toEqual(2);

With error

Error: Expected 1 to equal 2.

So it appears that only 1 threat is being generated. The unit test definition is like this:

describe('threatengine service', function () {

    var threatengine;
    var $rootScope;

    beforeEach(function () {

        angular.mock.module('app')

        angular.mock.inject(function (_$rootScope_, _threatengine_) {
            threatengine = _threatengine_;
            $rootScope = _$rootScope_;
        });

        $rootScope.$apply();
    });

    describe('threat generation tests', function () {

        it('process should generate two threats', function () {

            var element = { attributes: { type: 'tm.Process' }};
            var threats;
            threatengine.generateForElement(element).then(function (data) {
                threats = data;
            });
            expect(threats).toBeUndefined();
            $rootScope.$apply();
            expect(threats).toBeDefined();
            expect(threats.length).toEqual(2);
        });
    });
});

Clearly I am doing something wrong. As I said, when I run the full application I definitely get 2 threats which make me think the fault is either with the unit test, or maybe with how I am handling the promises in my service, but I just can't see it.

Why is my unit test failing?

1

There are 1 answers

6
Michael Radionov On BEST ANSWER

First problem is in flow.getSession(threats, el).match(); call which is called synchronously in your code, but originally it seems to be asynchronous (I am not familiar with nools, but here are the docs). So even if you place console.log inside handlers for both rules, you'll see that rules get processed way more later than your following sync code. The solution for it is to use a promise, which .match() returns:

function generateForElement(element) {
    var threats = new Threats();
    var el = new Element(element);
    // handle async code via promises and resolve it with custom collection
    return flow.getSession(threats, el).match().then(function () {
      return threats.collection;
    });
}

The other issue is in a test file. There, you also have async code, but you process it like sync code. See Asynchronous Support in Jasmine docs. Basically, you have to tell Jasmine if your tests are async and notify it when they are done.

it('process should generate two threats', function (done) {
    // telling Jasmine that code is async ----------^^^

    var element = { attributes: { type: 'tm.Process' }};

    // this call is async
    threatengine.generateForElement(element).then(function (threats) {
        expect(threats).toBeUndefined();
        expect(threats).toBeDefined();
        expect(threats.length).toEqual(2);

        done(); // telling Jasmine that code has completed
    });

    // is required to start promises cycle if any
    $rootScope.$apply();
});

Here is a working JSFiddle.

Update:

Here is a spec for Jasmine 1.3, it uses another API for async flow:

it('process should generate two threats', function (done) {

    var element = { attributes: { type: 'tm.Process' }};
    var threats;

    runs(function () {
        threatengine.generateForElement(element).then(function (data) {
            threats = data;
        });
        $rootScope.$apply();
        });

        waitsFor(function () {
            return typeof threats !== 'undefined';
    });

    runs(function () {
        expect(threats).not.toBeUndefined();
        expect(threats).toBeDefined();
        expect(threats.length).toEqual(20);
    });

});