AngularJS TypeScript 单元测试

AngularJS TypeScript unit testing

我正在努力为 angularjs (v1.4.9) 应用程序创建适当的单元测试,该应用程序包含 javascript 文件(带有 jasmine 测试)和打字稿文件(根本没有测试,现在我正在尝试使用 Mocha,但它可以是任何框架)。

因此它混合了一个没有模块的旧 angularjs,我决定将所有 .ts 编译成一个 bundle.js 文件,以避免文件排序问题(当我有单个 .js 时发生每个 .ts 文件并将其与 gulp 任务一起注入到 index.html).

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

测试示例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);
}

我的模块声明:

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

'use strict';

module PaymentCondition {

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

我是'injecting'这个模块到主模块文件,它已经在javascript- App.module.js.:

(function () {
    'use strict';

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

})();

最后是我的测试 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;
        });
    });
});

当我尝试 运行 mocha 测试命令时:

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

我有这个例外:

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.

我知道这是因为我正在使用 amd 模块将我的打字稿编译成一个 js 文件,但我真的不知道如何修复它。或者,如果它无法修复,也许您有一些建议如何将类型脚本 'marrige' 现有 AngularJs 解决方案。

Ps。我正在将 mocha 与支持的打字稿编译器一起使用,因为我不知道如何 运行 使用此组合进行 jasmine 测试。

我的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>

Hence it hybrid and an old angularjs without modules

您曾声明您没有使用模块,但实际上您在使用。

您显示的 tsconfig.json 表示您已配置 TypeScript 将您的代码转换为 AMD 模块。此外,您的 index.html 是相应设置的,因为您实际上使用的是 AMD 加载程序,即 RequireJS。

这一切都很好。您应该 使用模块,而使用 AngularJS 不仅可能而且简单。

然而,顺便说一下,ts-node 很棒,它会获取您的 TypeScript 代码,然后自动转译并 运行s 它。当它执行此操作时,它会从您的 tsconfig.json 加载设置,实例化一个传递这些设置的 TypeScript 编译器,编译您的代码,然后将结果传递给 Node.js 执行。

NodeJS 不是 AMD 模块环境。它不支持 AMD,也不提供 define 功能。

有几种有效的方法可以执行您的测试。

一个选择是对 ts-node 使用不同的配置,具体来说,告诉它输出 CommonJS 模块而不是 AMD 模块。这将产生 Node.js 理解的输出。

类似

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

其中 tsconfig.tests.json 看起来像

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

请记住,AMD 和 CommonJS 模块具有不同的语义,虽然您可能永远不会在测试代码中找到它们的任何差异,但您的代码将使用与生产代码不同的加载器进行测试。

另一种选择是在节点中使用 AMD 兼容的加载器来 运行 您的测试。您可以使用 mocha 的 --require 选项来做到这一点。例如

mocha --require requirejs

备注:

您的代码中存在一些应该解决的错误,即使它们不是问题的直接原因,它们与模块、路径等相关。

  • 不要使用/// <reference path="..."/>加载声明文件。编译器会自动拾取它们。

  • 不要使用 module 关键字在 TypeScript 代码中创建命名空间。这已被长期弃用并被删除,因为它引入了术语混淆。请改用 namespace 关键字。

  • 永不 混合模块语法,import x from 'y'/// <reference path="x.ts"/> 实际加载代码。

    换句话说,在你的测试中,替换

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

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

    马上!

    此更改后,您的测试将如下所示

    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;
    

    这很严重。别想,就去做。

  • 考虑将整个代码库转换为适当的模块。 AngularJS 使用这种方法效果很好,它将降低工具链的整体复杂性,同时使您的系统更好地分解,并且您的代码更易于维护和重用。

    这个想法最终会改变

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

    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;
    

    你的控制器是

    export default class ConnectedCustomersListController {
      static $inject = ['paymentCondition'];
    
      name: string;
    
      constructor(public paymentCondition: PaymentConditionModel) {
        this.name = paymentCondition.Name;
        this.bindData();
      }
    
      bindData() {
        // do something
      }                
    }
    

感谢 Aluan Haddad 我有解决方案,没有对旧 Javascript 代码进行任何重构,它没有模块(任何类型)也没有特殊 node.js装载机。所有 .js 脚本文件都包含在 index.html 中,我不想更改它。在这里,我展示了在我的测试开始工作之前我必须修复的所有错误:

模块和命名空间

因为我的应用程序没有模块加载器,所以我无法在不弄乱文件引用的情况下使用任何类型的打字稿模块。有趣的是,当文件开头有任何 importexport 关键字时,typescript 编译器会将所有文件视为模块。所以我决定使用 namespaces 并在整个应用程序中坚持使用它们:

namespace PaymentCondition {

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

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

对文件的引用

主要错误是将 ///<referenceimport angularimport angular from angular 等导入作为文件之间的依赖项混合使用。这不是一个好主意。正如我之前提到的 - typescript 将文件开头带有 import 关键字的所有文件视为 module 并且因为我使用的是 namespaces 我不得不将所有依赖项更改为 ///<reference :

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

module PaymentCondition {

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

包含文件的顺序

现在,当我有有效的 .ts 个文件时,我可以将它们编译成一个 bundle.js。由于正确的 ///<reference... 声明,我不担心正确的文件排序。为了能够将所有 .ts 编译为一个 .js,我必须选择 amd 模块作为默认 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/**/*"      
    ]
}

现在,要启动我的应用程序,我只需要将 bundle.js 文件添加到 index.html

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

Git忽略

当然文件bundle.jsbundle.js.map也可以添加到git.ignore因为它们是自动生成的。它确实有助于避免经常合并冲突 :)

单元测试

我的第二个大错误是决定使用 Mocha 作为单元测试 运行ner。这是 node.js 运行ner 并且要测试 angular 没有解决方法的应用程序,建议使用一些类似浏览器的解决方案。我决定使用 Chutzpath (because already have it in project for javascript unit testing) with jasmine

为此,我添加了新的 tsconfig.test.js,其中包括所有 .spec.ts 测试文件和 .ts 应用程序文件,并将它们一起编译成一些 git .ignore 作为分隔文件的路径,因为这是 chuzpath 配置方式最容易读取的(TODO:添加 tsconfig 示例)

接下来,我只将这条路径包含到 chutzpath 配置和神奇的工作中 - 所有打字稿测试现在都 运行ning 就像一个魅力。 (TODO:添加 chutzpath 配置示例)。

Gulp

在此解决方案中,所有 .ts 文件必须在测试 运行 之前编译。为了自动执行此操作,我正在使用 gulp watch(TODO:添加 gulp 观察者配置)