Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS TypeScript unit testing

I am struggle with create proper unit tests for the angularjs (v1.4.9) application which contains both javascript files (with jasmine tests) and typescript files (with no tests at all, now I am trying to use Mocha, but it can be any framework).

Hence it hybrid and an old angularjs without modules, I decided to compile all .ts to one bundle.js file, due to avoid files ordering problem (which occurs when I have single .js file per .ts and inject it with gulp task to index.html).

My tsconfig.js:

{
    "compileOnSave": true,
    "compilerOptions": {
        "noImplicitAny": false, 
        "removeComments": false,
        "outFile": "./wwwroot/bundle.js",
        "sourceMap": true,
        "inlineSources": true,
        "module": "amd",
        "moduleResolution": "node",
        "target": "es5",
        "sourceRoot": "./wwwroot"        
    },
    "include": [
      "wwwroot/app/**/*"
    ],
    "exclude": [
      "node_modules/**/*",
      "tests/**/*"      
    ]
}

example of tested class:

///<reference path="../models/paymentCondition.model.ts"/>
///<reference path="../../../../../node_modules/@types/angular/index.d.ts"/>

'use strict';


module PaymentCondition {

    export class ConnectedCustomersListController {
        name: string;

        static $inject = ['paymentCondition'];
        constructor(private paymentCondition: PaymentConditionModel) {
            this.name = paymentCondition.Name;
            this.bindData();
        }



        bindData() {
            // do something
        }                
    }

    angular
        .module('app.paymentConditions')
        .controller('ConnectedCustomersListController', ConnectedCustomersListController);
}

My module declaration:

///<reference path="../../../../node_modules/@types/angular/index.d.ts"/>

'use strict';

module PaymentCondition {

    angular.module('app.paymentConditions', ['ui.router', 'ui.bootstrap']);
}

and I am 'injecting' this module to main module file, which is already in javascript- App.module.js.:

(function () {
    'use strict';

    var module = angular.module('app', [       
        'app.paymentCondition',
        'ui.router',     
        'ui.bootstrap',        
    ]);

})();

and finally my test class:

///<reference path="../../../node_modules/@types/angular/index.d.ts"/>
///<reference path="../../../wwwroot/app/domain/paymentConditions/connectedCustomersList/connectedCustomersList.controller.ts"/>
///<reference path="../../../node_modules/@types/angular-mocks/index.d.ts"/>

import { expect } from 'chai';
import "angular-mocks/index";
import * as angular from "angular";


describe("app.paymentConditions.connectedCustomersList", () => {
    var mock;
    // inject main module
    beforeEach(angular.mock.module('app.paymentConditions'));
    beforeEach(angular.mock.inject(($controller: ng.IControllerService) => {

        mock = {           
            connectedCustomersListModel: {
                columnDefinitions() {
                }
            },
            paymentCondition: {},
            createController(): PaymentCondition.ConnectedCustomersListController {
                return $controller<PaymentCondition.ConnectedCustomersListController >('ConnectedCustomersListController', {
                    connectedCustomersListModel: mock.connectedCustomersListModel,

                });
            }
        };
    }));

    describe("ConnectedCustomersListController", () => {
        var controller: PaymentCondition.ConnectedCustomersListController;
        beforeEach(() => {
            controller = mock.createController();
        });

        it("should be defined", () => {
            expect(controller).not.undefined;
        });
    });
});

when I am trying to run mocha tests with command:

./node_modules/.bin/mocha --compilers ts:ts-node/register ./tests/**/*.spec.ts

I have this exception:

