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

123
使用AngularJS。
有一个指令。
指令定义了templateUrl
指令需要进行单元测试。
目前使用Jasmine进行单元测试。 这个建议使用以下代码:
describe('module: my.module', function () {
    beforeEach(module('my.module'));

    describe('my-directive directive', function () {
        var scope, $compile;
        beforeEach(inject(function (_$rootScope_, _$compile_, $injector) {
            scope = _$rootScope_;
            $compile = _$compile_;
            $httpBackend = $injector.get('$httpBackend');
            $httpBackend.whenGET('path/to/template.html').passThrough();
        }));

        describe('test', function () {
            var element;
            beforeEach(function () {
                element = $compile(
                    '<my-directive></my-directive>')(scope);
                angular.element(document.body).append(element);
            });

            afterEach(function () {
                element.remove();
            });

            it('test', function () {
                expect(element.html()).toBe('asdf');
            });

        });
    });
});

在Jasmine中运行代码。
收到错误:
TypeError: Object #<Object> has no method 'passThrough'

需要按原样加载templateUrl

无法使用respond

可能与使用ngMock有关,而不是ngMockE2E的使用。


1
我没有以那种方式使用.passThrough();,但是从文档中来看,你是否尝试过像这样的东西:$httpBackend.expectGET('path/to/template.html'); // do action here $httpBackend.flush(); 我认为这更适合你的用法 - 你不想捕获请求,即whenGet(),而是要检查它是否被发送,然后实际发送它? - Alex Osborn
2
没错,expectGet 只是检查是否尝试了一个请求。 - Alex Osborn
1
啊,我需要一种方法来告诉$httpBackend模拟器实际上使用指令中提供的templateUrl下的URL并获取它。我以为passThrough会做到这一点。你知道其他的方法吗? - Jesus is Lord
2
嗯,我还没有做过太多的端到端测试,但是查看了文档 - 你尝试使用e2e后端了吗?我认为这就是为什么你得到了“no method passThrough”的原因 - http://docs.angularjs.org/api/ngMockE2E.$httpBackend - Alex Osborn
1
@AlexOsborn是正确的,如果您想进行端到端测试,您应该绝对使用ngMockE2E。 - brysgo
显示剩余2条评论
12个回答

186

你说得没错,这与ngMock有关。对于每个Angular测试,ngMock模块都会自动加载并初始化模拟的$httpBackend来处理任何使用$http服务(包括模板获取)的情况。模板系统试图通过$http加载模板,但在模拟中却成为了“意外请求”。

你需要一种方法将模板预先加载到$templateCache中,这样当Angular请求它们时,它们已经可用,而不需要使用$http

首选方案:Karma

如果你正在使用Karma运行测试(你应该这样做),你可以使用ng-html2js预处理器配置它来为你加载模板。Ng-html2js读取你指定的HTML文件,并将它们转换为一个预加载$templateCache的Angular模块。

步骤1:在你的karma.conf.js中启用并配置预处理器。

// karma.conf.js

preprocessors: {
    "path/to/templates/**/*.html": ["ng-html2js"]
},

ngHtml2JsPreprocessor: {
    // If your build process changes the path to your templates,
    // use stripPrefix and prependPrefix to adjust it.
    stripPrefix: "source/path/to/templates/.*/",
    prependPrefix: "web/path/to/templates/",

    // the name of the Angular module to create
    moduleName: "my.templates"
},

如果你正在使用Yeoman来搭建你的应用程序,这个配置将起作用。

plugins: [ 
  'karma-phantomjs-launcher', 
  'karma-jasmine', 
  'karma-ng-html2js-preprocessor' 
], 

preprocessors: { 
  'app/views/*.html': ['ng-html2js'] 
}, 

ngHtml2JsPreprocessor: { 
  stripPrefix: 'app/', 
  moduleName: 'my.templates' 
},

步骤2:在测试中使用该模块

// my-test.js

beforeEach(module("my.templates"));    // load new module containing templates

完整的示例请看Angular测试大师Vojta Jina的经典示例。它包括了整个设置:karma配置、模板和测试。

非Karma解决方案

如果出于某种原因您不使用Karma(我在传统应用程序中有一个不灵活的构建过程),并且只是在浏览器中进行测试,我发现可以通过使用原始XHR获取真实的模板并将其插入到$templateCache中来避开ngMock对$httpBackend的接管。这个解决方案的灵活性要少得多,但目前它能够完成任务。

// my-test.js

// Make template available to unit tests without Karma
//
// Disclaimer: Not using Karma may result in bad karma.
beforeEach(inject(function($templateCache) {
    var directiveTemplate = null;
    var req = new XMLHttpRequest();
    req.onload = function() {
        directiveTemplate = this.responseText;
    };
    // Note that the relative path may be different from your unit test HTML file.
    // Using `false` as the third parameter to open() makes the operation synchronous.
    // Gentle reminder that boolean parameters are not the best API choice.
    req.open("get", "../../partials/directiveTemplate.html", false);
    req.send();
    $templateCache.put("partials/directiveTemplate.html", directiveTemplate);
}));

