ngDeveloper
ngDeveloper

Reputation: 1304

In Angular unit testing, how to clean-up $timeout promises so they don't persist?

I have a series of unit tests. One of the controllers is setting a $timeout promise which executes every 1000 ms (it invokes its own function, so it behaves like an $interval, continues executing every 1 second as long as the controller is being viewed). However, when the controller is destroyed (user navigates to another view), the $timeout.cancel is called accurately, thus the self-invoking ceases to continue.

But when running unit tests, for some reason after the test for this controller is executed, the $destroy is never triggered.

So when second unit test is run (in this case a unit test for a directive), which also uses $timeout.flush(...), it is activating the $timeout promise in the unassociated controller!

But I don't know how to force the controller to $destroy after its unit tests are completed, or how to nuke $timeout to remove/cancel all promises before starting the other unit tests.

As a result, it's causing a conflict between the two unit tests, where one has unexpected behavior that is causing it to fail.

Any suggestions?

[Edit] Solution: By adding scope.$destroy() to all our controller and directive unit tests, we went from 30 seconds of unit test execution time to 6 seconds! And the $timeout conflict no longer appears.

afterEach(function () { 
    scope.$destroy(); //directive, or controller: $scope.$destroy();
});

Upvotes: 1

Views: 953

Answers (2)

vpsingh016
vpsingh016

Reputation: 703

You can inject $timeout service when instantiating controllers and use the timeout.flush() to get the callback executed and test. Updated Plunker with real $timeout service.

Upvotes: 1

Stubb0rn
Stubb0rn

Reputation: 1402

I would suggest to avoid creating real timeouts while unit-testing. That would require injecting mock of $timeout service when instantiating controllers, but provides more control over asynchronous operations based on timeouts during unit-testing. In simplest case when there is no need to test timeout callbacks $timeout mock can be very simple (example uses Jasmine):

describe('MainCtrl', function() {
var $scope = null;
var ctrl = null;
var mockedTimeout = function() {
  console.log('Timeout is mocked');
}
mockedTimeout.cancel = function() {
  console.log('Timeout is cancelled');
}

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

  ctrl = $controller('MainCtrl', {
    $scope: $scope,
    $timeout: mockedTimeout
  });
}));
});

Here is plunker that shows complete test setup in action

Upvotes: 1

Related Questions