8

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>
Michał Jarzyna
  • 1,836
  • 18
  • 26

2 Answers2

5

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
      }                
    }
    
Aluan Haddad
  • 29,886
  • 8
  • 72
  • 84
  • Great feedback, now I am working to adopt it to my solution. As I understand in `const paymentConditions =...` I should declare all my controllers, directives and services from my old PaymentConditions module? And because it will register all stuff in `angular.module`, it doesn't require any special steps to inject this `export default paymentConditions` module to my app? Of course except compile TypeScript and include .js files to my `index.html` – Michał Jarzyna Feb 02 '18 at 00:38
  • @MichałJarzyna well, it depends. You will want to remove your application script tags and load your app using the [RequireJS API](http://requirejs.org/docs/api.html#jsfiles) instead. The file that defines your primary module can bootstrap it by calling `angular.bootstrap(document.body, app.name);`. Note that conversion will be a complex process. If your application is large definitely be careful. – Aluan Haddad Feb 02 '18 at 00:42
  • I have last issue before I can end this bounty - you have mentioned, that I can load my app using RequireJS API - this is a lot of work because my application in javascrpit is pretty large. Is there any solution to 'inject' `paymentConditions.module.ts` to my existing app? without touching other files? – Michał Jarzyna Feb 03 '18 at 15:42
  • In my solution, I have main js module, lets say `app.module.js` when I am injecting all other modules. I am invoking it by `angular.bootstrap('appModule')`. And now angular is not finding `paymentConditions` module. When I am changing app.module to ts and importing paymentConditions, the other files are screaming they cant find app.module. The best scenario is to refactor rest of application to AMD support, and use `main.js` to load old app as AMD + compiled bundle.js as AMD too, but I can't transform >500 js files – Michał Jarzyna Feb 03 '18 at 15:44
  • That's why I said that this was more of a long-term plan that you might want to take. You can still apply the primary part of my answer to get your tests working without refactoring your code into modules. Refactoring a large project to use a module system isn't my experience a long effort. It is possible, you don't need to rewrite from scratch but you have to peel off the outer layers, turning them into modules and then you need to declare some Global type shims and so on if you can't convert it all in one go – Aluan Haddad Feb 03 '18 at 16:04
  • @MichałJarzyna see this question/my answer (it got +5 and accepted) for how you would incrementally convert to modules. https://stackoverflow.com/questions/44029788/restructuring-typescript-internal-modules-to-external-modules/44030878#44030878 Again, note that this is not necessary to get your tests running, it is more of a an addendum which is why I left it in remarks and didn't go into detail since question was about loading code. – Aluan Haddad Feb 03 '18 at 16:08
  • @MichałJarzyna definitely let me know if you run into issues running your tests, I would be happy to extend this answer. – Aluan Haddad Feb 05 '18 at 14:37
  • 1
    thanks to you I have developed fully working solution, I will add the comprehensive description later. Your other answers also put more light on cases which are not well documented but important when you want adopt legacy code to TypeScript. True Hero :) – Michał Jarzyna Feb 05 '18 at 15:52
  • I'm really glad to hear that. Thank you for your kind words :) – Aluan Haddad Feb 05 '18 at 15:52
1

Thanks to Aluan Haddad I have solution without any refactor of old Javascript code, which has not got modules (any kind) nor special node.js loader. All .js script files was included to index.html and I would not like to change it. Here I present all mistakes which I have to fix before my tests started to work:

Modules and Namespaces

Because my application hasn't got a module loader I could not use any kind of typescript modules without messing files references. The interesting fact is, the typescript compiler is treating all files as modules when they have any import or export keyword at the beginning of the file. So I decided to use namespaces and stick to them in the whole application:

namespace PaymentCondition {

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

or

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

References to files

The main mistake was to mix ///<reference with some imports like import angular or import angular from angular as dependencies between files. It was not a good idea. As I mentioned before - typescript treats all files with import keyword on the beginning of the file as module and because of I am using namespaces I had to change all dependencies to ///<reference :

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

module PaymentCondition {

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

Order of including files

Now when I have valid .ts files I can compile them to one bundle.js. I have no worries about correct files ordering because of proper ///<reference... declarations. To be able to compile all .ts to one .js I had to choose amd modules as default js output modules.

{
    "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/**/*"      
    ]
}

Now, to start my application I only need to add my bundle.js file to index.html

<script src="bundle.js"></script>

Git ignore

Of course file bundle.js and bundle.js.map also can be added to git .ignore because they are auto-generated. It really helps to avoid often merge conflicts :)

Unit Testing

My second big mistake was decision to use Mocha as unit test runner. This is node.js runner and to test angular application without workarounds is recommended to use some browser-like solution. I decided to use Chutzpath (because already have it in project for javascript unit testing) with jasmine

For this purpose I have added new tsconfig.test.js which includes all .spec.ts test files and .ts app files and compile them together in some git .ignore path as separated files because this is the easiest readable by chuzpath config way (TODO: add tsconfig example)

next I only included this path to chutzpath config and the magic works- all typescript tests now are running like a charm. (TODO: add chutzpath config example).

Gulp

In this solution all .ts files must be compiled before tests run. To automate this operation I am using gulp watch(TODO: add gulp watcher configuration)

Michał Jarzyna
  • 1,836
  • 18
  • 26