说真的,使用Karma。它需要一些设置工作,但它可以让你在命令行中同时在多个浏览器中运行所有测试。 因此,您可以将其作为您的持续集成系统的一部分,和/或者您可以从您的编辑器中将其设置为快捷键。比alt-tab-refresh-ad-infinitum好多了。


6
这可能很明显,但如果其他人遇到同样的问题并在这里寻找答案:我无法使它正常运行,除了在 karma.conf.js 文件中的 files 部分中也添加 preprocessors 文件模式(例如 "path/to/templates/**/*.html")。 - Johan
1
那么在继续之前不等待响应是否存在任何重大问题?当请求返回时,它会更新值吗(即需要30秒)? - Jackie
1
我能够在没有Karma的情况下运行grunt-jasmine。 - FlavorScape
5
还有一件事:您需要安装karma-ng-html2js-preprocessor(npm install --save-dev karma-ng-html2js-preprocessor),并将其添加到karma.conf.js配置文件的插件部分中,具体方法请参考https://dev59.com/iGIk5IYBdhLWcg3wrvy4#19077966。 - Vincent
1
如果您正在使用yeoman angular generator和karma,这是适用于我的配置`//启用哪些插件 plugins: [ 'karma-phantomjs-launcher', 'karma-jasmine', 'karma-ng-html2js-preprocessor' ],preprocessors: { 'app/views/*.html': ['ng-html2js'] }, ngHtml2JsPreprocessor: { stripPrefix: 'app/', moduleName: 'templates' },` - Paul Sheldrake
显示剩余11条评论

37
我最终做的是获取模板缓存并将视图放入其中。结果是我无法控制不使用ngMock。
beforeEach(inject(function(_$rootScope_, _$compile_, $templateCache) {
    $scope = _$rootScope_;
    $compile = _$compile_;
    $templateCache.put('path/to/template.html', '<div>Here goes the template</div>');
}));

26
这是我对这种方法的投诉......现在,如果我们要将一个大的HTML片段作为字符串注入到模板缓存中,那么当我们在前端更改HTML时,我们该怎么办?也要同时更改测试中的HTML吗?在我看来,这是一种不可持续的解决方案,也是我们选择使用templateUrl选项的原因。尽管我非常不喜欢将我的HTML作为指令中的大字符串 - 但这是最可持续的解决方案,可以避免更新两个HTML位置。很容易想象HTML随着时间的推移可能不匹配。 - Sten Muchow

13

这个初始问题可以通过添加以下内容来解决:

beforeEach(angular.mock.module('ngMockE2E'));

这是因为它会默认在 ngMock 模块中查找 $httpBackend,但并不完整。


1
这确实是原始问题的正确答案(这是帮助我的那个)。 - Mat
尝试了这个,但 passThrough() 对我仍然不起作用。它仍然会给出“意外请求”错误。 - frodo2975

8
我找到了一个解决方案,需要使用jasmine-jquery.js和代理服务器。我按照以下步骤进行操作:
  1. 在karma.conf文件中:

添加jasmine-jquery.js到你的文件列表中

files = [
    JASMINE,
    JASMINE_ADAPTER,
    ...,
    jasmine-jquery-1.3.1,
    ...
]

添加一个代理服务器,用于提供您的夹具。

proxies = {
    '/' : 'http://localhost:3502/'
};
  1. In your spec

    describe('MySpec', function() { var $scope, template; jasmine.getFixtures().fixturesPath = 'public/partials/'; //custom path so you can serve the real template you use on the app beforeEach(function() { template = angular.element('');

        module('project');
        inject(function($injector, $controller, $rootScope, $compile, $templateCache) {
            $templateCache.put('partials/resources-list.html', jasmine.getFixtures().getFixtureHtml_('resources-list.html')); //loadFixture function doesn't return a string
            $scope = $rootScope.$new();
            $compile(template)($scope);
            $scope.$apply();
        })
    });
    

    });

  2. Run a server on your app's root directory

    python -m SimpleHTTPServer 3502

  3. Run karma.

我花了一段时间才弄清楚这个问题,需要搜索很多帖子。我认为关于这个问题的文档应该更清晰明确,因为它是如此重要。


我在从localhost/base/specs提供资产方面遇到了麻烦,但是通过运行python -m SimpleHTTPServer 3502添加代理服务器解决了问题。您真是个天才! - pbojinov
我在测试中从 $compile 收到一个空元素。其他地方建议运行 $scope.$digest() :仍为空。然而,运行 $scope.$apply() 起作用了。我想这是因为我在我的指令中使用了控制器?不确定。感谢建议!有所帮助! - Sam Simmons

7

我的解决方案:

test/karma-utils.js

function httpGetSync(filePath) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/base/app/" + filePath, false);
  xhr.send();
  return xhr.responseText;
}

function preloadTemplate(path) {
  return inject(function ($templateCache) {
    var response = httpGetSync(path);
    $templateCache.put(path, response);
  });
}

karma.config.js:

