web-dev-qa-db-ja.com

より高いレベルのディレクティブの単体テストを有効にするには、ディレクティブをどのようにモックしますか?

このアプリには、ネストされたディレクティブの複数のレイヤーがあります。私はトップレベルのディレクティブのためのいくつかのユニットテストを書き込もうとしています。ディレクティブ自体が必要とするものをあざけりましたが、今では下位レベルのディレクティブからエラーに遭遇しています。最上位レベルのディレクティブの単体テストでは、下位レベルのディレクティブで何が行われているのか心配する必要はありません。下位レベルのディレクティブをモックしたいだけで、基本的には何もしないので、トップレベルのディレクティブを単独でテストできます。

私はこのようなことをしてディレクティブ定義を上書きしようとしました:

angular.module("myModule").directive("myLowerLevelDirective", function() {
    return {
        link: function(scope, element, attrs) {
            //do nothing
        }
    }
});

ただし、これは上書きせず、実際のディレクティブに加えてこれを実行するだけです。これらの低レベルのディレクティブが、トップレベルのディレクティブのユニットテストで何かを行うのを止めるにはどうすればよいですか?

48
dnc253

ディレクティブ登録の実装により、既存のディレクティブをモックされたディレクティブで置き換えることはできないようです。

ただし、下位レベルのディレクティブからの干渉なしに上位レベルのディレクティブを単体テストする方法はいくつかあります。

1)単体テストテンプレートで下位レベルのディレクティブを使用しないでください:

下位レベルのディレクティブが上位レベルのディレクティブによって追加されない場合、単体テストでは、higer-level-directiveのみを含むテンプレートを使用します。

var html = "<div my-higher-level-directive></div>";
$compile(html)(scope);

したがって、低レベルのディレクティブは干渉しません。

2)ディレクティブ実装でサービスを使用します:

サービスによって、低レベルのディレクティブリンク機能を提供できます。

angular.module("myModule").directive("myLowerLevelDirective", function(myService) {
    return {
        link: myService.lowerLevelDirectiveLinkingFunction
    }
});

その後、ユニットテストでこのサービスをモックして、より高いレベルのディレクティブとの干渉を回避できます。このサービスは、必要に応じてディレクティブオブジェクト全体を提供することもできます。

3)terminalディレクティブで下位レベルのディレクティブを上書きできます

angular.module("myModule").directive("myLowerLevelDirective", function(myService) {
    return {
        priority: 100000,
        terminal: true,
        link: function() {
            // do nothing
        }
    }
});

端末オプションとより高い優先度を使用すると、実際の低レベルのディレクティブは実行されません。 directive doc の詳細情報。

これでどのように機能するかを確認してください Plunker

29
Bastien Caudan

ディレクティブは単なるファクトリーであるため、これを行う最善の方法は、module関数(通常はbeforeEachブロック)を使用してディレクティブのファクトリをモックすることです。 do-something-elseと呼ばれるディレクティブで使用されるdo-somethingという名前のディレクティブがあると仮定すると、そのようにモックすることになります。

beforeEach(module('yourapp/test', function($provide){
  $provide.factory('doSomethingDirective', function(){ return {}; });
}));

