user7043436 user7043436 - 1 month ago 20
AngularJS Question

Angular unit tests, mocking a service that is injected using $injector

I'm trying to unit test an Angular service, that has another service injected into it using $injector, rather than passing the service dependency as an argument. The injected service is dynamic - it could be one of a number of different services - and so it can't be passed as a function argument.

The service under test determines if the application is online or offline, then uses the strategy pattern to either make an HTTP call (we're online, so the strategy is 'remote') or use local storage to get the required data (we're offline, so the strategy is 'local'). It's used in a number of scenarios across the application, so the service and the service method that is subsequently called are dynamic. Easier to show the simplified, relevant, code:

class NetworkStrategyService {
constructor (private Offline, private $injector) {}

fulfill (config) {
this.Offline.check()
.then(
() => {
// we're online
const service = $injector.get(config.remote.service);
service[config.remote.method](config.remote.data)
.then(() => {
// Do something with the data
})
},
() => {
// Grab it from local storage
}
)
}

}


My problem is, because I can't inject the service in the normal way in my unit tests, I don't know how to test this code. I've mocked the config, now I want to test that the correct service methods are being called while online and offline, such as:

it (`calls the remote service, with the remote method, if we're online`, () => {
const remoteMethod = spyOn(config.remote.service, config.remote.method);
// Set up, to get a successful Offline.check() outcome
expect(remoteMethod).toHaveBeenCalledWith(config.remote.data);
});


How can I ensure that the mocked service is a service, and pass it into my service under test?

Answer

OK, I seem to have solved it:

let deferred;
let config = {
    remote: {
        service: 'RemoteService',
        method: 'remoteAction',
        data: {alpha:1}
    },
    local: ...
};

beforeEach(() => {
    angular.mock.module(($provide) => {
        $provide.service(config.remote.service, () => {
            return {
                [config.remote.method]: () => {}
            };
        });
    });
});

beforeEach(() => {
    inject((_$q_, _$rootScope_, _$injector_) => {
        $q = _$q_;
        $scope = _$rootScope_.$new();
        $injector = _$injector_
    });
    deferred = $q.defer();
});

beforeEach(() => serviceUnderTest = new NetworkStrategyService(Offline, $injector));

it (`calls the remote service, with the remote method, if we're online`, () => {
    let dynamicService;
    inject((_RemoteService_) => {
        dynamicService = _RemoteService_;
    });
    serviceUnderTest.strategy = 'remote';
    const dynamicMethod = spyOn(dynamicService, config.remote.method).and.returnValue(deferred.promise);
    serviceUnderTest.makeCall(config);
    deferred.resolve();
    $scope.$apply();
    expect(dynamicMethod).toHaveBeenCalledWith(config.remote.data);
});