0

I have code that is something like this:

function doThing() {
  if (invalidInput) {
    console.error('Invalid input.');
    return;
  }

  $timeout(function() {
    MyService.doThing();
  }, 1000);
}

I want to test that MyService.doThing isn't called when invalid input is passed in.

If I call doThing(invalidInput) without doing $timeout.flush(), MyService.doThing wouldn't be called, regardless of whether I have lines 2-5. So to really test whether MyService.doThing is called when invalid input is passed in, I need to call $timeout.flush.

The problem is that it throws an error if I try to flush when there's nothing to flush. Error: No deferred tasks to be flushed.

How can I handle this scenario? I'd like to do something like, $timeout.flushIfFlushable().

Adam Zerner
  • 17,797
  • 15
  • 90
  • 156

2 Answers2

0

I suggest define two separate unit tests to verify the behavior of your doThing function. Try following code:

  • Controller
(function () {
  'use strict';

  angular.module('myApp', [])
    .controller('MainCtrl', function ($timeout, MyService) {
      var vm = this;

      vm.invalidInput = true;
      vm.doThing = doThing;

      function doThing() {
        if (vm.invalidInput) {
          return;
        }

        $timeout(function () {
          MyService.doThing();
        }, 1000);
      }

    });
})();
  • Service
(function () {
  'use strict';

  angular.module('myApp').service('MyService', MyService);

  function MyService() {

    this.doThing = function () {
      // doThing code
    };
  }
})();
  • Unit test
'use strict';

describe('Controller: MainCtrl', function () {

  beforeEach(module('myApp'));

  var vm,
    $timeout,
    MyService;

  beforeEach(inject(function (_$controller_, _$timeout_, _MyService_) {
    $timeout = _$timeout_;
    MyService = _MyService_;
    vm = _$controller_('MainCtrl', {
      $timeout: $timeout,
      MyService: MyService
    });
  }));

  it('should call doThing for valid inputs', function () {
    spyOn(MyService, 'doThing').andCallThrough();

    vm.invalidInput = false;
    vm.doThing();
    $timeout.flush();
    expect(MyService.doThing).toHaveBeenCalled();
  });

  it('should not call doThing for invalid inputs', function () {
    spyOn(MyService, 'doThing').andCallThrough();

    vm.invalidInput = true;
    vm.doThing();
    expect(MyService.doThing).not.toHaveBeenCalled();
  });

});

With the first test we expect to call MyService.doThing() function. On other hand, if you have invalidInput as true, the previous function should not be called.

I hope It helps.

luixaviles
  • 691
  • 5
  • 8
  • Yeah, but then the logic for checking and handling invalid inputs is going to be duplicated wherever the service is used. – Adam Zerner May 13 '16 at 21:57
  • Actually you're not going to duplicate any logic in the unit test of the controller, since It will delegate to the actual implementation of the service. Also think about we're testing your function to determine whether it behave as expected. – luixaviles May 13 '16 at 22:00
  • I don't understand. In the controller, you'd have to check to see if the input is valid or not, and then conditionally call `MyService.doThing`, right? And then if you wanted to use `MyService.doThing` in `Controller2`, you'd have to also check to see if the input is valid or not, and then conditionally call `MyService.doThing` in `Controller2` also, right? – Adam Zerner May 13 '16 at 22:05
  • I don't understand your new scenario. However, the code I provided is in favor to have a good test coverage for your function. – luixaviles May 13 '16 at 22:34
0

No uncertainty is welcome in unit tests, it should be predictable whether there is something to flush for $timeout or not.

In this piece of code two cases should be tested, in both $timeout shouldn't be a blackbox, it should be a stub instead (optionally a spy that wraps around the real service).

beforeEach(module('app', ($provide) => {
  $provide.decorator('$timeout', ($delegate) => {
    var timeoutSpy = jasmine.createSpy().and.returnValue($delegate);
    // methods aren't copied automatically to spy
    return angular.extend(timeoutSpy, $delegate);
  });
}));

The first is falsey invalidInput:

...
MyService.doThing();
expect($timeout).not.toHaveBeenCalled();

And the second one is truthy invalidInput:

...
MyService.doThing();
expect($timeout).toHaveBeenCalledWith(jasmine.any(Function), 1000);
$timeout.flush();
expect(MyService.doThing).toHaveBeenCalledTimes(2);

Disregarding this case, it is generally a good thing to return promises from promise-powered functions:

function doThing() {
  if (invalidInput) {
    console.error('Invalid input.');
    return;
  }

  return $timeout(function() {
    return MyService.doThing();
  }, 1000);
}

This way the callers (most likely specs) may have some control on function's asynchronous behaviour.


Answering the question directly, the expected way to do 'flushIfFlushable()' is

try {
  $timeout.verifyNoPendingTasks(); // just for semantics, not really necessary
  $timeout.flush();
} catch (e) {}

Which should be avoided for the reasons listed above.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565