bier hier bier hier - 4 months ago 79
AngularJS Question

How to unit test statetransition in angular?

I am getting my head around unit test with karma. I want to check the state transitions so I created this test. The app starts off with going to the

landing
state and from there I want to go to
main
. This is a secure route so gets redirected to
login
:

it('should go to loginpage before main', function () {
$scope.$apply();
expect($state.current.name).toBe('landing');
$state.transitionTo('main');
$scope.$apply();
expect($state.current.name).toBe('login');
});


In my app.js I have:

$rootScope.$on('$stateChangeStart', function (event,next) {
console.log('statechange from:', $state.current.name)
//console.log('statechange to:',$state)

if (!next.authenticate) {
return;
}

//if next state is secure then check whether user is logged in:

if (next.authenticate) {
event.preventDefault();
$state.transitionTo('login');
console.log('goto login');
}


});


When I run my test with karma I get:

Chrome 51.0.2704 (Mac OS X 10.11.4) secure tests should go to loginpage before main FAILED
Expected '' to be 'landing'.
at Object.<anonymous> (/Users/dimitri/karma/basic_karma/appSpec.js:23:37)
Expected '' to be 'login'.


Why is the
$state.current.name
empty?

github ref:https://github.com/dimitri-a/basic_karma

Answer

It seems as if you're trying to test two different things. First of all, you are testing if the $stateChangeStart-event gets fired. Secondly, you're testing your custom handler and logic inside it if it redirects when needed.

Unit testing is about testing a small part of your application. The $stateChangeStart-event is not part of your application, because it's part of the ui-router component. Testing if this event fires is not your responsibility. Even better; ui-router made their own test cases which checks if $stateChangeStart gets fired. You can see them yourself right here. You can be fairly certain the event gets triggered when it's needed.

So, how do we test your own custom logic? First, we need to refactor the $rootScope.$on handler.

$rootScope.$on('$stateChangeStart', handleStateChange);

function handleStateChange(event, next) {
    console.log('statechange from:', $state.current.name)
    //console.log('statechange to:',$state)

    if (!next.authenticate) {
        return;
    }

    //if next state is secure then check whether user is logged in:

    if (next.authenticate) {
        event.preventDefault();
        $state.transitionTo('login');
        console.log('goto login');
    }
}

This way, we decouple you custom logic from the Angular $rootScope and we can test the logic by itself. Don't worry, your logic will still run when a state is about to change.

Next up the test itself. We don't have to deal with $state.transitionTo because we are not testing that part of the application (again, ui-router has it's own tests for $state.transitionTo). We don't even need to use $scope.$apply.

describe('handleStateChange()', function() {

    var $state;

    beforeEach(inject(function(_$state_) {
        $state          = _$state_;

        // we spy on the $state.transitionTo method, to check if it 
        // has been called by our test case.
        spyOn($state, 'transitionTo');
    }));

    it('should transition to login state when authenticate:true', function () {
        // arrange
        var next = {
            name: 'main',
            authenticate: true
        };

        // act
        handleStateChange({}, next);

        // assert
        // in case of authenticate: true, $state.transitionTo should 
        // have been called with the 'login' name
        expect($state.transitionTo).toHaveBeenCalledWith('login');
    });

    it('should not transition to login state when authenticate:false', function () {
        // arrange
        var next = {
            name: 'landing',
            authenticate: false
        };

        // act
        handleStateChange({}, next);

        // assert
        expect($state.transitionTo).not.toHaveBeenCalledWith('login');
    });

});