// Or using the shorthand sytax
beforeEach(module('yourapp/test', { doSomethingDirective: {} ));

次に、テストでテンプレートがコンパイルされると、ディレクティブがオーバーライドされます

inject(function($compile, $rootScope){
  $compile('<do-something-else></do-something-else>', $rootScope.$new());
});

コンパイラが内部的にこれを行うため、名前に「Directive」サフィックスを追加する必要があることに注意してください: https://github.com/angular/angular.js/blob/821ed310a75719765448e8b15e3a56f0389107a5/src/ng/compile.js #L5

79
trodrigues

ディレクティブをモックするクリーンな方法は、$compileProviderを使用することです

beforeEach(module('plunker', function($compileProvider){
  $compileProvider.directive('d1', function(){ 
    var def = {
      priority: 100,
      terminal: true,
      restrict:'EAC',
      template:'<div class="mock">this is a mock</div>',
    };
    return def;
  });
}));

元のディレクティブがコンパイルされないように、モックの優先度をモックしているディレクティブよりも高くし、モックがターミナルであることを確認する必要があります。

priority: 100,
terminal: true,

結果は次のようになります。

このディレクティブを指定すると:

var app = angular.module('plunker', []);
app.directive('d1', function(){
  var def =  {
    restrict: 'E',
    template:'<div class="d1"> d1 </div>'
  }
  return def;
});

次のようにモックできます:

describe('testing with a mock', function() {
var $scope = null;
var el = null;

beforeEach(module('plunker', function($compileProvider){
  $compileProvider.directive('d1', function(){ 
    var def = {
      priority: 9999,
      terminal: true,
      restrict:'EAC',
      template:'<div class="mock">this is a mock</div>',
    };
    return def;
  });
}));

beforeEach(inject(function($rootScope, $compile) {
  $scope = $rootScope.$new();
  el = $compile('<div><d1></div>')($scope);
}));

it('should contain mocked element', function() {
  expect(el.find('.mock').length).toBe(1);
});
});

さらにいくつかのこと:

  • モックを作成するとき、replace:truetemplateが必要かどうかを考慮する必要があります。たとえば、ng-srcをモックしてバックエンドへの呼び出しを防ぐ場合、replace:trueは不要であり、templateは指定したくありません。しかし、視覚的なものをモックする場合は、そうしたいかもしれません。

  • 優先度を100以上に設定すると、モックの属性は補間されません。 $ compile source code を参照してください。たとえば、ng-srcをモックしてpriority:101を設定すると、モックではng-src="{{variable}}"ではなくng-src="interpolated-value"になります。

これは配管工です すべてのものです。私を正しい方向に向けてくれた@trodriguesに感謝します。

ここにいくつかのドキュメントがあります 詳細については、「構成ブロック」セクションを参照してください。 @ebelangerに感謝します!

65
Sylvain

$templateCache内のテンプレートを変更して、より低いレベルのディレクティブを削除できます。

beforeEach(angular.mock.inject(function ($templateCache) {
  $templateCache.put('path/to/template.html', '<div></div>');
}));
5
Cinamonas

大好き Sylvain's answer とても多く、私はそれをヘルパー関数に変えなければなりませんでした。ほとんどの場合、必要なのは、依存関係なしで親コンテナディレクティブをコンパイルおよびテストできるように、子ディレクティブを強制終了することです。したがって、このヘルパーを使用すると次のことができます。

function killDirective(directiveName) {
  angular.mock.module(function($compileProvider) {
    $compileProvider.directive(directiveName, function() {
      return {
        priority: 9999999,
        terminal: true
      }
    });
  });
}

これにより、インジェクタが作成される前にを実行することにより、ディレクティブを完全に無効にできます。

killDirective('myLowerLevelDirective');
3
Merott

このことについて自分で考えることを余儀なくされ、私たちのニーズを満たす解決策を思いつきました。ディレクティブはすべて属性であるため、単体テストで使用するattributeRemoverディレクティブを作成しました。次のようになります。

angular.module("myModule").directive("attributeRemover", function() {
    return {
        priority: -1, //make sure this runs last
        compile: function(element, attrs) {
            var attributesToRemove = attrs.attributeRemover.split(",");
            angular.forEach(attributesToRemove, function(currAttributeToRemove) {
                element.find("div[" + currAttributeToRemove + "]").removeAttr(currAttributeToRemove);
            });
        }
    }
});

次に、テストしているディレクティブのhtmlは次のようになります。

<div my-higher-level-directive attribute-remover="my-lower-level-directive,another-loweler-level-directive"></div>

そうするとき my-higher-level-directiveattribute-removerはすでに下位レベルのディレクティブの属性を削除しているので、それらが何をしているのか心配する必要はありません。

おそらくすべての種類のディレクティブ(属性のディレクティブだけでなく)に対してこれを行うより堅牢な方法があり、組み込みのJQLiteを使用するだけでこれが機能するかどうかはわかりませんが、必要なものには機能します。

3
dnc253

別の小さなアイデアがあります。このコードをジャスミンヘルパーに追加するだけです(コーヒースクリプト)

window.mockDirective = (name, factoryFunction) ->
  mockModule = angular.module('mocks.directives', ['ng'])
  mockModule.directive(name, factoryFunction)

  module ($provide) ->
    factoryObject = angular.injector([mockModule.name]).get("#{name}Directive")
    $provide.factory "#{name}Directive", -> factoryObject
    null

そしてそれを使用します:

beforeEach mockDirective, "myLowerLevelDirective", ->
  link: (scope, element) ->

これにより、指定されたディレクティブの他のすべての実装が完全に削除され、ディレクティブに渡された引数をテストするためのフルアクセスが提供されます。たとえば、mm.foundationアラートディレクティブは次のようにモックできます。

beforeEach mockDirective 'alert', ->
  scope:
    type: '='

そしてテストしました:

expect(element.find('alert').data('$isolateScopeNoTemplate').type).toEqual 
1
BroiSatse