angular / quickstart

Angular QuickStart - source from the documentation
MIT License
3.11k stars 3.14k forks source link

How to override a single provider in TestBed (for unit tests for a service, not a component, with service constructor dependencies)? #320

Closed gituser7878-Ultralinq closed 7 years ago

gituser7878-Ultralinq commented 7 years ago

I have:

beforeEach(() => { TestBed.configureTestingModule({ providers: [ DocumentsLoaderSvc, { provide: MgrSvc, useClass: MockMgrSvc }, { provide: URLLoaderSvc, useClass: MockURLLoaderSvcWhenData} ] }); });

How do I override URLLoaderSvc with another mock, in an "it" case with its own unique requirement? Is there something like TestBed.overrideProvider... Right now, I have each it statement in its own "describe", with its own beforeEach.

wardbell commented 7 years ago

Last provider for a token wins. You can keep adding providers — even for the same token — as long as you haven't frozen the TestingModule by calling compileComponents or createComponent.

beforeEach( () => {
  TestBed.configureTestingModule({
    providers: [ { provide: URLLoaderSvc, useClass: Mock1 } ]
  });
});
...
beforeEach( () => {
  TestBed.configureTestingModule({
    providers: [ { provide: URLLoaderSvc, useClass: Mock2} ]
  });
});
...
beforeEach( () => {
  TestBed.configureTestingModule({
    providers: [ { provide: URLLoaderSvc, useClass: Mock3} ]
  });
});
...
it( 'some test', inject([URLLoaderSvc,  (loader:URLLoaderSvc) => {
  ... // loader will be Mock3
}]);
ghost commented 7 years ago

This is useful information, but what to do if you actually called compileComponents ? Is there a way to override a provider after that call ?

nwayve commented 7 years ago

Although the inject example above does the trick, it adds a lot of cruft to the it declarations. Looking at the Angular docs, I found the TestBed has some .overrideX() methods (.overrideModule, .overrideComponent, etc...). One of these is .overrideProvider. To simplify this a bit (and make it feel a bit more like working in mocha/chai/sinon), I configured my .component.spec.ts like-a-so:

discribe('HeroesComponent', () => {
  let component: HeroesComponent;
  let fixture: ComponentFixture<HeroesComponent>;
  let mockHeroesService: MockHeroesService;

  beforeEach(async(() => {
    mockHeroesService = new MockHeroesService();
    TestBed.overrideProvider(HeroesService, { useValue: mockHeroesService });

    TestBed.configureTestingModule({
      declarations: [ HeroesComponent ],
      providers: [ HeroesService ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HeroesComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should call the heroesService\'s getHeroes method', () => {
    // Arrange
    spyOn(mockHeroesService, 'getHeroes').and.returnValue([]);

    // Act
    component.getHeroes();

    // Assert
    expect(mockHeroesService.getHeroes).toHaveBeenCalledTimes(1);
  });
});

class MockHeroesService extends HeroesService {}

I like this approach a bit more than the inject example above, just feels cleaner and less confusing. Another thing to note is that TestBed overrides need to happen before .createComponent is called. I tried the .overrideProvider call after .compileComponents and before .createComponent in the 2nd beforeEach, and it still worked fine. If, for whatever reason, the .overrideProvider needs to be called within a test (after .createComponent)(@trichetriche), all the setup in the 2nd beforeEach would need to get called again afterwards, but I don't see why that would be needed when the mock service can easily have spies and return values stubbed after the override.

I used the above example based on what the angular-cli generates from the command ng generate component Heroes for the spec file (v1.2.1). I'm not sure if this is the preferred method of injecting mocks over the inject method provided above.

vinyakas commented 7 years ago

@nwayve would like to thank you for sample above code. That cleared how spy works with mockclass together.

adamdabbracci commented 6 years ago

If you're trying to do this with a test suite that was scaffolded by the Angular CLI, you can do something like this without needing to change the entire structure of the test:

  1. At the top of the suite, defined a TestBed configuration that will serve as the "base" for your configs:

    // Testbed configs
    const tb_base = {
    declarations: [ MyComponent ],
    providers: [
        {
          provide: ConfigurationService,
          useValue: instance(ConfigurationServiceMock)
        }
    ],
    imports:[]
    }
  2. For your simple tests (that dont need overriding), you just pass this base object into the configureTestingModule() call:

    
    describe('MyComponent', () => {
    let component: MyComponent;
    let fixture: ComponentFixture<MyComponent>;
    beforeEach(async(() => {
    TestBed.configureTestingModule(tb_base)
    .compileComponents();
    }));

......


3. For any tests that need a new instance of the provider, just override it before you call `compileComponents();`:
```typescript
describe('MyComponent With Override', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule(tb_base);
    TestBed.overrideProvider(ConfigurationService, {useValue: instance(ConfigurationServiceMock2)});
    TestBed.compileComponents();
  }));

...... 

You can apply the same concept to individual test cases:


describe('MyComponent Single Overrides', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule(tb_base);
  }));

it('should override the provider in this one test case', () => {
    TestBed.overrideProvider(ConfigurationService, {useValue: instance(ConfigurationServiceMock2)});
    TestBed.compileComponents();
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();

    // Assert here
  });

.......
andrewryan1906 commented 6 years ago

This just saved me... thank you...

crhistianramirez commented 6 years ago

Is any of this info relevant for angular 6+ ?

I tried the first two approaches but I get an error that I'm unable to configure a testing module once it's been configured.

Parziphal commented 6 years ago

I looked at some of the Angular Material specs and found out that they make a createComponent function that they call in beforeEach() inside nested describe(), thus you can configure different providers according to different testing scenarios.

Then I ended up with something similar to what nwayve presented: exposing the mocks to the describe block.

describe('SomeComponent', () => {
  let fixture;
  let component;

  // This is the createComponent function
  function createComponent(providers: Provider[] = []) {
    TestBed.configureTestingModule({
      imports: [ /* ... */ ],
      declarations: [ /* ... */ ],
      providers: [
        // Here I set default providers mocks. Providers for the same service
        // added in the providers array will override the defaults.
        // By default I won't touch MatDialog so I don't need more than a object.
        { provide: MatDialog, useValue: { } },
        ...providers,
      ]
    })
    .compileComponents();

    fixture = TestBed.createComponent(SomeComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }

  describe('nested describe', () => {
    // Inside this block I'll need to mock MatDialog.
    let matDialogMock;

    beforeEach(() => {
      // I prepare the mock object
      matDialogMock = {open: () => ({afterClosed: () => of(true)})};

      // And here I create the component passing the new mock.
      // Note that I use useFactory, because for some reason, useValue
      // creates a copy of the object, so I won't be able to create a spy
      // using the variable declared at the top of this nested describe.
      createComponent([
        { provide: MatDialog, useFactory: () => matDialogMock }
      ]);
    });

    it('should work with the new mock', () => {
      // Now I create a spy...
      const spy = spyOn(matDialogMock, 'open').and.callThrough();

      // Then do some stuff, and test against it
      expect(spy).toHaveBeenCalled();
    });
  });
});
hevans90 commented 5 years ago

Is any of this info relevant for angular 6+ ?

I tried the first two approaches but I get an error that I'm unable to configure a testing module once it's been configured.

@crhistianramirez to get this working with Angular 7.1, call TestBed.resetTestingModule(); before re-configuring it.

Can't guarantee this works with Angular 6 though, haven't tried.

rocketkittens commented 5 years ago

Where are you people getting the instance function from? Please explain. None of your code works.

ksz-ksz commented 4 years ago

@rocketkittens I bet that the instance functions comes from ts-mockito: https://github.com/NagRock/ts-mockito.

pinich commented 2 years ago

I was able to substitute the instance method by calling a constructor "useValue: new MockSrv()"