1

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?

Mike Goodwin
  • 8,810
  • 2
  • 35
  • 50

1 Answers1

2

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

});
Michael Radionov
  • 12,859
  • 1
  • 55
  • 72
  • Thanks for your answer! I have a small problem with it though - in your fiddle, if you change the expected length of the threats collection, say from 2 to 20, the test still passes!? See this: http://jsfiddle.net/cjbmy5mn/1/ – Mike Goodwin Oct 12 '15 at 17:01
  • Oh, I guess I know what's the reason. I've just noticed that you use Jasmine 1.3, but I've posted my solution for Jasmine 2.x. It is funny that tests were passing though. [Here is a JSFiddle](http://jsfiddle.net/cjbmy5mn/3/) for Jasimine 1.3, it has different API for working with async code ([see the docs](http://jasmine.github.io/1.3/introduction.html#section-Asynchronous_Support)). – Michael Radionov Oct 12 '15 at 18:04
  • I am not sure about nools though, because you said it works for you when doing manual tests, so I'd investigate if it really works ok on a live site with synchronous flow. – Michael Radionov Oct 12 '15 at 18:09
  • In fact I made a mistake - I am using Jasmine 2.x - I adapted an existing fiddle and forgot to check the versions :oS And when I changed the versions on your fiddle it doesn't produce any output :o( Would you be able to do a new one based with Jasmine 2.x - sorry for being a pain, and thanks very much for your help – Mike Goodwin Oct 12 '15 at 20:33
  • I got it working on JSBin - [link](http://jsbin.com/nenelotonu/edit?html,js,output), on JSFiddle I get an empty screen too. Not sure why it fails in there. – Michael Radionov Oct 12 '15 at 21:26
  • Thanks! Finally I got it working on JSFiddle too: http://jsfiddle.net/4yhnnjft/6/. I updated your answer with this link and will now mark is as correct. – Mike Goodwin Oct 12 '15 at 22:20