files: [
  //(...)
  'test/karma-utils.js',
  'test/mock/**/*.js',
  'test/spec/**/*.js'
],

测试内容:

'use strict';
describe('Directive: gowiliEvent', function () {
  // load the directive's module
  beforeEach(module('frontendSrcApp'));
  var element,
    scope;
  beforeEach(preloadTemplate('views/directives/event.html'));
  beforeEach(inject(function ($rootScope) {
    scope = $rootScope.$new();
  }));
  it('should exist', inject(function ($compile) {
    element = angular.element('<event></-event>');
    element = $compile(element)(scope);
    scope.$digest();
    expect(element.html()).toContain('div');
  }));
});

第一个不强制开发人员使用Karma的可行解决方案。为什么Angular的开发者会在如此酷的东西中做出如此糟糕且容易避免的事情呢?哼! - Fabio Milheiro
我看到你添加了一个 'test/mock/**/*.js',我猜想这是为了加载所有模拟的东西,比如服务等等?我正在寻找避免模拟服务代码重复的方法。你能多给我们展示一些吗? - Stephane
不记得确切的设置,但可能有一些关于$http服务的JSON设置。没有什么花哨的东西。 - bartek
今天遇到了这个问题 - 很棒的解决方案。我们使用karma,但我们也使用Chutzpah - 没有理由强制我们只能使用karma来进行指令的单元测试。 - lwalden
我们正在使用Django和Angular,这对于测试通过static加载其templateUrl的指令非常有效,例如beforeEach(preloadTemplate(static_url +'seed/partials/beChartDropdown.html')); 谢谢! - Aleck Landgraf

6
如果您正在使用Grunt,则可以使用grunt-angular-templates。它会将您的模板加载到templateCache中,并且对于您的specs配置是透明的。
我的示例配置:
module.exports = function(grunt) {

  grunt.initConfig({

    pkg: grunt.file.readJSON('package.json'),

    ngtemplates: {
        myapp: {
          options: {
            base:       'public/partials',
            prepend:    'partials/',
            module:     'project'
          },
          src:          'public/partials/*.html',
          dest:         'spec/javascripts/angular/helpers/templates.js'
        }
    },

    watch: {
        templates: {
            files: ['public/partials/*.html'],
            tasks: ['ngtemplates']
        }
    }

  });

  grunt.loadNpmTasks('grunt-angular-templates');
  grunt.loadNpmTasks('grunt-contrib-watch');

};

6

我用了与所选解决方案略有不同的方法解决了相同的问题。

  1. First, I installed and configured the ng-html2js plugin for karma. In the karma.conf.js file :

    preprocessors: {
      'path/to/templates/**/*.html': 'ng-html2js'
    },
    ngHtml2JsPreprocessor: {
    // you might need to strip the main directory prefix in the URL request
      stripPrefix: 'path/'
    }
    
  2. Then I loaded the module created in the beforeEach. In your Spec.js file :

    beforeEach(module('myApp', 'to/templates/myTemplate.html'));
    
  3. Then I used $templateCache.get to store it into a variable. In your Spec.js file :

    var element,
        $scope,
        template;
    
    beforeEach(inject(function($rootScope, $compile, $templateCache) {
      $scope = $rootScope.$new();
      element = $compile('<div my-directive></div>')($scope);
      template = $templateCache.get('to/templates/myTemplate.html');
      $scope.$digest();
    }));
    
  4. Finally, I tested it this way. In your Spec.js file:

    describe('element', function() {
      it('should contain the template', function() {
        expect(element.html()).toMatch(template);
      });
    });
    

4
要动态加载模板html到$templateCache中,您可以使用html2js karma预处理器,如此处所述。这简化为在conf.js文件中将模板'.html'添加到您的文件中,并使用以下预处理器:

preprocessors = {
  '.html': 'html2js'
};

然后使用:

beforeEach(module('..'));

beforeEach(module('...html', '...html'));

将其添加到您的JS测试文件中。

我遇到了 Uncaught SyntaxError: Unexpected token < 的错误。 - Melbourne2991

2
如果您在测试中使用requirejs,那么可以使用“text”插件来引入HTML模板并将其放入$templateCache中。
require(["text!template.html", "module-file"], function (templateHtml){
  describe("Thing", function () {

    var element, scope;

    beforeEach(module('module'));

    beforeEach(inject(function($templateCache, $rootScope, $compile){

      // VOILA!
      $templateCache.put('/path/to/the/template.html', templateHtml);  

      element = angular.element('<my-thing></my-thing>');
      scope = $rootScope;
      $compile(element)(scope);   

      scope.$digest();
    }));
  });
});

2
如果您正在使用Karma,请考虑使用karma-ng-html2js-preprocessor预编译您的外部HTML模板,避免在测试执行期间Angular尝试HTTP获取它们。我为此苦苦挣扎了几个小时 - 在我的情况下,templateUrl的部分路径在正常应用程序执行期间得到解析,但在测试期间却没有 - 这是由于应用程序与测试目录结构的差异所导致的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接