programing

컨트롤러가 필요한 테스트 지침

codeshow 2023. 3. 13. 20:40
반응형

컨트롤러가 필요한 테스트 지침

그래서 또 다른 질문을 봤어요디렉티브 UT에서 필요한 디렉티브 컨트롤러를 조롱하는 방법은 기본적으로 저의 문제이지만, 이 스레드에 대한 답은 "설계 변경"이었습니다.나는 이것을 할 방법이 없는지 확인하고 싶었다.어린이용 디렉티브에 의해 사용되는 컨트롤러를 선언하는 디렉티브가 있습니다.현재 어린이 디렉티브의 재스민 테스트를 작성하려고 하는데 컨트롤러에 의존하기 때문에 테스트에서 컴파일할 수 없습니다.다음은 예를 제시하겠습니다.

addressModule.directive('address', ['$http', function($http){
        return {
            replace: false,
            restrict: 'A',
            scope: {
                config: '='
            },
            template:   '<div id="addressContainer">' +
                            '<div ng-if="!showAddressSelectionPage" basic-address config="config"/>' +
                            '<div ng-if="showAddressSelectionPage" address-selector addresses="standardizedAddresses"/>' +
                        '</div>',
            controller: function($scope)
            {
                this.showAddressInput = function(){
                    $scope.showAddressSelectionPage = false;
                };

                this.showAddressSelection = function(){
                    $scope.getStandardizedAddresses();
                };

                this.finish = function(){
                    $scope.finishAddress();
                };
            },
            link: function(scope, element, attrs) {
              ...
            }
       }
}])

자 지시문:

addressModule.directive('basicAddress360', ['translationService', function(translationService){
        return {
            replace: true,
            restrict: 'A',
            scope: {
                config: '='
            },
            template:
                '...',
            require: "^address360",
            link: function(scope, element, attrs, addressController){
            ...
            }
       }
}])

재스민 테스트:

it("should do something", inject(function($compile, $rootScope){
            parentHtml = '<div address/>';
            subDirectiveHtml = '<div basic-address>';

            parentElement = $compile(parentHtml)(rootScope);
            parentScope = parentElement.scope();
            directiveElement = $compile(subDirectiveHtml)(parentScope);
            directiveScope = directiveElement.scope();
            $rootScope.$digest();
}));

서브 디렉티브를 재스민으로 테스트할 수 있는 방법은 없습니까?그렇다면 어떤 점이 부족합니까?컨트롤러 기능이 없어도 디렉티브 자체를 테스트할 수 있다면 좋겠습니다.

다음 두 가지 방법을 생각할 수 있습니다.

1) 두 가지 지시를 모두 사용한다.

예를 들어 다음과 같은 지침이 있다고 가정합니다.

app.directive('foo', function() {
  return {
    restrict: 'E',
    controller: function($scope) {
      this.add = function(x, y) {
        return x + y;
      }
    }
  };
});

app.directive('bar', function() {
  return {
    restrict: 'E',
    require: '^foo',
    link: function(scope, element, attrs, foo) {
      scope.callFoo = function(x, y) {
        scope.sum = foo.add(x, y);
      }
    }
  };
});

테스트하기 위해서callFoomethod, 당신은 단순히 명령어를 컴파일 할 수 있습니다.bar사용하다foo의 실장:

it('ensures callFoo does whatever it is supposed to', function() {
  // Arrange
  var element = $compile('<foo><bar></bar></foo>')($scope);
  var barScope = element.find('bar').scope();

  // Act
  barScope.callFoo(1, 2);

  // Assert
  expect(barScope.sum).toBe(3);
});    

현업용 플런커

2) 모의 foo 컨트롤러 출력

이것은 매우 간단하지 않고 조금 까다롭다.사용할 수 있습니다.element.controller()요소의 컨트롤러를 가져와 재스민으로 시뮬레이션을 실시합니다.

it('ensures callFoo does whatever it is supposed to', function() {
    // Arrange
    var element = $compile('<foo><bar></bar></foo>')($scope);
    var fooController = element.controller('foo');
    var barScope = element.find('bar').scope();
    spyOn(fooController, 'add').andReturn(3);

    // Act
    barScope.callFoo(1, 2);

    // Assert
    expect(barScope.sum).toBe(3);
    expect(fooController.add).toHaveBeenCalledWith(1, 2);
  });

현업용 플런커

한 디렉티브가 다른 디렉티브의 컨트롤러를 바로 사용하는 경우 까다로운 부분이 나타납니다.link기능:

app.directive('bar', function() {
  return {
    restrict: 'E',
    require: '^foo',
    link: function(scope, element, attrs, foo) {
      scope.sum = foo.add(parseInt(attrs.x), parseInt(attrs.y));
    }
  };
});

이 경우 각 디렉티브를 개별적으로 컴파일하여 첫 번째 디렉티브를 목격한 후 두 번째 디렉티브를 사용해야 합니다.

it('ensures callFoo does whatever it is supposed to', function() {
  // Arrange
  var fooElement = $compile('<foo></foo>')($scope);
  var fooController = fooElement.controller('foo');
  spyOn(fooController, 'add').andReturn(3);

  var barElement = angular.element('<bar x="1" y="2"></bar>')
  fooElement.append(barElement);

  // Act
  barElement = $compile(barElement)($scope);
  var barScope = barElement.scope();

  // Assert
  expect(barScope.sum).toBe(3);
  expect(fooController.add).toHaveBeenCalledWith(1, 2);
});

