Joseph hooper
Joseph hooper

Reputation: 1067

Spying on service dependencies

I'm curious about the best way to spy on dependencies so I can make sure that their methods are being called in my services. I reduced my code to focus on the problem at hand. I'm able to test my service fine, but I want to also be able to confirm that my service (In this case metricService) has methods that are also being called. I know I have to use createSpyObj in some way, but while the function is executing properly, the spyObj methods are not being caught. Should I even be using createSpyObj? Or should I use spyObj? I'm a but confused about the concept of spying when it concerns dependencies.

UPDATE: When using SpyOn I can see one method getting called, but other methods are not

Test.spec

describe("Catalogs service", function() {

  beforeEach(angular.mock.module("photonServicesCommons"));

  var utilityService, metricsService, loggerService, catalogService, localStorageService;

  var $httpBackend, $q, $scope;

  beforeEach(
    inject(function(
      _catalogService_,
      _metricsService_,
      _$rootScope_,
      _$httpBackend_
    ) {
      catalogService = _catalogService_;
      $scope = _$rootScope_.$new();
      $httpBackend = _$httpBackend_;
      $httpBackend.when('GET', "/ctrl/catalog/all-apps").respond(
        {
          catalogs: catalogs2
        }
      );

      metricsService = _metricsService_;
      startScope = spyOn(metricsService, 'startScope')
      emitSuccess = spyOn(metricsService, 'emitGetCatalogSuccess').and.callThrough();
      endScope = spyOn(metricsService, 'endScope');
    })
  );



  afterEach(function(){
    $httpBackend.verifyNoOutstandingExpectation();
    $httpBackend.verifyNoOutstandingRequest();
  });

  describe('get catalog', function(){
    it("Should get catalogs", function(done) {
      catalogService.normalizedDynamicAppList = testDynamicAppList1;
      catalogService.response = null;
      var promise3 = catalogService.getCatalog();
      promise3.then(function (res) {
      expect(res.catalogs).toEqual(catalogs2);
      });
      expect(metricsService.startScope).toHaveBeenCalled();
      expect(metricsService.emitGetCatalogSuccess).toHaveBeenCalled();
      expect(metricsService.endScope).toHaveBeenCalled();

      $scope.$digest();
      done();
      $httpBackend.flush();
    });
  });

});

Service

public getCatalog(): IPromise<Interfaces.CatalogsResponse> {
  if (this.response !== null) {
    let finalResponse:any = angular.copy(this.response);
    return this.$q.when(finalResponse);
  }

  return this.$q((resolve, reject) => {
    this.metricsService.startScope(Constants.Photon.METRICS_GET_CATALOG_TIME);
    this.$http.get(this.catalogEndpoint).then( (response) => {
      let data: Interfaces.CatalogsResponse = response.data;
      let catalogs = data.catalogs;
      if (typeof(catalogs)) { // truthy check
        catalogs.forEach((catalog: ICatalog) => {
          catalog.applications.forEach((application: IPhotonApplication) => {
            if( !application.appId ) {
              application.appId = this.utilityService.generateUUID();
            }
          })
        });

      } else {
        this.loggerService.error(this.TAG, "Got an empty catalog.");
      }
      this.response = data;
      this.metricsService.emitGetCatalogSuccess();
      console.log("CALLING END SCOPE");
      this.metricsService.endScope(Constants.Photon.METRICS_GET_CATALOG_TIME);
      resolve(finalResponse);
    }).catch((data) => {
      this.loggerService.error(this.TAG, "Error getting apps: " + data);
      this.response = null;
      this.metricsService.emitGetCatalogFailure();
      reject(data);
    });
  });
} // end of getCatalog()

Upvotes: 1

Views: 278

Answers (1)

Roope
Roope

Reputation: 291

Instead of using createSpyObj, you can just use spyOn. As in:

beforeEach(
  inject(function(
    _catalogService_,
    _$rootScope_,
    _$httpBackend_,
    _metricsService_ //get the dependecy from the injector & then spy on it's properties
  ) {
    catalogService = _catalogService_;
    metricsService = _metricsService_; 
    $scope = _$rootScope_.$new();
    ...
    // create the spy object for easy referral later on
    someMethodSpy = jasmine.spyOn(metricsService, "someMethodIWannaSpyOn")
})
);

describe('get catalog', function(){
  it("Should get catalogs", function(done) {
    catalogService.normalizedDynamicAppList = testDynamicAppList1;
    catalogService.response = null;
    var promise3 = catalogService.getCatalog();
    ...other expects
    ...
    //do the spy-related expectations on the function spy object
    $httpBackend.flush(); // this causes the $http.get() to "move forward" 
    // and execution moves into the .then callback of the request. 
    expect(someMethodSpy).toHaveBeenCalled(); 
  });
});

I've used this pattern when testing complex angular apps, along with wrapping external imported/global dependencies in angular service wrappers to allow spying and mocking them for testing.

The reason that createSpyObject won't work here is that using it will create a completely new object called metricService with the spy props specified. It won't be the same "metricService" that is injected into the service being tested by the angular injector. You want to get the actual same singleton service object from the injector and then spy on it's properties.

The other source of dysfunction was the $httpBackend.flush()s location. The $httpBackend is a mock for the $http service: you pre-define any number of expected HTTP requests to be made by the code you are testing. Then, when you call the function that internally uses $http to make a request to some url, the $httpBackend instead intercepts the call to $http method (and can do things like validate the request payload and headers, and respond). The $http call's then/error handlers are only called after the test code calls $httpBackend.flush(). This allows you to do any kind of setup necessary to prep some test state, and only then trigger the .then handler and continue execution of the async logic.

For me personally this same thing happens every single time I write tests with $httpBackend, and it always takes a while to figure out or remember :)

Upvotes: 1

Related Questions