ReferenceError: define is not defined
    at Object.<anonymous> (C:\Projects\App.Frontend\EasyFrontend\src\EasyFrontend\tests\paymentConditions\connec
edCustomersList\connectedCustomersList.controller.spec.ts:5:1)
    at Module._compile (module.js:643:30)
    at Module.m._compile (C:\Projects\App.Frontend\EasyFrontend\src\EasyFrontend\node_modules\ts-node\src\index.
s:422:23)
    at Module._extensions..js (module.js:654:10)
    at Object.require.extensions.(anonymous function) [as .ts] (C:\Projects\App.Frontend\EasyFrontend\src\EasyFr
ntend\node_modules\ts-node\src\index.ts:425:12)
    at Module.load (module.js:556:32)
    at tryModuleLoad (module.js:499:12)
    at Function.Module._load (module.js:491:3)
    at Module.require (module.js:587:17)
    at require (internal/module.js:11:18)
    at C:\Projects\App.Frontend\EasyFrontend\src\EasyFrontend\node_modules\mocha\lib\mocha.js:231:27
    at Array.forEach (<anonymous>)
    at Mocha.loadFiles (C:\Projects\App.Frontend\EasyFrontend\src\EasyFrontend\node_modules\mocha\lib\mocha.js:2
8:14)
    at Mocha.run (C:\Projects\App.Frontend\EasyFrontend\src\EasyFrontend\node_modules\mocha\lib\mocha.js:536:10)
    at Object.<anonymous> (C:\Projects\App.Frontend\EasyFrontend\src\EasyFrontend\node_modules\mocha\bin\_mocha:
82:18)
    at Module._compile (module.js:643:30)
    at Object.Module._extensions..js (module.js:654:10)
    at Module.load (module.js:556:32)
    at tryModuleLoad (module.js:499:12)
    at Function.Module._load (module.js:491:3)
    at Function.Module.runMain (module.js:684:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3
npm ERR! Test failed.  See above for more details.

I know it is because I am using amd module to compile my typescript to one js file, but I don't really know how to fix it. Or if it can't be fixed maybe you have some advices how to 'marrige' the type script to existing AngularJs solution.

Ps. I am using mocha with backed typescript compiler, because I have no idea how to run jasmine tests with this combination.

My Index.html:

<!DOCTYPE html>
<html>

<head ng-controller="AppCtrl">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta lang="da" />
    <title>{{ Page.title() }}</title>


   <!-- endbuild -->
    <!-- inject:css -->
    <link rel="stylesheet" type="text/less" href="less/site.less" />
    <!-- endinject -->
    <!-- build:remove -->
    <script src="less/less.js"></script>
    <!-- endbuild -->    
    <!-- bower:js -->
    <script src="lib/jquery/dist/jquery.js"></script>
    <script src="lib/bootstrap/dist/js/bootstrap.js"></script>
    <script src="lib/angular/angular.js"></script>
    <script src="lib/toastr/toastr.js"></script>
    <script src="lib/angular-ui-router/release/angular-ui-router.js"></script>
    <script src="lib/angular-ui-grid/ui-grid.js"></script>
    <script src="lib/angular-bootstrap/ui-bootstrap-tpls.js"></script>
    <script src="lib/sugar/release/sugar-full.development.js"></script>
    <script src="lib/ng-context-menu/dist/ng-context-menu.js"></script>
    <script src="lib/ng-messages/angular-messages.js"></script>
    <script src="lib/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js"></script>
    <script src="lib/bootstrap-datepicker/dist/locales/bootstrap-datepicker.da.min.js"></script>
    <script src="lib/angular-ui-tree/dist/angular-ui-tree.js"></script>
    <script src="lib/angular-sanitize/angular-sanitize.js"></script>
    <script src="lib/color-hash/dist/color-hash.js"></script>
    <script src="lib/angular-ui-mask/dist/mask.js"></script>
    <script src="lib/google-maps-js-marker-clusterer/src/markerclusterer.js"></script>
    <script src="lib/ngDraggable/ngDraggable.js"></script>
    <script src="lib/requirejs/require.js"></script>
    <!-- endbower -->
    <!-- endbuild -->
    <!-- build:site_js js/site.min.js -->
    <!-- inject:app:js -- >   
    <script src="bundle.js"></script>
    <script src="app/app.module.js"></script>  
    <script src="app/app.route.config.js"></script>
    <script src="app/app.module.config.js"></script>
    <script src="app/app.constants.js"></script>
    <script src="app/app.appCtrl.js"></script>       
    <!-- endinject -->
    <!-- endbuild -->
    <!-- endbuild -->
    <!-- build:remove -->
    <script src="init.js"></script>
    <!-- endbuild -->    
</head>

<body>
    <div class="fluid-container">
        <ee-global-context-menu></ee-global-context-menu>
        <ui-view></ui-view>
    </div>
</body>
</html>
like image 388
Michał Jarzyna Avatar asked Jan 26 '18 16:01

Michał Jarzyna


People also ask

How do I run a unit test in AngularJS?

Load the Angular App beforeEach(module('MyApp')); //3. Describe the object by name describe('compute', function () { var compute; //4. Initialize the filter beforeEach(inject(function ($filter) { compute = $filter('compute', {}); })); //5. Write the test in the it block along with expectations.

Is AngularJS code unit testable?

AngularJS is written with testability in mind, but it still requires that you do the right thing. We tried to make the right thing easy, but if you ignore these guidelines you may end up with an untestable application.


1 Answers

Hence it hybrid and an old angularjs without modules

You have stated that you are not using modules but you in fact you are.

The tsconfig.json you have shown indicates that you have configured TypeScript to transpile your code to AMD modules. Furthermore, your index.html is set up accordingly as you are in fact using an AMD loader, namely RequireJS.

All of this is well and good. You should use modules and doing so with AngularJS is not only possible but easy.

However, ts-node, which is great by the way, takes your TypeScript code, and then automatically transpiles and runs it. When it does this, it loads the settings from your tsconfig.json, instantiates a TypeScript compiler passing those settings, compiles your code, and then passes the result to Node.js for execution.

NodeJS is not an AMD module environment. It does not support AMD and does not provide a define function.

There are several valid ways to execute your tests.

One option is to use different configuration for ts-node, specifically, tell it to output CommonJS modules instead of AMD modules. This will produce output that Node.js understands.

Something like

./node_modules/.bin/mocha --compilers ts:ts-node/register --project tsconfig.tests.json

where tsconfig.tests.json looks like

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true
  },
  "include": ["tests/**/*.spec.ts"]
}

