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