简体   繁体   中英

angularjs unit test karma with jasmine mock dilemma

I am currently writing tests for some services using karma with jasmine , and I was wondering if I had to mock a service's service dependency that uses $http , as described below.

PS : I'm already using $httpBackend to mock any GET request, and I plan on using $httpBackend.expect* if I don't mock the service ApiProvider


This is the service I am testing

.factory('CRUDService', ['ApiProvider', function (ApiProvider) {
    'use strict';

    var CRUD = function CRUD(modelName) {
        this.getModelName = function () {
            return modelName;
        };
    },
        overridableMethods = {
            save: null
        };

    CRUD.prototype = {
        save: function () {
            // ABSTRACT
        },
        /**
         * Deletes instance from id property
         * @return http promise
         */
        remove: function () {
            return ApiProvider.delete(this.getModelName(), this.id);
        }
    };

    return {
        /**
         * Function creating a class extending CRUD
         * @param {string} modelName
         * @param {Object} methods an object with methods to override, ex: save
         * return {classD} the extended class
         */
        build: function (modelName, methods) {
            var key,
                Model = function () {
            };

            // Class extending CRUD
            Model.prototype = new CRUD(modelName);

            // Apply override on methods allowed for override
            for (key in methods) {
                if (key in overridableMethods &&
                    typeof methods[key] === 'function') {
                    Model.prototype[key] = methods[key];
                }
            }
            /**
             * Static method
             * Gets an entity of a model
             * @param {Object} config @see ApiProvider config
             * @return {CRUD} the reference to the entity
             */
            Model.get = function (config, success) {
                var entity = new Model();

                ApiProvider.get(modelName, config)
                    .success(function (data) {
                        angular.extend(entity, data);

                        if (success) {
                            success();
                        }
                    });

                return entity;
            };
            /**
             * Static method
             * Gets entities of a model
             * @param {Object} config @see ApiProvider config
             * @return {CRUD[]} the reference to the entity
             */
            Model.query = function (config, success) {
                var entities = [];

                ApiProvider.get(modelName, config)
                    .success(function (data) {
                        data.map(function (model) {

                            var entity = new Model();
                            angular.extend(entity, model);

                            return entity;
                        });

                        Array.prototype.push.apply(entities, data);

                        if (success) {
                            success();
                        }
                    });

                return entities;
            };

            return Model;
        },
        // Direct link to ApiProvider.post method
        post: ApiProvider.post,
        // Direct link to ApiProvider.put method
        put: ApiProvider.put
    };
}]);

And this is the service's service dependency ApiProvider

.service('ApiProvider', function ($http) {

    /**
     * Private
     * @param {string}
     * @param {object}
     * @return {string} Example: /service/[config.id[/config.relatedModel], /?config.params.key1=config.params.value1&config.params.key2=config.params.value2]
     */
    var buildUrl = function (service, config) {
            var push   = Array.prototype.push,
                url    = [apiRoot, service],
                params = [],
                param  = null;

            // if a key id is defined, we want to target a specific resource
            if ('id' in config) {
                push.apply(url, ['/', config.id]);

                // a related model might be defined for this specific resource
                if ('relatedModel' in config) {
                    push.apply(url, ['/', config.relatedModel]);
                }
            }

            // Build query string parameters
            // Please note that you can use both an array or a string for each param
            // Example as an array:
            // {
            //  queryString: {
            //      fields: ['field1', 'field2']
            //  }
            // }
            // Example as a string
            // {
            //  queryString: {
            //      fields: 'field1,field2'
            //  }
            // }
            if ('queryString' in config) {

                // loop through each key in config.params
                for (paramName in config.queryString) {
                    // this gives us something like [my_key]=[my_value]
                    // and we push that string in params array
                    push.call(params, [paramName, '=', config.queryString[paramName]].join(''));
                }

                // now that all params are in an array we glue it to separate them
                // so that it looks like
                // ?[my_first_key]=[my_first_value]&[my_second_key]=[my_second_value]
                push.apply(url, ['?', params.join('&')]);
            }

            return url.join('');
        },
        request = function (method, url, methodSpecificArgs) {
            trace({
                method: method,
                url: url,
                methodSpecificArgs: methodSpecificArgs
            }, 'ApiProvider request');
            return $http[method].apply($http, [url].concat(methodSpecificArgs));
        },
        methods = {
            'get': function (url, config) {
                config.cache = false;
                return request('get', url, [config]);
            },
            'post': function (url, data, config) {
                config.cache = false;
                return request('post', url, [data, config]);
            },
            'put': function (url, data, config) {
                config.cache = false;
                return request('put', url, [data, config]);
            },
            'delete': function (url, config) {
                config.cache = false;
                return request('delete', url, [config]);
            }
        };

    return {
        'get': function (service, config) {
            config = config || {};
            return methods.get(buildUrl(service, config), config);
        },
        'post': function (service, data, config) {
            config = config || {};
            return methods.post(buildUrl(service, config), data, config);
        },
        'put': function (service, data, config) {
            config = config || {};
            return methods.put(buildUrl(service, config), data, config);
        },
        'delete': function (service, config) {
            config = config || {};
            return methods.delete(buildUrl(service, config), config);
        }
    };
});

Finally this is how I tested the CRUDService so far

describe('CRUDServiceTest', function () {
    'use strict';

    var CRUDService;

    beforeEach(function () {
        inject(function ($injector) {
            CRUDService = $injector.get('CRUDService');
        });
    });

    it('should have a method build', function () {
        expect(CRUDService).toHaveMethod('build');
    });

    it('should ensure that an instance of a built service has a correct value for getModelName method',
        function () {
            var expectedModelName = 'myService',
                BuiltService = CRUDService.build(expectedModelName),
                instanceOfBuiltService = new BuiltService();

            expect(instanceOfBuiltService).toHaveMethod('getModelName');
            expect(instanceOfBuiltService.getModelName()).toEqual(expectedModelName); 
    });

    // TEST get
    it('should ensure build returns a class with static method get', function () {
        expect(CRUDService.build()).toHaveMethod('get');
    });

    it('should ensure get returns an instance of CRUD', function() {
        var BuiltService = CRUDService.build(),
            instanceOfBuiltService = new BuiltService();

        expect((BuiltService.get()).constructor).toBe(instanceOfBuiltService.constructor);
    });

    // TEST query
    it('should ensure build returns a class with static method query', function () {
        expect(CRUDService.build()).toHaveMethod('query');
    });

    it('should  a collection of CRUD', function () {
        expect(CRUDService.build()).toHaveMethod('query');
    });

    it('should have a static method post', function () {
        expect(CRUDService).toHaveMethod('post');
    });

    it('should have a static method put', function () {
        expect(CRUDService).toHaveMethod('put');
    });
});

TLDR;

To mock or not to mock a depending service depending itself on $http ?

In general I think it's a good idea to mock out your services. If you keep up on doing it, then it makes isolating the behavior of any service you add really easy.

That being said, you don't have to at all, you can simply use Jasmine spy's.

for instance if you were testing your CRUDService which had a method like this:

remove: function () {
    return ApiProvider.delete(this.getModelName(), this.id);
}

You could, in your test write something like:

var spy = spyOn(ApiProvider, 'delete').andCallFake(function(model, id) {
    var def = $q.defer();
    $timeout(function() { def.resolve('something'); }, 1000)
    return def.promise;
});

Then if you called it:

var promise = CRUDService.remove();
expect(ApiProvider.delete).toHaveBeenCalledWith(CRUDService.getModelName(), CRUDService.id);

So basically you can mock out the functionality you need in your test, without fully mocking out the service. You can read about it more here

Hope this helped!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM