Подтвердить что ты не робот

BindToController в модульных тестах

Я использую bindToController в директиве, чтобы иметь изолированную область, непосредственно прикрепленную к контроллеру, например:

app.directive('xx', function () {
  return {
    bindToController: true,
    controller: 'xxCtrl',
    scope: {
      label: '@',
    },
  };
});

Тогда в контроллере у меня есть значение по умолчанию, если метка в HTML не указана:

app.controller('xxCtrl', function () {
  var ctrl = this;

  ctrl.label = ctrl.label || 'default value';
});

Как я могу создать экземпляр xxCtrl в модульных тестах Jasmine, чтобы я мог проверить ctrl.label?

describe('buttons.RemoveButtonCtrl', function () {
  var ctrl;

  beforeEach(inject(function ($controller) {
    // What do I do here to set ctrl.label BEFORE the controller runs?
    ctrl = $controller('xxCtrl');
  }));

  it('should have a label', function () {
    expect(ctrl.label).toBe('foo');
  });
});

Отметьте этот, чтобы проверить проблему.

4b9b3361

Ответ 1

В Angular 1.3 (см. ниже для 1.4 +)

Копаясь в исходном коде AngularJS, я нашел недокументированный третий аргумент службе $controller, называемой later (см. $controller source).

Если true, $controller() возвращает функцию с свойством instance, на котором вы можете установить свойства.
Когда вы будете готовы создать экземпляр контроллера, вызовите функцию, и он будет создавать экземпляр контроллера со свойствами, доступными в конструкторе.

Ваш пример будет работать следующим образом:

describe('buttons.RemoveButtonCtrl', function () {

  var ctrlFn, ctrl, $scope;

  beforeEach(inject(function ($rootScope, $controller) {
    scope = $rootScope.$new();

    ctrlFn = $controller('xxCtrl', {
      $scope: scope,
    }, true);
  }));

  it('should have a label', function () {
    ctrlFn.instance.label = 'foo'; // set the value

    // create controller instance
    ctrl = ctrlFn();

    // test
    expect(ctrl.label).toBe('foo');
  });

});

Здесь обновленный Plunker (пришлось обновить Angular, чтобы он работал, теперь это 1.3.0-rc.4): http://plnkr.co/edit/tnLIyzZHKqPO6Tekd804?p=preview

Обратите внимание, что, вероятно, не рекомендуется использовать его, чтобы процитировать исходный код Angular:

Мгновенный запуск контроллера: этот механизм используется для создания экземпляр объекта перед вызовом конструктора контроллера сам по себе.

Это позволяет добавлять свойства к контроллеру до вызывается конструктор. В первую очередь это используется для выделения области привязки в компиляции $.

Эта функция не предназначена для использования приложениями и, следовательно, не является задокументировано публично.

Однако отсутствие механизма тестирования контроллеров с bindToController: true заставило меня использовать его, тем не менее. Возможно, ребятам из Angular следует подумать о том, чтобы сделать этот флаг общедоступным.

Под капотом он использует временный конструктор, мы также можем написать его сами, я думаю.
Преимущество вашего решения состоит в том, что конструктор не вызывается дважды, что может вызвать проблемы, если свойства не имеют значений по умолчанию, как в вашем примере.

