使用 Jest 进行单元测试 AngularJS 指令

Unit testing AngularJS Directives with Jest

我觉得我在这个极其简化的 angular 指令单元测试中遗漏了一些重要的东西:

import * as angular from 'angular'
import 'angular-mocks'

const app = angular.module('my-app', [])

app.directive('myDirective', () => ({
    template: 'this does not work either',
    link: (scope, element) => { // have also tried compile fn
        console.log('This does not log')
        element.html('Hi!')
    }
}))

describe('myDirective', () => {
    var element, scope

    beforeEach(app)

    beforeEach(inject(($rootScope, $compile) => {
        scope = $rootScope.$new()
        element = $compile('<my-directive />')(scope)
        scope.$digest()
    }))

    it('should actually do something', () => {
        expect(element.html()).toEqual('Hi!')
    })
})

当 jest 运行时,指令似乎没有被 linked/compiled/whatever

 FAIL  test/HtmlToPlaintextDirective.spec.js
  ● myDirective › should actually do something

    expect(received).toEqual(expected)

    Expected value to equal:
      "Hi!"
    Received:
      ""

更新后的答案:

你是对的,当在单个文件中导入所有内容时,事情并没有按预期工作。

深入研究您似乎 运行 的一些魔法 Babel/Jest 确实支持依赖全局变量的浏览器脚本(如 AngularJS)。

你的模块的 angular 变量 与 angular 可见的全局 angular 变量不同-嘲笑。

您可以通过 运行 在您的一项测试的顶部进行检查:

import * as angular from 'angular'
import 'angular-mocks'

console.log(angular === window.angular); // `false` in Jest!

console.log(angular.mock); // undefined
console.log(window.angular.mock); // `{...}` defined

要解决此问题,您只需在测试中使用全局 angular 变量。

src/__test__/all-in-one.test.js:

import "angular";
import "angular-mocks";

/*
Work around Jest's window/global mock magic.

Use the global version of `angular` that has been augmented by angular-mocks.
*/
var angular = window.angular;


export var app = angular.module('app', []);

app.directive('myDirective', () => ({
    link: (scope, element) => {
        console.log('This does log');
        scope.content = 'Hi!';
    },
    template: 'content: {{content}}'
}));


describe('myDirective', function(){
    var element;
    var scope;

    beforeEach(function(){
        angular.mock.module(app.name);
    });

    it('should do something', function(){
        inject(function(
            $rootScope,
            $compile
        ){
            scope = $rootScope.$new();
            element = $compile('<my-directive></my-directive>')(scope);
            scope.$digest();
        });

        expect(element.html()).toEqual('content: Hi!');
    });
});

原答案:(之所以有效,是因为我在测试中不小心使用了 angular 的全球版本。)

测试中的 Angular 模块未在您的测试中正确初始化。

您对 beforeEach(app) 的调用不正确。

相反,您需要使用 angular.mock.module("moduleName") 来初始化您的模块。

describe('myDirective', () => {
    var element, scope

    // You need to pass the module name to `angular.mock.module()`
    beforeEach(function(){
        angular.mock.module(app.name);
    });


    // Then you can set up and run your tests as normal:
    beforeEach(inject(($rootScope, $compile) => {
        scope = $rootScope.$new()
        element = $compile('<my-directive></my-directive>')(scope)
        scope.$digest()
    }))

    it('should actually do something', () => {
        expect(element.html()).toEqual('Hi!')
    })
});

然后你的测试按我的预期工作:

 PASS  src\__test__\app.test.js
  myDirective
    √ should do something (46ms)

作为参考,这里是完整的应用程序和测试:

src/app/app.module.js:

import * as angular from 'angular'

export var app = angular.module('app', []);

app.directive('myDirective', () => ({
    link: (scope, element) => {
        console.log('This does log');
        scope.content = 'Hi!';
    },
    template: 'content: {{content}}'
}))

src/__test__/app.test.js:

import {app} from "../app/app.module";
import "angular-mocks";

describe('myDirective', function(){
    var element;
    var scope;

    beforeEach(function(){
        angular.mock.module(app.name);
    });

    beforeEach(inject(function(
        $rootScope,
        $compile
    ){
        scope = $rootScope.$new();
        element = $compile('<my-directive></my-directive>')(scope);
        scope.$digest();
    }));

    it('should do something', function(){
        expect(element.html()).toEqual('content: Hi!');
    });
});

几年后我 运行 遇到了同样令人困惑的行为,我想分享我的发现

如果你运行使用 babel 拼写测试并查看导入你会发现类似于下面的内容

var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
var angular = _interopRequireWildcard(require("angular"));
require("angular-mocks");

_interopRequireWildcard 目前有以下实现

function _interopRequireWildcard(obj) {
  if (obj && obj.__esModule) {
    return obj;
  } else {
    var newObj = {};

    if (obj != null) {
      for (var key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
          var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {};

          if (desc.get || desc.set) {
            Object.defineProperty(newObj, key, desc);
          } else {
            newObj[key] = obj[key];
          }
        }
      }
    }

    newObj.default = obj;
    return newObj;
  }
}

简而言之,它 创建一个新对象 并从导入的对象复制所有属性。这就是 angular === window.angularfalse 的原因。它还解释了为什么 angular.mock 没有定义,当 _interopRequireWildcard 复制模块

时它不存在

考虑到除了已接受的答案之外,还有一些其他方法可以解决问题

而不是使用 import * as angular from 'angular' 使用 import angular from 'angular' 应该避免这种行为,因为 _interopRequireDefault 不会 return 不同的对象。 (但是,如果您使用的是 TypeScript,则使用此方法可能无法正确解析 'angular' 的类型)

另一种选择是导入 angular 两次:

import 'angular'
import 'angular-mocks'
import * as angular from 'angular'