angularjs单位测试业力与茉莉花模拟困境

axe*_*uch 3 javascript unit-testing mocking jasmine angularjs

我正在使用karmawith 编写一些服务的测试jasmine,我想知道是否必须模拟使用的服务依赖项$http,如下所述.

PS:我已经$httpBackend用来模拟任何GET请求,$httpBackend.expect*如果我不嘲笑服务,我打算使用它ApiProvider


这是我正在测试的服务

.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
    };
}]);
Run Code Online (Sandbox Code Playgroud)

这就是服务的服务依赖性 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);
        }
    };
});
Run Code Online (Sandbox Code Playgroud)

最后,这是我到目前为止测试CRUDService的方式

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');
    });
});
Run Code Online (Sandbox Code Playgroud)

TLDR;

是否依赖于$ http来模拟或不模拟依赖服务?

has*_*sin 5

总的来说,我认为嘲笑你的服务是个好主意.如果你继续这样做,那么它会使你添加的任何服务的行为变得非常容易.

话虽这么说,你根本不需要,你可以简单地使用Jasmine间谍.

例如,如果您正在测试具有如下方法的CRUDService:

remove: function () {
    return ApiProvider.delete(this.getModelName(), this.id);
}
Run Code Online (Sandbox Code Playgroud)

您可以在测试中写下以下内容:

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

如果你打电话给它:

var promise = CRUDService.remove();
expect(ApiProvider.delete).toHaveBeenCalledWith(CRUDService.getModelName(), CRUDService.id);
Run Code Online (Sandbox Code Playgroud)

因此,基本上您可以模拟测试中所需的功能,而无需完全模拟服务.你可以在这里阅读更多内容

希望这有帮助!