AngularJS TypeScript unit testing

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