angularngrxngrx-effectsngrx-store-4.0

Ngrx testing - How to configure TestBed in order to instantiate the entire state store in a test


I want to run some integration tests for the ngrx 4 state store in an Angular 5 app. My desire is to test dispatching ngrx actions, effects, webapi, mock server and selectors all together in one shot. Writting separate tests for effects and reducers is going to be a time sink and I need to cut some corners. I only need to know when one of my pages fails not the exact spot.

I managed to do this in a previous project while using redux together with Angular 2. That was quite easy to setup. However, with ngrx I have some trouble. Somehow AccountsWebapi is not properly injected in AccountsEffects class when running the TestBed. Instead of receiving an instance of the webapi, it looks like I'm getting the constructor. At the same time, the same webapi seems to be injected and instantiated properly in _AccountsService.

accounts.service.spec.ts

describe("AccountsService - ", () => {

    let accService: AccountsService;

    beforeAll( ()=> {
        TestBed.resetTestEnvironment();
        TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
    });

    beforeEach(async(() => {

        TestBed.configureTestingModule({
            imports: [
                StoreModule.forRoot({appReducer}),
                EffectsModule.forRoot([AccountsEffects]),
            ],
            declarations: [],
            providers: [
                { provide: AccountsService, useValue: AccountsService },
                { provide: AccountsWebapi, useValue: AccountsWebapi },
                // ... Other dependencies
            ]
        }).compileComponents();

        // Instantiation
        let _AccountsUtilsService = TestBed.get(AccountsUtilsService);
        let _store = TestBed.get(Store);
        let _AccountsService = TestBed.get(AccountsService);
        // ... Other dependencies

        accService = new _AccountsService(
            new _AccountsUtilsService(),
            new _AccountsWebapi(), 
            _store,
            // ... Other dependencies
        );

    }));

    it("Sample Test", () => {
        console.log(`Accounts SERVICE`, accService.accountsWebapi);
        accService.getAccountAsync(1);
        expect(1).toEqual(1);
    });

accounts.effects.ts

@Injectable()
export class AccountsEffects {

    constructor(
        private actions$: Actions,
        private store$: Store<AppState>,
        private accountsService: AccountsService,
        private accountsWebapi: AccountsWebapi,
    ) {
        console.log('Accounts EFFECTS', this.accountsWebapi)

        // Typing this line will instantiate the dependency...
        this.accountsWebapi = new (this.accountsWebapi as any)()
    }

Console log

enter image description here


Solution

  • First attempt. Do not do it this way! Read bellow

    Finally, I found a way to get this kind of test running. I am 100% sure there is a far better way to do it. Until I find it, this has to do.

    /** Force initialise the web api in effects */
    @Injectable()
    class AccountsEffectsTest extends AccountsEffects {
    
        constructor(
            protected _actions$: Actions,
            protected _store$: Store<AppState>,
            protected _accountsWebapi: AccountsWebapi,
        ) {
            super(
                _actions$,
                _store$,
                _accountsWebapi,
            );
    
            // This line looks ugly but it spares a lot of imports
            // Certainly there is a better way then this
            let providers = ((new HttpModule()) as any).__proto__.constructor.decorators['0'].args['0'].providers;
            let injector = ReflectiveInjector.resolveAndCreate([
                ...providers
            ]);
    
            this.accountsWebapi = new (_accountsWebapi as any)(
                null,
                injector.get(Http)
            );
    
            console.log('Accounts EFFECTS', this.accountsWebapi);
        }
    
    }
    

    Finally, I can get an instance of the webapi in the effects capable of running real requests. Now all I have to do is to mock a webapi. I know it goes against the grain. I've done my fair share of research on what is the best way to test. I still find it more suitable to run integration tests rather than unit tests for the kind of app I'm building.

    Edit (the proper answer) I will keep the previous answer just to demonstrate how willing I was to ignore a code smell...

    TestBed can instantiate EffectsModule

    Due to some misunderstanding about how to configure the TestBed the AccountsWebapi constructor was delivered instead of the instance. That was because I was using { provider: AccountsWebapi, useValue: AccountsWebapi }. The proper setup is to make use of useClass as demonstrated here { provider: AccountsWebapi, useClass: AccountsWebapi }. When using a class the dependency injection framework will trigger the construction step and it will inject the proper dependencies as longs as they are configured properly.

    Another issue was that the AuthHttp service was not provided in AccountsWebapi. This was easily solved by declaring the HttpModule as a dependency in the testBed imports. Also, this was a good opportunity for me to learn that I have several tightly coupled services that need to be provided together in the TestBed. In the future, I need to reduce the number of dependencies between services or loosen them by using events.

    After finishing all these modifications the TestBed can be easily instantiated without the need to wrap the EffectsClass in another layer that takes care of initing the AccountWebapi. That was a grossly overengineered solution due to lack of proper understanding of the dependency injection framework.