현업용 플런커

첫 번째 접근방식은 두 번째 접근방식보다 훨씬 쉽지만 첫 번째 명령의 구현에 의존합니다. 즉, 유닛 테스트 방식이 아닙니다.한편, 디렉티브의 컨트롤러를 조롱하는 것은 그리 쉬운 일이 아니지만, 테스트 제어가 강화되어 첫 번째 디렉티브에 대한 의존성이 없어집니다.그러니 현명하게 고르세요.:)

마지막으로 위의 모든 작업을 보다 쉽게 수행할 수 있는 방법이 무엇인지 모르겠습니다.더 나은 방법을 아는 사람이 있다면 제 답변을 개선해 주세요.

마이클 벤포드의 (환상적인) 답변을 포기하다.

테스트에서 컨트롤러와 디렉티브를 완전히 분리하려면 조금 다른 접근법이 필요합니다.

3) 필요한 부모 컨트롤러를 완전히 조롱하다

컨트롤러를 디렉티브에 관련지으면 컨트롤러 인스턴스가 요소의 데이터스토어에 저장됩니다.키 값의 명명 규칙은 '$' + 지시문 이름 + 'Controller'입니다.Angular는 필요한 컨트롤러를 해결하려고 할 때마다 이 규칙을 사용하여 데이터 계층을 통과하여 필요한 컨트롤러를 찾습니다.이는 모의 컨트롤러 인스턴스를 부모 요소에 삽입하면 쉽게 조작할 수 있습니다.

it('ensures callFoo does whatever it is supposed to', function() {

    // Arrange

    var fooCtrl = {
      add: function() { return 123; }
    };

    spyOn(fooCtrl, 'add').andCallThrough();

    var element = angular.element('<div><bar></bar></div>');
    element.data('$fooController', fooCtrl);

    $compile(element)($scope);

    var barScope = element.find('bar').scope();

    // Act

    barScope.callFoo(1, 2);

    // Assert

    expect(barScope.sum).toBe(123);
    expect(fooCtrl.add).toHaveBeenCalled();
});

현업용 플런커

4) 분리 링크 방식

최선의 접근법은 링크 방식을 분리하는 것이라고 생각합니다.지금까지의 접근방식은 모두 실제로 너무 많은 테스트를 거치고 있으며, 여기에 제시된 단순한 예보다 상황이 조금 복잡해지면 셋업이 너무 많이 필요합니다.

Angular는 이러한 우려의 분리를 완벽하게 지원합니다.

// Register link function

app.factory('barLinkFn', function() {
  return function(scope, element, attrs, foo) {
    scope.callFoo = function(x, y) {
      scope.sum = foo.add(x, y);
    };
  };
});

// Register directive

app.directive('bar', function(barLinkFn) {
  return {
    restrict: 'E',
    require: '^foo',
    link: barLinkFn
  };
});

그리고 각각 링크 기능을 포함하도록 이전 버전을 변경함으로써...:

inject(function(_barLinkFn_) {
  barLinkFn = _barLinkFn_;
});

...할 수 있는 것은 다음과 같습니다.

it('ensures callFoo does whatever it is supposed to', function() {

  // Arrange

  var fooCtrl = {
    add: function() { return 321; }
  };

  spyOn(fooCtrl, 'add').andCallThrough();

  barLinkFn($scope, $element, $attrs, fooCtrl);

  // Act

  $scope.callFoo(1, 2);

  // Assert

  expect($scope.sum).toBe(321);
  expect(fooCtrl.add).toHaveBeenCalled();

});

현업용 플런커

이 방법에서는 관련된 것만 테스트하고 있습니다.필요한 경우 컴파일 기능을 분리하기 위해 동일한 접근 방식을 사용할 수 있습니다.

5) 지시적 정의 주입 및 제어기 기능 조롱

또 다른 접근법은 지침의 정의를 주입하고 우리가 필요한 것은 무엇이든 조롱하는 것이다.가장 좋은 점은 부모에게 의존하지 않고 자녀에게 지시하는 단위 테스트를 완전히 작성할 수 있다는 것입니다.

inject()를 사용하면 디렉티브 + 'Directive'의 이름을 제공하는 디렉티브 정의를 삽입하여 메서드에 액세스하여 필요에 따라 치환할 수 있습니다.

it('ensures callFoo does whatever it is supposed to', inject(function(fooDirective) {
  var fooDirectiveDefinition = fooDirective[0];

  // Remove any behavior attached to original link function because unit
  // tests should isolate from other components
  fooDirectiveDefinition.link = angular.noop;

  // Create a spy for foo.add function
  var fooAddMock = jasmine.createSpy('add');

  // And replace the original controller with the new one defining the spy
  fooDirectiveDefinition.controller = function() {
    this.add = fooAddMock;
  };

  // Arrange
  var element = $compile('<foo><bar></bar></foo>')($scope);
  var barScope = element.find('bar').scope();

  // Act
  barScope.callFoo(1, 2);

  // Verify that add mock was called with proper parameters
  expect(fooAddMock).toHaveBeenCalledWith(1, 2);
}));

아이디어는 Angular의 Daniel Tabuenca에 의해 제안되었습니다.JS 구글 그룹

플런커에서는 다니엘이 ngModel 디렉티브를 조롱하고 있습니다.

언급URL : https://stackoverflow.com/questions/19227036/testing-directives-that-require-controllers

반응형