如何在Angular 5中覆盖Provider,只针对一个测试?

75

在我的一个单元测试文件中,我需要使用不同的模拟来多次模拟相同的服务。

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
             */
        });
    });
});

我发现函数overrideProvider存在,但我没有在我的测试中成功使用它。当我在一个“it”中使用它时,提供者不会改变。我没有找到一个调用此函数的示例。你能解释一下如何正确使用它吗?或者你有其他方法吗?

6个回答

33

我注意到自从Angular 6以来,overrideProvider可以使用useValue属性。因此,为了使其起作用,请尝试以下方法:

class MockRequestService1 {
  ...
}

class MockRequestService2 {
  ...
}

那么你可以这样编写你的测试床(TestBed):

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

每当您想要覆盖提供者时,只需使用:

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

将其放在beforeEach()函数中或将其放置在it()函数中。


2
{useValue: new MockRequestService1()} 对我来说是缺失的一部分。可惜它不像 TestBed.configureTestingModule() 一样接受 useClass - Jacob Stamm
1
我尝试过这个方法,但一旦你调用了compileComponents,overrideProvider就不再起作用了(即使你再次调用compileComponents)。所以我认为这种方法不能在同一个规范中为两个测试用例工作。 - Benjamin Caure
@BenjaminCaure...在同一规范中尝试覆盖时确实无法工作。 - Vincent J. Michuki
我还不得不创建两个不同的mockApi类,对于jest单元测试,我只需创建另一个testBed.configurationModule,然后在providers部分指定哪个mock服务。 - tony2tones

27

如果您需要在不同的测试用例中使用不同的值来覆盖TestBed.overrideProvider(),则需要注意,在调用TestBed.compileComponents()之后,TestBed将被冻结,正如@Benjamin Caure已经指出的那样。我发现,在调用TestBed.get()之后,它也会被冻结。

作为解决方案,在您的“主”describe中使用:

let someService: SomeService;

beforeEach(() => {
    TestBed.configureTestingModule({
        providers: [
            {provide: TOKEN, useValue: true}
        ]
    });

    // do NOT initialize someService with TestBed.get(someService) here
}

在您具体的测试用例中使用

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(...)

});


14

如果将服务作为公共属性注入,例如:

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

你可以这样做:

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

  // Your test here...
})

如果注入服务被存储在一个私有属性中,你可以这样写(component as any).myServiceMockMyService2 = new MockMyService2(...); 来绕过TS检查。

虽然不太美观但它有效。

至于TestBed.overrideProvider,我尝试过但没有成功(如果能行的话会更好):

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.
});

3
一旦调用compileComponents,TestBed就会被冻结,因此overrideProvider对于这种情况不起作用。 - Benjamin Caure
只需删除 compileComponents 调用,并在每个 it() 调用内获取服务。 - Adrian Marinica
我在这种方法中遇到了一些问题。请考虑以下内容:it('当配方的成分列表为空时不应该调度"ShoppingListActions.AddIngredients"', async(() => { component.recipe.ingredients = []; addIngredientsBtn.nativeElement.click(); expect(dispatchSpy).not.toHaveBeenCalled(); }));测试通过,但是行为仍在同一“describe”块中持续到后续测试。 在“expect”之后的测试结束时调用component.recipe.ingredients = ingredients,或者在afterEach块内部进行设置列表重置并没有起到作用。 - Oisín Foley

8

我曾经遇到过类似的问题,但是情景更为简单,只涉及一个测试(describe(...))和多个规格(it(...))。

对我有效的解决方案是将 TestBed.compileComponentsTestBed.createComponent(MyComponent) 命令推迟执行。现在,我会在每个单独的测试/规格中执行它们,在需要时调用 TestBed.overrideProvider(...)

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);
});

1
我认为这不是一个好主意,因为你最终会得到大量重复的代码,来配置几乎所有测试中的相同内容。 - Tawfiek Khalaf
2
问题是如何仅覆盖一个测试的提供程序...如果其余测试将共享相同的初始化,则可以将该公共初始化提取到公共方法中,并从其余单元测试中调用。 - J.J

5
仅供参考,如果有人遇到此问题。
我试图使用
TestBed.overrideProvider(MockedService, {useValue: { foo: () => {} } });

它没有起作用,但原始服务仍然被注入到测试中(使用 providedIn: root

在测试中,我使用 别名导入 OtherService

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

在服务中,我使用了相对路径进行导入

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

在将测试服务本身的导入项进行修正后,TestBed.overrideProvider()开始生效。

环境:Angular 7库 - 不是应用程序和jest。


5
如果有人能解释这种行为,那就很好了。 - Felix

1
我需要为两种不同的测试方案配置MatDialogConfig。
正如其他人指出的那样,调用compileCompents将不能允许您调用overrideProviders。因此,我的解决方案是在调用overrideProviders后调用compileComponents
  let testConfig;

  beforeEach(waitForAsync((): void => {
    configuredTestingModule = TestBed.configureTestingModule({
      declarations: [MyComponentUnderTest],
      imports: [
        MatDialogModule
      ],
      providers: [
        { provide: MatDialogRef, useValue: {} },
        { provide: MAT_DIALOG_DATA, useValue: { testConfig } }
      ]
    });
  }));

  const buildComponent = (): void => {
    configuredTestingModule.compileComponents(); // <-- compileComponents here
    fixture = TestBed.createComponent(MyComponentUnderTest);
    component = fixture.componentInstance;
    fixture.detectChanges();
  };

  describe('with default mat dialog config', (): void => {
    it('sets the message property in the component to the default', (): void => {
      buildComponent(); // <-- manually call buildComponent helper before each test, giving you more control of when it is called.
      expect(compnent.message).toBe(defaultMessage);
    });
  });

  describe('with custom config', (): void => {
    const customMessage = 'Some custom message';
    beforeEach((): void => {
      testConfig = { customMessage };
      TestBed.overrideProvider(MAT_DIALOG_DATA, { useValue: testConfig }); //< -- override here, before compiling
      buildComponent();
    });
    it('sets the message property to the customMessage value within testConfig', (): void => {
      expect(component.message).toBe(customMessage);
    });
  });

对我来说,这似乎也是最干净和最简单的方法。还有一件事要注意的是,您可以嵌套“describe”块。因此,在我刚重构的文件中,我创建了“buildComponent”函数,用另一个描述包装了所有现有的描述,并在其中使用beforeEach调用buildComponent。现在处理所有正常测试而无需更新每个测试。现在创建另一个顶级描述,在调用buildComponent之前执行您的覆盖操作,这样就可以处理您的自定义测试了。 - maru

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接