user2157249
user2157249

Reputation: 31

Angular 2 - How to Override a Dependency in Another Module for Testing

I'm trying to test a service in my module that depends on services in another module. The other module's services, components...are accessed via npm packages. My company, like most is a bit finicky about putting source code on the web so hopefully I've posted enough to get help.

The service that I want to mock get's user information and exists in another module. I get an error saying that a property that is returned by this service is undefined. I tried using TestBed.overrideModule to change the provider in the other module to a mock version that I created. I'm having trouble finding documentation for overrideModule and I've tried for a couple of days now and can't figure out how to make what I want work, even after finding a few usages of overrideModules. Any help would be appreciated.

Based on the error I get, I'm wondering if the correct dependency is being injected but don't really know how to check. The error stack trace point to the equivalent of webpack:///node_modules/@another-repo/prefs/index.js.

Code (please let me know if you have questions I tried to strip out things that would identify my company):

The following are in my module

// classa.service.ts
import { Injectable } from '@angular/core';
import { IDModel } from '@another-repo/core';
import { FormatService } from '@another-repo/prefs';
import { MyRepo } from './my-repo.service';

@Injectable()
export class ClassA {
    constructor(idModel: IDModel, repo: MyRepo, formats: FormatService) {

    }
    doSomething() { }
}
// in my module
// my-repo.service.ts
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';

@Injectable()
export class MyRepo {
    constructor(private readonly http: Http) {
    }

    get(id): Observable<any> {
        return this.http
            .get(api(`${id}`))
            .map(response => response.json());
    }
}

// my-repo-service.spec.ts
//...
describe('test', () => {
    let testService;
    beforeEach(async () => {
        TestBed.configureTestingModule({
            imports: [
                HttpModule,
                FooModule
            ],
            providers: [
                { provide: RepoToMock1, useClass: MockRepoToMock1 },
                MyRepo,
            ],
        });

        TestBed.overrideModule(FooModule,
            {
                remove: {
                    providers: [RepoToMock1]
                },
                add: {
                    providers: [{ provide: RepoToMock1, useClass: MockRepoToMock1 }]
                }
            });
    });

    beforeEach(() => {
        testService = TestBed.get(MyRepo);
    });
    //...
});  

this is in the index.d.ts file in node_modules let's call the module FooModule

export class IDModel {
    id: Observable<number>;
    constructor(otherIdRepo: OtherIdRepo, otherType: OtherTypeModel);
}

export class FormatService {
    constructor(formatModel: FormatModel, timeService: TimeService, numberService, NumberService);
}

export class FormatModel {
    myFormat: Format;
    constructor(repo: RepoToMock1);
}

export class Format {
    branding: string;
}

export class RepoToMock1 {
    constructor(http: Http);
    getPrefs(): Observable<Format>;
}

export class TimeService {
    constructor(formatModel: FormatModel);
}

export class NumberService {
    getNumber();
} 

Upvotes: 2

Views: 1844

Answers (2)

seBaka28
seBaka28

Reputation: 960

As @mgm87 wrote, you should mock all your services and components for simple unit tests. (As a sidenote, for component mocking I am currently using ng-mocks library, as this makes mocking really easy -> I have no affiliation with this library)

When mocking services in tests, most of the time you still want to be able to get some information on them, like whether or not it was called, or return specific values to test different conditions in your code. This can be simply achieved with jasmine and spies.

An example for a unit test setup of a component could look like the following:

describe(ToTestComponent.name, () => {
  let component: ToTestComponent;
  let fixture: ComponentFixture<ToTestComponent>;
  let mockService: any;

  beforeEach(async(() => {
    mockService = {
      coolMethod: jasmine.createSpy('coolMethod')
    };
    TestBed.configureTestingModule({
      declarations: [
        ToTestComponent,
        MockComponent(SomeChildComponent) // example of ng-mocks library
      ],
      providers: [
        {provide: RealService, useValue: mockService}
      ]
    }).compileComponents();
  }));
...

});

You can then simply call

expect(mockService.coolMethod).ToHaveBeenCalled();
// or
mockService.coolMethod.and.returnValue('I am so cool');

As you can see, in this case, you do not even have to import the other Module (FooModule). I used any as type for the mockService, as otherwise you would have to fulfill the type requirements.

Upvotes: 1

mgm87
mgm87

Reputation: 894

I use useFactory. See the code below.

As a side-note I have found that in testing, all dependencies should be mocked for injection. This decouples the codebase and allows updates to injectables without impacting tests of items dependent on the injectable.

For example, if you were to inject X service into Y component's tests without mocking it and you decided X service needed a new function that introduced the need to use lodash. All of component Y's tests would start failing because you introduced a new dependancy and didn't account for it. Component Y's tests will fail regardless of whether you use the new lodash dependant function or not.

If service X were mocked in the component Y tests any change can be made to service X with no impact on the component Y tests.

//y.component.spec.ts
describe('YComponent', () => {

  let component: YComponent;
  let fixture: ComponentFixture<YComponent>;
  let xService: XService;

  beforeEach(async(() => {
    let mockXService = {
      getTextColor: () => {}
    };

    TestBed.configureTestingModule({
      declarations: [
        YComponent,
      ],
      imports: [
        FormsModule
      ],
      providers: [
        {provide: XService, useFactory: () => { return mockXService; }}
      ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA]
    }).compileComponents();

    fixture = TestBed.createComponent(YComponent);
    component = fixture.componentInstance;
    roomingService = TestBed.get(XService);

    fixture.detectChanges();
  }));

  it('should create', async(() => {
    // Assert
    expect(component).toBeTruthy();
  }));

});

Upvotes: 0

Related Questions