error with angular2 testing component with Testbed
Asked Answered
S

1

7

trying to learn this testing utility TestBed in angular-2 with a simple example and have hit my first blocker. google or SO search didn't yield any matching example,

so, I have a very basic component header as below -

import { Component } from '@angular/core';

@Component({
    selector: 'header',
    template: ''
})
export class HeaderComponent{
    public title: string;

    constructor(testparam: string){
        this.title = 'test';
    }
}

and then have its spec as below -

import { TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';

describe('HeaderComponent Test', () => {
    let component: HeaderComponent;

    beforeEach(() => {
        TestBed.configureTestingModule({
            declarations: [HeaderComponent]
        });

        const fixture = TestBed.createComponent(HeaderComponent);
        component = fixture.componentInstance;
    });

    it('should have the component defined', () => {
        expect(component).toBeDefined();
    });

    it('should initialize the title to test', () => {
        expect(component.title).toBe('test');
    });
});

running the karma test is throwing - Error: No provider for String! in karma.entry.js

karma.entry.js is basically just setting the test env configuration for TestBed and then goes thru each test in my spec folder, below is my karma.entry.js

require('core-js/es6');
require('core-js/es7/reflect');

require('es6-shim');
require('reflect-metadata');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy');
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch');
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
require('rxjs/Rx');

const browserTesting = require('@angular/platform-browser-dynamic/testing');
const coreTesting = require('@angular/core/testing');

coreTesting.TestBed.initTestEnvironment(
   browserTesting.BrowserDynamicTestingModule,
   browserTesting.platformBrowserDynamicTesting()
);

const context = require.context('../src', true, /\.spec\.ts$/);

context.keys().forEach(context);

Error.stackTraceLimit = Infinity;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000;

If I remove the parameter from the constructor of the component class, the tests pass, so I am thinking that I am missing some pre-configuration thats causing the TestBed.createComponent(HeaderComponent) not to properly compile the component's constructor with the string type parameter.

any clue what I might be missing?


UPDATE:

if it helps anyone - based on @mrkosima's answer, my updated component class now looks like below and the unit tests all pass good now :)

import { Component,  OpaqueToken, Inject } from '@angular/core';

export let TITLE_TOKEN = new OpaqueToken('title token');

@Component({
    selector: 'header',
    template: '',
    providers: [{ provide: TITLE_TOKEN, useValue: 'test' }]
})
export class HeaderComponent{
    public title: string;

    constructor(@Inject(TITLE_TOKEN) titleParam: string){
        this.title = titleParam;
    }
}
Siward answered 12/2, 2017 at 19:23 Comment(0)
K
6

You are right that the root cause of issue in the constructor's argument.

During component instantiation Injector trying to resolve all dependencies listed in constructor. Injector looks up dependencies by type in providers. More about DI here: https://angular.io/docs/ts/latest/guide/dependency-injection.html

That means if component has constructor(authService: AuthService) { }, the Injector looking for AuthService token in providers.

The same in your case - your component depends on String. But there is no any provider with String token.

Actually, it's a mistake to list primitive type as dependency.

Instead of this OpaqueToken should be used

export let TITLE_TOKEN = new OpaqueToken('title token');

Configure token in module providers

providers: [{ provide: TITLE_TOKEN, useValue: 'title value' }]

Than inject token in component:

constructor(@Inject(TITLE_TOKEN) title: string) {
  this.title = title;
}

That's the correct usage of injecting primitive.

More details here: https://angular.io/docs/ts/latest/guide/dependency-injection.html#!#opaquetoken

PS: to test your component the TITLE_TOKEN should be added to testing module:

import {TITLE_TOKEN} from ...
TestBed.configureTestingModule({
      providers: [ { provide: TITLE_TOKEN, useValue: 'test' } ]
});

And than create test component and expect title as 'test'.

Kramatorsk answered 12/2, 2017 at 19:55 Comment(1)
ok, so its the angular DI framework that TestBed uses to instantiate the component class because of which its not possible to inject primitive types or interfaces sort of things directly in the component's constructor, and needs OpaqueToken wrapper. Earlier when I read DI, I skipped this portion just out of laziness :) and this was the issue. Thnx @mrkosima, I think this DI pattern makes sense because now with Typescript we would mostly be using OOPS patterns and would mostly be passing class-types or angular services to component constructors , thanks again.Siward

© 2022 - 2024 — McMap. All rights reserved.