Bear in mind that AMD and CommonJS modules have different semantics and, while it is you will likely never hit any of their differences in your test code, your code will using different loaders for your tests than your production code.

Another option is to use an AMD compliant loader in node to run your tests. You might be able to do this with mocha's --require option. e.g.

mocha --require requirejs

Remarks:

You have some mistakes in your code that should be addressed even if they are not the direct cause of your issue, they relate to modules, paths, and the like.

  • Do not use /// <reference path="..."/> to load declaration files. The compiler will pick them up automatically.

  • Do not use the module keyword to create namespaces in your TypeScript code. This is long deprecated and was removed because it introduced terminological confusion. Use the namespace keyword instead.

  • Never mix module syntax, import x from 'y', and /// <reference path="x.ts"/> to actually load code.

    In other words, in your test, replace

    ///<reference path="../../../wwwroot/app/domain/paymentConditions/connectedCustomersList/connectedCustomersList.controller.ts"/>
    

    with

    import "../../../wwwroot/app/domain/paymentConditions/connectedCustomersList/connectedCustomersList.controller.ts";
    

    at once!

    After this change, your test will look like

    import "../../../wwwroot/app/domain/paymentConditions/connectedCustomersList/connectedCustomersList.controller.ts";
    import chai from 'chai';
    import "angular-mocks/index"; // just like controller.ts
    import angular from "angular";
    const expect = chai.expect;
    

    This is serious. Don't think about, just do it.

  • Consider converting your entire code base to proper modules. AngularJS works fine with this approach and it will reduce overall complexity in your toolchain while making your system better factored and your code easier to maintain and reuse.

    The idea would be to eventually change

    namespace PaymentConditions {
      angular.module('app.paymentConditions', ['ui.router', 'ui.bootstrap']);
    }
    

    to

    import angular from 'angular';
    import uiRouter from 'angular-ui-router';
    import uiBootstrap from 'angular-ui-bootstrap';
    
    import ConnectedCustomersListController from './connectedCustomersList/connectedCustomersList.controller';
    
    const paymentConditions = angular.module('app.paymentConditions', [
        uiRouter,
        uiBootstrap
      ])
      .controller({
        ConnectedCustomersListController
      });
    
    export default paymentConditions;
    

    with your controller being

    export default class ConnectedCustomersListController {
      static $inject = ['paymentCondition'];
    
      name: string;
    
      constructor(public paymentCondition: PaymentConditionModel) {
        this.name = paymentCondition.Name;
        this.bindData();
      }
    
      bindData() {
        // do something
      }                
    }
    
like image 141
Aluan Haddad Avatar answered Sep 27 '22 22:09

Aluan Haddad