Angular 1.4+ (обновление 2015-12-06):
Команда Angular добавила прямую поддержку для этого в версии 1.4.0. (См. # 9425)
Вы можете просто передать объект функции $controller:

describe('buttons.RemoveButtonCtrl', function () {

  var ctrl, $scope;

  beforeEach(inject(function ($rootScope, $controller) {
    scope = $rootScope.$new();

    ctrl = $controller('xxCtrl', {
      $scope: scope,
    }, {
      label: 'foo'
    });
  }));

  it('should have a label', function () {
    expect(ctrl.label).toBe('foo');
  });
});

Смотрите также этот пост в блоге.

Ответ 2

Тестирование устройств BindToController с использованием ES6

Если вы используете ES6, вы можете напрямую импортировать контроллер и протестировать, не используя angular mocks.

Директива

import xxCtrl from './xxCtrl';

class xxDirective {
  constructor() {
    this.bindToController = true;
    this.controller = xxCtrl;
    this.scope = {
      label: '@'
    }
  }
}

app.directive('xx',  new xxDirective());

Контроллер:

class xxCtrl {
  constructor() {
    this.label = this.label || 'default value';
  }
}

export default xxCtrl;

Тест контроллера:

import xxCtrl from '../xxCtrl';

describe('buttons.RemoveButtonCtrl', function () {

  let ctrl;

  beforeEach(() => {
    xxCtrl.prototype.label = 'foo';
    ctrl = new xxCtrl(stubScope);
  });

  it('should have a label', () => {
    expect(ctrl.label).toBe('foo');
  });

});

см. это для получения дополнительной информации: Надлежащее модульное тестирование Angular JS приложения с модулями ES6

Ответ 3

На мой взгляд, этот контроллер не предназначен для тестирования изолированно, потому что он никогда не будет работать изолированно:

app.controller('xxCtrl', function () {
  var ctrl = this;

  // where on earth ctrl.lable comes from???
  ctrl.newLabel = ctrl.label || 'default value';
});

Он тесно связан с директивой, полагающейся на получение ее свойств области. Он не может использоваться повторно. От взгляда на этот контроллер, я должен задаться вопросом, откуда эта переменная. Это не лучше, чем негерметичная функция, использующая переменную из внешней области:

function Leaky () {

    ... many lines of code here ...

    // if we are here we are too tired to notice the leakyVariable:
    importantData = process(leakyVariable);

    ... mode code here ...

    return unpredictableResult;
}

Теперь у меня есть нечеткая функция, поведение которой очень непредсказуемо на основе переменной leakyVariable присутствует (или нет) в любой области, вызываемой функцией.

Неудивительно, что эта функция - это кошмар для тестирования. Это на самом деле хорошая вещь, возможно, чтобы заставить разработчика переписать функцию на нечто более модульное и повторно используемое. Что не так сложно:

function Modular (outsideVariable) {
    ... many lines of code here ...

    // no need to hit our heads against the wall to wonder where the variable comes from:
    importantData = process(outsideVariable);

    ... mode code here ...

    return predictableResult;   
}

Отсутствие проблем с утечкой и очень простое тестирование и повторное использование. Который мне говорит, что использование старого старого $scope - лучший способ:

app.controller('xxCtrl', function ($scope) {
  $scope.newLabel = $scope.label || 'default value';
});

Простой, короткий и простой в тестировании. Кроме того, нет большого количества объектов с объективом.

Первоначальная аргументация синтаксиса controllerAs - это нечеткая область, унаследованная от родителя. Однако директивная изолированная область уже решает эту проблему. Таким образом, я не вижу причин использовать более сильный синтаксис утечки.

Ответ 4

Я нашел способ, который не особенно элегантен, но работает как минимум (если есть лучший вариант, оставляйте комментарий).

Мы устанавливаем значение, которое "приходит" из директивы, а затем снова вызываем функцию контроллера, чтобы проверить, что она делает. Я сделал помощника "invokeController" более сухим.

Например:

describe('buttons.RemoveButtonCtrl', function () {

  var ctrl, $scope;
  beforeEach(inject(function ($rootScope, $controller) {
    scope = $rootScope.$new();
    ctrl = $controller('xxCtrl', {
      $scope: scope,
    });
  }));

  it('should have a label', function () {
    ctrl.label = 'foo'; // set the value

    // call the controller again with all the injected dependencies
    invokeController(ctrl, {
      $scope: scope,
    });

    // test whatever you want
    expect(ctrl.label).toBe('foo');
  });

});


beforeEach(inject(function ($injector) {
  window.invokeController = function (ctrl, locals) {
    locals = locals || {};
    $injector.invoke(ctrl.constructor, ctrl, locals);
  };
}));