JavaScript

Avoid test code duplication in Jasmine tests

Test code has to be treated like production code. Obviously we cannot charge the customer for it, it’s something that helps us developers to make sure we keep our codebase healthy, which ultimately is the responsibility we have towards our customers. Thus we need to apply the same best practices principles as we do for our production code, where, code duplication is evil.

Let’s quickly take a look at some Angular code and the corresponding Jasmine test. I have the following Angular Provider which holds some functionality for handling the application menu.
 
 

function menuProvider(){

    // expose the provider contract
    this.addMenuEntry = addMenuEntry;
    ...

    // expose the service contract
    this.$get = function(){
        var service = {
            addMenuEntry: addMenuEntry
            ...
        }
        return service;
    };

    ///////////

    function addMenuEntry(newEntry){
        ...
    }
}

It’s not really important, but to understand the context, in Angular you have “Providers” and “Services”. The main difference is their availability during the application lifecycle (i.e. the config vs run phase). So basically if you want to have them available during both phases, you’d do something similar as I did above, namely to expose the exact same contract (or part of it) as a provider and as a service.

Obviously, I’d like to test the availability and correct functioning of this exposed contract on both, the provider and the service class. This leads to duplicated tests. Let’s see this on the example of this excerpt from a Jasmine test.

describe('The Menu's', function(){

    describe('provider interface', function(){
        var provider;

        beforeEach(function(){
            provider = /* Angular code to inject the provider */;
        });

        it('should allow to add new menu entries', function(){
            provider.addMenuEntry({
                title: 'Menu title',
                url: 'www.google.com'
            });

            /* assertion code here */
        });

    });

    describe('service interface', function(){
        var service;

        beforeEach(function(){
            service = /* Angular code to inject the service */
        });

        it('should allow to add new menu entries', function(){
            service.addMenuEntry({
                title: 'Menu title',
                url: 'www.google.com'
            });

            /* assertion code here */
        });

    });
});

Refactoring duplications

Guess you clearly see the duplication. On the Pivotallabs site there’s a blog post “DRYing up Jasmine Specs with Shared Behavior” which describes the possibility to factor out your describe statement into a separate function:

function sharedTests(someParams){
    describe(function(){
        ...
    });
}

You can then use that function simply by invoking it within your test code:

describe('My functionality', function(){
    ...
    sharedTests(...);
})

This works like charm, with one exception. Usually factoring out is useful to be able to parameterize the describe, in my case to use the same tests, the first time passing in a provider instance and then the service one. Like..

describe('The Menu's', function(){

    describe('provider interface', function(){
        var provider;

        beforeEach(function(){
            provider = /* Angular code to inject the provider */;
        });

        // this is the line of interest!
        executeSharedTests(provider);
    });

    describe('service interface', function(){
        var service;

        beforeEach(function(){
            service = /* Angular code to inject the service */
        });

        // this is the line of interest!
        executeSharedTests(service);
    });

    function executeSharedTests(instance){
        ...
    }
});

This doesn’t work, for the simple reason that the beforeEach is executed after the executeSharedTests(...) is being invoked, thus passing in undefined.

To solve this problem you can pass in a constructor function which creates the object lazily when the test is effectively executed.

describe('The Menu's', function(){

    describe('service interface', function(){

        function createInstance(){
            return /* Angular code to inject the service */
        }

        executedSharedTests(createInstance);
    });

    executedSharedTests(createInstanceFn){
        describe('when adding a new menu entry', function(){
            var subjectUnderTest;

            beforeEach(function(){
                //create an instance by invoking the constructor function
                subjectUnderTest = createInstanceFn();
            });

            it('should allow to add new menu entries', function(){
                ...
            });
        });
    }
});

Conclusion

So, the whole refactored code now looks like this:

describe('The Menu's', function(){

    describe('provider interface', function(){
        var provider;

        beforeEach(function(){
            provider = /* Angular code to inject the provider */;
        });

        // this is the line of interest!
        executeSharedTests(provider);
    });

    describe('provider interface', function(){
        function createInstance(){
            return /* Angular code to inject the provider */
        }

        executeSharedTests(createInstance);
    });

    describe('service interface', function(){
        function createInstance(){
            return /* Angular code to inject the service */
        }

        executeSharedTests(createInstance);
    });

    executedSharedTests(createInstanceFn){
        describe('when adding a new menu entry', function(){
            var subjectUnderTest;

            beforeEach(function(){
                //create an instance by invoking the constructor function
                subjectUnderTest = createInstanceFn();
            });

            it('should allow to add new menu entries', function(){
                subjectUnderTest.addMenuEntry({
                    title: 'Menu title',
                    url: 'www.google.com'
                });

                /* assertion code here */
            });
        });
    }
});

Related articles

Juri Strumpflohner

Juri Strumpflohner mainly operates in the web sector developing rich applications with HTML5 and JavaScript. Beside having a Java background and developing Android applications he currently works as a software architect in the e-government sector. When he’s not coding or blogging about his newest discoveries, he is practicing Yoseikan Budo where he owns a 2nd DAN.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Kevin
Kevin
8 years ago

Are lines 3-12 supposed to be in the conclusion?

Juri
8 years ago
Reply to  Kevin

Nope, it’s already corrected in the original article: http://juristr.com/blog/2014/10/avoid-test-code-duplication-jasmine/

Back to top button