Issues with overriding an ngrx store setup in a test with TestBed.overrideModule

69 Views Asked by At

I am testing an angular/NgRx application and I am facing difficulties overriding the Store with the angular TestBed.

Please note that for the purpose of this test suite, I cannot rely on the provideMockStore utility. I have to use the actual Store.

Here is the global test setup in the top-level describe:

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [
        SmartRecoRecommendationComponent,
        ...
      ],
      imports: [
        ...
        StoreModule.forRoot({
          products: productsReducer,
          ...
        }),
        StoreModule.forFeature(smartRecoFeatureKey, {
          ...fromSmartReco.reducers,
      recommendation: (
        state: {
          products: ProductRecommendation[];
        } | null = smartRecoStateMock.recommendation,
        type: Action
      ) => fromSmartReco.reducers.recommendation(state, type),
        ...

Here is how I try to override the Store import in a local describe:

  beforeEach(async () => {
   TestBed.overrideModule(StoreModule, {
    remove: {
      imports: [StoreModule.forRoot({}), StoreModule.forFeature(smartRecoFeatureKey, {})],
    },
    add: {
      imports: [
        StoreModule.forRoot({
          products: productsReducer,
          ...
        }),
        StoreModule.forFeature(smartRecoFeatureKey, {
          ...fromSmartReco.reducers,
          recommendationErrorType: (state: ProductRecommendationErrorTypes = ProductRecommendationErrorTypes.TECHNICAL, type: Action) =>
        }),
      ],
    },
  });
  await TestBed.compileComponents();
  ...

However, the Store is not overridden by the local setup... Only the global setup is taken into account.

The documentation about overriding providers or modules is scarce and I am not sure what I got wrong. Can someone please help?

1

There are 1 best solutions below

0
Shlang On BEST ANSWER

TestBed.overrideModule is used for overriding metadata (an object passed to @NgModule decorator) of a certain module used for setting up a TestBed. So the call TestBed.overrideModule(StoreModule, ...) overrides StoreModule's imports but what you need instead is to override the imports of the module created with TestBed.configureTestingModule. It seems that there is no way to do it (more info at the end of the answer), but there are other ways to achieve the same result.

  1. Call TestBed.configureTestingModule in nested beforeEach or even as part of it. That is it, you don't have to have only 1 configuration in the top-level beforeEach. The most obvious downside of this approach is that you may have a lot of duplication, but it is always possible to extract imports, providers, and other parts into arrays, or create your own wrapper around TestBed.configureTestingModule:
describe('Top Level', () => {
  const commonImports = [
    StoreModule.forRoot({
      products: productsReducer,
      /* ... */
    })
  ];

  describe('Test with feature A', () => {
    beforeEach(async () => {
      await TestBed.configureTestingModule({
        imports: [
          ...commonImports,
          StoreModule.forFeature(smartRecoFeatureKey, /* ... */)
        ],
      }).compileComponents();
    });
  });
});
  1. This does not really seem to apply to your case, but just for completeness - it is possible to use TestBed.override* methods. For example, TestBed.overrideProvider can be used for overriding a provider:
describe('Top Level', () => {
  beforeEach(async () => {
    TestBed.configureTestingModule({
      imports: [AppComponent],
      providers: [{ provide: Test, useValue: '123' }]
    });
  });

  it(`should have the 'ssr' title`, async () => {
    TestBed.overrideProvider(Test, { useValue: '111' });
    expect(TestBed.inject(Test)).toEqual('111');
  });
});

This way it should be possible to override certain providers, provided by StoreModule, but it may be quite hard to understand what exactly should be overridden, as StoreModule uses a lot of providers.


About "It seems that there is no way to do it". Among other things TestBed.configureTestingModule does under the hood, it creates a module and a reference to this module is accessible via private fields: (TestBed as any).INSTANCE.testModuleRef, but as long as module metadata is passed through a decorator, at the time a reference to a module exists it is too late to override it. Because of the same reason, calls to TestBed.override* should be done before the test module is instantiated.