I have just started adding webpacker with angular 5 to the existing rails application. All is fine except for a weird issue with a DI in test.
It seems my Angular components are just working when created with browser, but when being tested with Jasmine/Karma, Dependency Injector fails to identify injection tokens. With pseudo code:
@Component({...})
export class SomeComponent {
constructor(private service: SomeService) {}
}
The above works in the browser, but is giving Error: Can't resolve all parameters for SomeComponent: (?).
in test. So far I have noticed it applies to all @Injectable()s, however once I replace each injection with explicit @Inject:
@Component({...})
export class SomeComponent {
constructor(@Inject(SomeService) private service: SomeService) {}
}
everything works (but obviously is quite a cumbersome). Is there anything obvious that could cause this?
I have a very simple service running with HttpClient:
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import 'rxjs/add/operator/map'
@Injectable()
export class GeneralStatsService {
constructor(
private http : HttpClient
) {}
getMinDate() {
return this.http.get("/api/v1/general_stats/min_date")
.map(r => new Date(r))
}
}
which works as expected when I navigate to component that is using said service. However, it does not work when testing with Jasmine:
import { TestBed } from "@angular/core/testing";
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing";
import { GeneralStatsService } from "./general-stats.service";
describe('GeneralStatsService', () => {
let service : GeneralStatsService;
let httpMock : HttpTestingController;
beforeEach(()=> {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
GeneralStatsService
]
})
});
beforeEach(() => {
service = TestBed.get(GeneralStatsService);
httpMock = TestBed.get(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
describe('getMinDate()', () => {
let fakeResponse : string = "2015-03-05T12:39:11.467Z";
it('returns instance of Date', (done) => {
service.getMinDate().subscribe((result : Date) => {
expect(result.getFullYear()).toBe(2015);
expect(result.getMonth()).toBe(2); // January is 0
expect(result.getDate()).toBe(5);
done();
});
const req = httpMock.expectOne("/api/v1/general_stats/min_date");
expect(req.request.method).toBe('GET');
req.flush(fakeResponse);
})
});
});
As mentioned above, adding explicit @Inject(HttpClient)
fixes the test, but I'd prefer to avoid this.
Karma:
const webpackConfig = require('./config/webpack/test.js');
module.exports = function(config) {
config.set({
basePath: '',
frameworks: [ 'jasmine' ],
plugins: [
require('karma-webpack'),
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('karma-spec-reporter')
],
files: [
'config/webpack/angular-bundle.ts'
],
webpack: webpackConfig,
preprocessors: {
'config/webpack/angular-bundle.ts': ["webpack"]
},
mime: { "text/x-typescript": ["ts"]},
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
client: { clearContext: false },
reporters: [ 'progress', 'kjhtml', 'coverage-istanbul' ],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: [ 'Chrome' ],
singleRun: false,
concurrency: Infinity
})
};
config/webpack/test.js:
const environment = require('./environment');
environment.plugins.get('Manifest').opts.writeToFileEmit = process.env.NODE_ENV !== 'test';
environment.loaders.set('istanbul-instrumenter', {
test: /\.ts$/,
enforce: 'post',
loader: 'istanbul-instrumenter-loader',
query: {
esModules: true
},
exclude: ["node_modules", /\.spec.ts$/]
});
module.exports = environment.toWebpackConfig()
config/webpack/angular-bundle.ts:
import 'zone.js/dist/zone'
import 'zone.js/dist/long-stack-trace-zone';
import 'zone.js/dist/proxy.js';
import 'zone.js/dist/sync-test';
import 'zone.js/dist/jasmine-patch';
import 'zone.js/dist/async-test';
import 'zone.js/dist/fake-async-test';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
jasmine.MAX_PRETTY_PRINT_DEPTH = 3;
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
const context = (require as any).context('../../app/javascript', true, /\.spec\.ts$/);
context.keys().map(context);
tsconfig.json:
{
"compilerOptions": {
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es6", "dom"],
"module": "es6",
"moduleResolution": "node",
"sourceMap": true,
"target": "es5"
},
"exclude": [
"**/*.spec.ts",
"node_modules",
"vendor",
"public",
"config/**/*.ts"
],
"compileOnSave": false
}
environment.js:
const environment = require('@rails/webpacker').environment;
const typescript = require('./loaders/typescript');
const erb = require('./loaders/erb');
const elm = require('./loaders/elm');
const html = require('./loaders/html');
environment.loaders.append('elm', elm);
environment.loaders.append('erb', erb);
environment.loaders.append('typescript', typescript);
environment.loaders.append('html', html);
module.exports = environment;
And just in case loaders/typescript:
module.exports = {
test: /\.(ts|tsx)?(\.erb)?$/,
use: [{
loader: 'ts-loader'
}]
}
The @Injectable() decorator defines a class as a service in Angular and allows Angular to inject it into a component as a dependency. Likewise, the @Injectable() decorator indicates that a component, class, pipe, or NgModule has a dependency on a service. The injector is the main mechanism.
@Injectable() lets Angular know that a class can be used with the dependency injector. @Injectable() is not strictly required if the class has other Angular decorators on it or does not have any dependencies. What is important is that any class that is going to be injected with Angular is decorated.
The TestBed provides methods for creating components and services in unit tests. The TestBed methods are inject() , configureTestingModule() etc. To inject a service, we use TestBed. inject() method.
To test a service, you set the providers metadata property with an array of the services that you'll test or mock. content_copy let service: ValueService; beforeEach(() => { TestBed. configureTestingModule({ providers: [ValueService] }); }); Then inject it inside a test by calling TestBed.
Try with the injector and spyOn.
You have to create a mocked service, without the 'HttpClient', that has ALL methods of the Service you want to mock. Then with spyOn you can return what you want.
TestBed.configureTestingModule({
imports: [
FormsModule,
BrowserAnimationsModule
],
providers: [
{
provide: YourService,
useValue: mockedYourService
}
]
....
beforeEach(() => {
fixture = TestBed.createComponent(YourTestingComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
fixture.detectChanges();
});
...
describe('methodName', () => {
it('message to print',
() => {
const your_Service = fixture.debugElement.injector.get(YourService);
spyOn(your_Service, 'methodName').and.returnValue(true);
.....
Hope this help!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With