Friday, August 28, 2015

Testing an AMD POJO Angular Controller

One of the nice things of Controllers in ASP.NET MVC is they are easy to test, just new the controller and you are good to go.

One of the nice things with new Angular removing reliance on $scope is we can now test controllers just by newing them, we don't need to worry where to get the $scope variable being passed to it. Our controller becomes very POJO, very easy to test.


An example of POJO controller.

///<reference path="../../../typings/requirejs/require.d.ts"/>
///<reference path="../../../typings/angularjs/angular.d.ts"/>
///<reference path="../../../shared/ViewValue/Header.ts"/>
///<reference path="../../../shared/Domain/Product.ts"/>


module App.ProductForSale {

    export class Controller {

        product : Domain.Product;

        percentDiscount: number;

        get discountedPrice() : number {

            return this.product.price * (1 - this.percentDiscount);
        }

        constructor(header: ViewValue.Header) {

            header.title = "Product for sale";

            this.product = new Domain.Product();
        }
    }
}


define(require => {

    var mod : angular.IModule = require('theMainModule');
    require('/shared/Domain/Product.js');

    mod["registerController"]('ProductForSaleController',['singletonHeader', App.ProductForSale.Controller]);

});


There's just one minor problem when testing the controller above. It's an AMD controller, to test it we should be able to ignore the define statement in it.


The easiest way to disable it is to monkey-patch the define function by changing it to something else. An example. Name this test setup as _must-be-runned-first.ts:


if (window["def"] == undefined) {
    window["def"] = window["define"];

    window["define"] = function(depArray : string[], c: any) {
        console.log('define intercepted');
        console.log(depArray);
    };
}



function doTest(test) {
    var def = window["def"];

    def(test);
}

doTest just wrap the test so tests uses the original define, other defines just got redirected.


Here's a sample test:

///<reference path="../typings/jasmine/jasmine.d.ts"/>
///<reference path="../typings/requirejs/require.d.ts"/>
///<reference path="_must-be-runned-first.ts"/>

doTest(require => {


    describe("Board Controller", () => {
        require('/base/public/app-dir/ProductForSale/Controller.js');
        require('/base/shared/ViewValue/Header.js');


        var h = new ViewValue.Header();

        it("computes discount", () => {

            var b = new App.ProductForSale.Controller(h);
            b.product.price = 50;
            b.percentDiscount = 0.10;
            var discountedPrice = 45;

            expect(b.discountedPrice).toEqual(discountedPrice);

        });
    });
});


By redefining the window["define"] to something else, requirejs won't complain of script error on theMainModule as it is not available when doing tests on controller.



Add test-main.js on files array of karma.conf

module.exports = function(config) {
    config.set({

        // base path that will be used to resolve all patterns (eg. files, exclude)
        basePath: '',


        // frameworks to use
        // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
        frameworks: ['jasmine', 'requirejs'],


        // list of files / patterns to load in the browser
        files: [
            'test-main.js',
            {pattern: 'shared/**/*.js', included: false},
            {pattern: 'public/**/*.js', included: false},
            {pattern: 'tests/*.js', included: false}
        ]
    });
}



Then on test-main.js, add the _must-be-runned-first.js as the first file to be dynamically-loaded, remove the .js extension when adding it to allTestFiles array.


var allTestFiles = [];

var TEST_REGEXP = /(spec|test)\.js$/i;

allTestFiles.push('tests/_must-be-runned-first');

// Get a list of all the test files to include
Object.keys(window.__karma__.files).forEach(function(file) {
    if (TEST_REGEXP.test(file)) {
        // Normalize paths to RequireJS module names.
        // If you require sub-dependencies of test files to be loaded as-is (requiring file extension)
        // then do not normalize the paths
        var normalizedTestModule = file.replace(/^\/base\/|\.js$/g, '');

        allTestFiles.push(normalizedTestModule);
    }
});


console.log(allTestFiles);

require.config({
    // Karma serves files under /base, which is the basePath from your config file
    baseUrl: '/base',


    // dynamically load all test files
    deps: allTestFiles,

    // we have to kickoff jasmine, as it is asynchronous
    callback: window.__karma__.start
});



Complete Code: https://github.com/MichaelBuen/PlayNodeTypescriptUirouterCouchpotato/tree/master/public


Happy Coding!

No comments:

Post a Comment