How override Provider in Angular 5 for only one test?

TestingMockingAngular5Angular ProvidersTestbed

Testing Problem Overview


In one of my unit test files, I have to mock several times the same service with different mocks.

import { MyService } from '../services/myservice.service';
import { MockMyService1 } from '../mocks/mockmyservice1';
import { MockMyService2 } from '../mocks/mockmyservice2';
describe('MyComponent', () => {

    beforeEach(async(() => {
        TestBed.configureTestingModule({
        declarations: [
            MyComponent
        ],
        providers: [
            { provide: MyService, useClass: MockMyService1 }
        ]
        })
        .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(MapComponent);
        mapComponent = fixture.componentInstance;
        fixture.detectChanges();
    });

    describe('MyFirstTest', () => {
        it('should test with my first mock', () => {
            /**
             * Test with my first mock
             */
        });
    });

    describe('MySecondTest', () => {
        // Here I would like to change { provide: MyService, useClass: MockMyService1 } to { provide: MyService, useClass: MockMyService2 }

        it('should test with my second mock', () => {
            /**
             * Test with my second mock
             */
        });
    });
});

I see that the function overrideProvider exists, but I did not manage to use it in my test. When I use it in a "it", the provider doesn't change. I didn't manage to find an example where this function is called. Could you explain me how to use it properly? Or have you an other method to do that?

Testing Solutions


Solution 1 - Testing

As of angular 6 I noticed that overrideProvider works with the useValue property. So in order to make it work try something like:

class MockRequestService1 {
  ...
}

class MockRequestService2 {
  ...
}

then write you TestBed like:

// example with injected service
TestBed.configureTestingModule({
  // Provide the service-under-test
  providers: [
    SomeService, {
      provide: SomeInjectedService, useValue: {}
    }
  ]
});

And whenever you want to override the provider just use:

TestBed.overrideProvider(SomeInjectedService, {useValue: new MockRequestService1()});
// Inject both the service-to-test and its (spy) dependency
someService = TestBed.get(SomeService);
someInjectedService = TestBed.get(SomeInjectedService);

Either in a beforeEach() function or place it in an it() function.

Solution 2 - Testing

If you need TestBed.overrideProvider() with different values for different test cases, TestBed is frozen after call of TestBed.compileComponents() as @Benjamin Caure already pointed out. I found out that it is also frozen after call of TestBed.get().

As a solution in your 'main' describe use:

let someService: SomeService;

beforeEach(() => {
	TestBed.configureTestingModule({
		providers: [
			{provide: TOKEN, useValue: true}
		]
	});
	
	// do NOT initialize someService with TestBed.get(someService) here
}

And in your specific test cases use

describe(`when TOKEN is true`, () => {

    beforeEach(() => {
		someService = TestBed.get(SomeService);
    });
	
	it(...)
	
});

describe(`when TOKEN is false`, () => {

    beforeEach(() => {
		TestBed.overrideProvider(TOKEN, {useValue: false});
		someService = TestBed.get(SomeService);
    });
	
	it(...)
	
});

Solution 3 - Testing

If the service is injected as public property, e.g.:

@Component(...)
class MyComponent {
  constructor(public myService: MyService)
}

You can do something like:

it('...', () => {
  component.myService = new MockMyService2(...); // Make sure to provide MockMyService2 dependencies in constructor, if it has any.
  fixture.detectChanges();

  // Your test here...
})

If injected service is stored in a private property, you can write it as (component as any).myServiceMockMyService2 = new MockMyService2(...); to bypass TS.

It's not pretty but it works.

As for TestBed.overrideProvider, I had no luck with that approach (which would be much nicer if it worked):

it('...', () =>{
  TestBed.overrideProvider(MyService, { useClass: MockMyService2 });
  TestBed.compileComponents();
  fixture = TestBed.createComponent(ConfirmationModalComponent);
  component = fixture.componentInstance;
  fixture.detectChanges();

  // This was still using the original service, not sure what is wrong here.
});

Solution 4 - Testing

I was facing similar problem, but in a simpler scenario, just one test(describe(...)) with multiple specifications(it(...)).

The solution that worked for me was postponing the TestBed.compileComponents and the TestBed.createComponent(MyComponent) commands. Now I execute those on each individual test/specification, after calling TestBed.overrideProvider(...) when needed.

describe('CategoriesListComponent', () => {
...
beforeEach(async(() => {
  ...//mocks 
  TestBed.configureTestingModule({
    imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])],
    declarations: [CategoriesListComponent],
    providers: [{provide: ActivatedRoute, useValue: mockActivatedRoute}]
  });
}));
...

it('should call SetCategoryFilter when reload is false', () => {
  const mockActivatedRouteOverride = {...}
  TestBed.overrideProvider(ActivatedRoute, {useValue: mockActivatedRouteOverride });
  TestBed.compileComponents();
  fixture = TestBed.createComponent(CategoriesListComponent);

  fixture.detectChanges();

  expect(mockCategoryService.SetCategoryFilter).toHaveBeenCalledTimes(1);
});

Solution 5 - Testing

Just for reference, if annynone meets this issue.

I tried to use

TestBed.overrideProvider(MockedService, {useValue: { foo: () => {} } });

it was not working, still the original service was injected in test (that with providedIn: root)

In test I used alias to import OtherService:

import { OtherService } from '@core/OtherService'`

while in the service itself I had import with relative path:

import { OtherService } from '../../../OtherService'

After correcting it so both test and service itself had same imports TestBed.overrideProvider() started to take effect.

Env: Angular 7 library - not application and jest

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionhbaltzView Question on Stackoverflow
Solution 1 - TestingDimitrios VythoulkasView Answer on Stackoverflow
Solution 2 - TestingKatjaView Answer on Stackoverflow
Solution 3 - TestingFilip VoskaView Answer on Stackoverflow
Solution 4 - TestingJ.JView Answer on Stackoverflow
Solution 5 - TestingFelixView Answer on Stackoverflow