I'm trying to write a unit test for component used in my service.
Component and service work fine.
Component:
import {Component} from '#angular/core';
import {PonyService} from '../../services';
import {Pony} from "../../models/pony.model";
#Component({
selector: 'el-ponies',
templateUrl: 'ponies.component.html',
providers: [PonyService]
})
export class PoniesComponent {
ponies: Array<Pony>;
constructor(private ponyService: PonyService) {
this.ponies = this.ponyService.getPonies(2);
}
refreshPonies() {
this.ponies = this.ponyService.getPonies(3);
}
}
Service:
import {Injectable} from "#angular/core";
import {Http} from "#angular/http";
import {Pony} from "../../models/pony.model";
#Injectable()
export class PonyService {
constructor(private http: Http) {}
getPonies(count: number): Array<Pony> {
let toReturn: Array<Pony> = [];
this.http.get('http://localhost:8080/js-backend/ponies')
.subscribe(response => {
response.json().forEach((tmp: Pony)=> { toReturn.push(tmp); });
if (count && count % 2 === 0) { toReturn.splice(0, count); }
else { toReturn.splice(count); }
});
return toReturn;
}}
Component unit test:
import {TestBed} from "#angular/core/testing";
import {PoniesComponent} from "./ponies.component";
import {PonyComponent} from "../pony/pony.component";
import {PonyService} from "../../services";
import {Pony} from "../../models/pony.model";
describe('Ponies component test', () => {
let poniesComponent: PoniesComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [PoniesComponent, PonyComponent],
providers: [{provide: PonyService, useClass: MockPonyService}]
});
poniesComponent = TestBed.createComponent(PoniesComponent).componentInstance;
});
it('should instantiate component', () => {
expect(poniesComponent instanceof PoniesComponent).toBe(true, 'should create PoniesComponent');
});
});
class MockPonyService {
getPonies(count: number): Array<Pony> {
let toReturn: Array<Pony> = [];
if (count === 2) {
toReturn.push(new Pony('Rainbow Dash', 'green'));
toReturn.push(new Pony('Pinkie Pie', 'orange'));
}
if (count === 3) {
toReturn.push(new Pony('Fluttershy', 'blue'));
toReturn.push(new Pony('Rarity', 'purple'));
toReturn.push(new Pony('Applejack', 'yellow'));
}
return toReturn;
};
}
Part of package.json:
{
...
"dependencies": {
"#angular/core": "2.0.0",
"#angular/http": "2.0.0",
...
},
"devDependencies": {
"jasmine-core": "2.4.1",
"karma": "1.2.0",
"karma-jasmine": "1.0.2",
"karma-phantomjs-launcher": "1.0.2",
"phantomjs-prebuilt": "2.1.7",
...
}
}
When I execute 'karma start' I get this error
Error: Error in ./PoniesComponent class PoniesComponent_Host - inline template:0:0 caused by: No provider for Http! in config/karma-test-shim.js
It looks like karma uses PonyService instead of mocking it as MockPonyService, in spite of this line: providers: [{provide: PonyService, useClass: MockPonyService}].
The question: How I should mock the service?
It's because of this
#Component({
providers: [PonyService] <======
})
This makes it so that the service is scoped to the component, which means that Angular will create it for each component, and also means that it supercedes any global providers configured at the module level. This includes the mock provider that you configure in the test bed.
To get around this, Angular provides the TestBed.overrideComponent method, which allows us to override things like the #Component.providers and #Component.template.
TestBed.configureTestingModule({
declarations: [PoniesComponent, PonyComponent]
})
.overrideComponent(PoniesComponent, {
set: {
providers: [
{provide: PonyService, useClass: MockPonyService}
]
}
});
Another valid approach is to use tokens and rely on Intefaces instead of base classes or concrete classes, which dinosaurs like me love to do (DIP, DI, and other SOLID Blablahs). And allow your component to have its dependencies injected instead of providing it yourself in your own component.
Your component would not have any provider, it would receive the object as an interface in its constructor during angular's magic dependency injection. See #inject used in the constructor, and see the 'provide' value in providers as a text rather than a class.
So, your component would change to something like:
constructor(#Inject('PonyServiceInterface') private ponyService: IPonyService) {
this.ponies = this.ponyService.getPonies(2); }
In your #Component part, you would remove the provider and add it to a parent component such as "app.component.ts". There you would add a token:
providers: [{provide: 'PonyServiceInterface', useClass: PonyService}]
Your unit test component (the analog to app.component.ts) would have:
providers: [{provide: 'PonyServiceInterface', useClass: MockPonyService}]
So your component doesn't care what the service does, it just uses the interface, injected via the parent component (app.component.ts or your unit test component).
FYI: The #inject approach is not very widely used, and at some point it looks like angular fellows prefer baseclasses to interfaces due to how the underlying javascript works.
Related
I have been struggling with this for a while, and I'm hoping someone can help. I have a component that uses a service to get data. I am attempting to add unit tests to it. My problem is that the tests always fail with "Error: No provider for Http". Here is my code:
Service:
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import { Contact } from './contact.model';
#Injectable()
export class ContactsService {
constructor(private http: Http) { }
public getContacts(): Observable<Array<Contact>> {
return this.http.get('assets/contacts.json').map(res => {
let r = res.json().Contacts;
return r;
});
}
}
Component:
import { Component, OnInit, NgModule } from '#angular/core';
import { FormsModule } from '#angular/forms';
import { Contact } from '../contact.model';
import { ContactsService } from '../contacts.service';
#Component({
selector: 'app-contacts',
templateUrl: './contacts.component.html',
styleUrls: ['./contacts.component.css'],
providers: [ContactsService]
})
export class ContactsComponent implements OnInit {
contactsAll: Array<Contact>;
contacts: Array<Contact>;
constructor(private contactsService: ContactsService) { }
ngOnInit() {
this.contactsService.getContacts().subscribe((x) => {
this.contactsAll = x;
this.contacts = this.contactsAll;
});
}
}
Tests:
import { async, ComponentFixture, TestBed, inject } from '#angular/core/testing';
import { FormsModule } from '#angular/forms';
import { By } from '#angular/platform-browser';
import { Observable } from 'rxjs/Rx';
import { ContactsComponent } from './contacts.component';
import { ContactsService } from '../contacts.service';
import { Contact } from '../contact.model';
class MockContactsService extends ContactsService {
constructor() {
super(null);
}
testContacts: Array<Contact> = [
new Contact("test1 mock", 12345, 10000),
new Contact("test2 mock", 23456, 20000)
];
public getContacts(): Observable<Array<Contact>> {
return Observable.of(this.testContacts);
}
}
describe('ContactsComponent', () => {
let component: ContactsComponent;
let fixture: ComponentFixture<ContactsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [ContactsComponent],
// providers: [{ provide: ContactsService, useClass: MockContactsService }] // this is needed for the service mock
}).overrideComponent(ContactsService, {// The following is to override the provider in the #Component(...) metadata
set: {
providers: [
{ provide: ContactsService, useClass: MockContactsService },
]
}
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContactsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('1st test', () => {
it('true is true', () => expect(true).toBe(true));
})
});
Let's try this:
First, move your providers array from your component to your NgModule. It's better to provide your services at the module level since it de-couples your providers from your component tree structure (unless you specifically want to have a separate instance of a provider per component, and from your simplified use case, there's no need for a separate instance per component).
so,
#Component({
selector: 'app-contacts',
templateUrl: './contacts.component.html',
styleUrls: ['./contacts.component.css'],
/// providers: [ContactsService] <-- remove this line
})
export class ContactsComponent implements OnInit {
.....
and add it to the NgModule that declares your ContactsComponent
#NgModule({
imports: ..
declarations: ...
providers: [ContactsService] // <-- provider definition moved to here
})
export class ModuleDeclaringContactsComponent
Once you do that, then mocking the ContactsService in your test is easy to implement.
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [ContactsComponent],
providers: [{ provide: ContactsService, useClass: MockContactsService }] // this is needed for the service mock
});
With that, you should be good to go.
Sorry everyone - turns out it was something completely different.
I modified my code as per snorkpete's answer, and I am going to mark that as the answer, as I believe that is the cleanest approach.
The real problem came from using Angular Cli to create my project. It automatically created tests for my component and my service. This meant the code in the service test was causing the error, not the code in the component. I commented out the code in the service test and everything passed.
Annoyingly, there was no indication in any of the failures that this is where the error was coming from!
In case if component should have Service in providers we can ovverride metadata by TestBed.overrideComponent():
#Component({
selector: 'app-contacts',
templateUrl: './contacts.component.html',
styleUrls: ['./contacts.component.css'],
providers: [ContactsService] // IF YOU NEED IT
})
we need to do next:
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [ SomeComponent ],
providers: [
{provide: SomeService, useValue: SomeMock},
provideMockStore({system: {userConfig: {viewSettings: {theme: ThemeName.light}}}} as any)
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
TestBed.overrideComponent(SomeComponent , { set: { providers: [{provide: SomeService, useValue: SomeMock}]}})
}));
beforeEach(() => {
fixture = TestBed.createComponent(SomeComponent );
component = fixture.componentInstance;
fixture.detectChanges();
});
more info here https://codecraft.tv/courses/angular/unit-testing/dependency-injection/
I'm new to angular 2 and I have some problems testing my code. I use the jasmine testing framework and the karma test runner to test my app.
I have a component (called GroupDetailsComponent) that I want to test. This component uses two services (GroupService & TagelerServie, both have CRUD methods to talk to an API) and some pipes in the html file. My component looks like this:
import 'rxjs/add/operator/switchMap';
import { Component, Input, OnInit } from '#angular/core';
import { Tageler } from '../../tagelers/tageler';
import { TagelerService } from '../../tagelers/tageler.service';
import { Params, ActivatedRoute } from '#angular/router';
import { GroupService} from "../group.service";
import { Group } from '../group';
#Component({
selector: 'app-group-details',
templateUrl: 'group-details.component.html',
styleUrls: ['group-details.component.css'],
})
export class GroupDetailsComponent implements OnInit {
#Input()
tageler: Tageler;
tagelers: Tageler[];
group: Group;
constructor(
private route: ActivatedRoute,
private groupService: GroupService,
private tagelerService: TagelerService) {
}
ngOnInit() {
console.log("Init Details");
this.route.params
.switchMap((params: Params) => this.groupService.getGroup(params['id']))
.subscribe(group => this.group = group);
this.tagelerService
.getTagelers()
.then((tagelers: Tageler[]) => {
// some code
}
return tageler;
});
});
}
}
And the test file looks like this:
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { GroupDetailsComponent } from './group-details.component';
import { FilterTagelerByGroupPipe } from '../../pipes/filterTagelerByGroup.pipe';
import { SameDateTagelerPipe } from '../../pipes/sameDateTageler.pipe';
import { CurrentTagelerPipe } from '../../pipes/currentTageler.pipe';
import { NextTagelerPipe } from '../../pipes/nextTageler.pipe';
import { RouterTestingModule } from '#angular/router/testing';
import { GroupService } from '../group.service';
import { TagelerService } from '../../tagelers/tageler.service';
describe('GroupDetailsComponent', () => {
let component: GroupDetailsComponent;
let fixture: ComponentFixture<GroupDetailsComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
GroupDetailsComponent,
FilterTagelerByGroupPipe,
SameDateTagelerPipe,
CurrentTagelerPipe,
NextTagelerPipe, ],
imports: [ RouterTestingModule ],
providers: [{provide: GroupService}, {provide: TagelerService}],
})
fixture = TestBed.createComponent(GroupDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
class MockGroupService {
getGroups(): Array<Group> {
let toReturn: Array<Group> = [];
toReturn.push(new Group('Trupp', 'Gruppe 1'));
return toReturn;
};
}
it('should create component', () => {
expect(component).toBeDefined();
});
});
I read the angular 2 documentation about testing and a lot of blogs, but I still don't really understand how to test a component that uses services and pipes. When I start the test runner, the test 'should create component' fails and I get the message that my component is not defined (but I don't understand why). I also don't understand how I have to inject the services and pipes. How do I mock them the right way?
I hope that someone can give me helpful advice!
Ramona
You can use spyOn to fake the call in jasmine.
spyOn(yourService, 'method').and.returnValue($q.resolve(yourState));
Created a new angular 2 project with angular-cli
Below is the default component app.component.ts and it has app.component.spec.ts
import { Component } from '#angular/core';
import { AngularFire } from 'angularfire2'; // import angularfire2
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app works!';
af: AngularFire;
constructor(af: AngularFire){
this.af=af;
this.firebaseCall();
}
//push data to firebase collection
firebaseCall(){
let post=this.af.database.list('/post');
post.push({a:'test'});
}
}
To Implement Unit test for the above firebaseCall() in app.component.spec.ts
I have added/updated below lines in app.component.spec.ts
import { AngularFire } from 'angularfire2';
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,AngularFire // extra added
],
});
TestBed.compileComponents();
});
I get below error while ng test
Unexpected value 'AngularFire' declared by the module 'DynamicTestModule'
You need to mock the services in the test.
Here you are injecting AngularFire. So you test setup should be like this.
import { AngularFire } from 'angularfire2';
...
const mockFirebase = jasmine.createSpyObj('af',['database']);
af.database.and.returnValue({list: Rx.Observable.of([])});
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
providers:[
{provide:AngularFire, useValue:}]
});
TestBed.compileComponents();
});
Hope, this helps you. always mock your providers in the unit-test and don't make a http call.
I am trying to setup a Karma Jasmine Unit test for a Angular 2 service. The service is called CreditService. The service has a observable property _credit$ and implements a ng2-redux selector #select(s => s.credit) to get the credit object from a Redux store.
import {Injectable, Inject} from '#angular/core';
import {ICreditData, ICreditSubmissionData, ContractType, ICredit} from '../../store/type-store/credit.types';
import 'rxjs/add/operator/map';
import {AppStateService} from '../app-state-service/app-state-service';
import {select} from 'ng2-redux';
import {Observable} from 'rxjs';
import {PromiseService} from '../promise/promise-service';
export interface ICreditResponse {
errors?: string[];
token?: string;
response?: ICreditData;
submission?: ICreditSubmissionData;
}
export interface ICreditLead {
address: string;
aptSuite: string;
city: string;
state: string;
zipCode: number;
email: string;
monthlyIncome: number;
birthDate: string;
socialSec: number;
contactPolicy: boolean;
}
#Injectable()
export class CreditService {
#select(s => s.credit)
private _credit$: Observable<ICredit>;
public isPostGA2: boolean = false;
constructor(#Inject(AppStateService) private _appStateService: AppStateService,
#Inject(PromiseService) private _promiseService: PromiseService) {
this.setupSubscriptionForPostGA2();
}
/**
* Method will setup subscription to determine of credit has been
* pulled or credit has already been pulled and exists in the Redux store.
*/
public setupSubscriptionForPostGA2(): void {
// subscribe to the store check for isPostGA2
this._credit$.subscribe(
credit => {
let creditDataExists = (credit.data !== undefined) || credit.data !== null;
let creditLeadIDExists = (credit.data.LeadID !== undefined) || credit.data.LeadID !== null;
let creditHistoryExists = (creditDataExists && creditLeadIDExists);
this.isPostGA2 = (credit.loadComplete || creditHistoryExists);
},
err => {}
);
}
}
The unit test is pretty basic at this point. I check to see if the CreditService is defined. My unit test looks as follows:
import { fakeAsync, inject, TestBed } from '#angular/core/testing';
import { DtmAppModule } from '../../modules/app.module';
import { configureTests } from '../../tests.configure';
import { CreditService } from './credit-service';
import {MockAppStateService} from '../../mock/service/app-state-service-mock';
import {MockPromiseService} from '../../mock/service/promise-service-mock';
import { AppStateService } from '../../services/app-state-service/app-state-service';
import { PromiseService } from '../../services/promise/promise-service';
import {NgRedux} from 'ng2-redux';
describe('Service: Credit', () => {
let creditService: CreditService = null;
beforeEach(done => {
const configure = (testBed: TestBed) => {
testBed.configureTestingModule({
imports: [DtmAppModule],
providers: [
{ provide: AppStateService, useClass: MockAppStateService },
{ provide: PromiseService, useClass: MockPromiseService },
{ provide: NgRedux, useClass: NgRedux },
]
});
};
configureTests(configure).then(testBed => {
done();
});
});
// Inject the service
beforeEach(inject([CreditService], (service: CreditService) => {
creditService = service;
}));
//Do a simple test
it('should have a defined service', () => {
expect(creditService).toBeDefined(false);
});
});
I am getting a exception when executing the test (see below). I believe the exception is because I am trying to preform a 'select' method on a undefined Redux store object.
How do I fix the basic 'toBeDefined' test?
How can I inject Rxjs mock store for testing purposes?
Is there a way to mock the return from the _credit$ Observable ?
I am getting a exception when executing the test (see below). I believe the exception is the 'select' of a undefined Redux store.
TypeError: Cannot read property 'select' of undefined
at CreditService.getter [as _credit$] (webpack:///~/ng2-redux/lib/decorators/select.js:23:0 <- src/tests.entry.ts:150235:47)
at CreditService.setupSubscriptionForPostGA2 (webpack:///src/services/credit/credit-service.ts:47:17 <- src/tests.entry.ts:24169:17)
at new CreditService (webpack:///src/services/credit/credit-service.ts:40:2 <- src/tests.entry.ts:24155:14)
at DynamicTestModuleInjector.get (DynamicTestModule.ngfactory.js:367:71)
at DynamicTestModuleInjector.getInternal (DynamicTestModule.ngfactory.js:638:53)
at DynamicTestModuleInjector.NgModuleInjector.get (webpack:///~/#angular/core/src/linker/ng_module_factory.js:94:0 <- src/tests.entry.ts:91283:27)
at TestBed.get (webpack:///~/#angular/core/bundles/core-testing.umd.js:1114:0 <- src/tests.entry.ts:9003:51)
at webpack:///~/#angular/core/bundles/core-testing.umd.js:1120:50 <- src/tests.entry.ts:9009:65
at Array.map (native)
at TestBed.execute (webpack:///~/#angular/core/bundles/core-testing.umd.js:1120:0 <- src/tests.entry.ts:9009:33)
You can provide an empty mock class to overwrite select:
export class MockSelect {
}
and then add it to your list of providers:
{provide: select, useClass: MockSelect}
You can mock out observables in the same way:
export class MockObservable<T> {
}
Then just set the values yourself according to what you need in your unit test.
I am trying to test a component that receives a reference to ElementRef through DI.
import { Component, OnInit, ElementRef } from '#angular/core';
#Component({
selector: 'cp',
templateUrl: '...',
styleUrls: ['...']
})
export class MyComponent implements OnInit {
constructor(private elementRef: ElementRef) {
//stuffs
}
ngAfterViewInit() {
// things
}
ngOnInit() {
}
}
and the test:
import {
beforeEach,
beforeEachProviders,
describe,
expect,
it,
inject,
} from '#angular/core/testing';
import { ComponentFixture, TestComponentBuilder } from '#angular/compiler/testing';
import { Component, Renderer, ElementRef } from '#angular/core';
import { By } from '#angular/platform-browser';
describe('Component: My', () => {
let builder: TestComponentBuilder;
beforeEachProviders(() => [MyComponent]);
beforeEach(inject([TestComponentBuilder], function (tcb: TestComponentBuilder) {
builder = tcb;
}));
it('should inject the component', inject([MyComponent],
(component: MyComponent) => {
expect(component).toBeTruthy();
}));
it('should create the component', inject([], () => {
return builder.createAsync(MyComponentTestController)
.then((fixture: ComponentFixture<any>) => {
let query = fixture.debugElement.query(By.directive(MyComponent));
expect(query).toBeTruthy();
expect(query.componentInstance).toBeTruthy();
});
}));
});
#Component({
selector: 'test',
template: `
<cp></cp>
`,
directives: [MyComponent]
})
class MyTestController {
}
Both the component and the test blueprint have been generated by Angular-cli. Now, I can't figure out which provider, if any, I should add in the beforeEachProviders for the injection of ElementRef to be successful. When I run ng test I got Error: No provider for ElementRef! (MyComponent -> ElementRef).
I encounter Can't resolve all parameters for ElementRef: (?) Error using the mock from #gilad-s in angular 2.4
Modified the mock class to:
export class MockElementRef extends ElementRef {
constructor() { super(null); }
}
resolves the test error.
Reading from the angular source code here: https://github.com/angular/angular/blob/master/packages/core/testing/src/component_fixture.ts#L17-L60
the elementRef of the fixture is not created from the mock injection. And in normal development, we do not explicitly provide ElementRef when injecting to a component. I think TestBed should allow the same behaviour.
On Angular 2.2.3:
export class MockElementRef extends ElementRef {}
Then in the test:
beforeEach(async(() => {
TestBed.configureTestingModule({
providers: [
//more providers
{ provide: ElementRef, useClass: MockElementRef }
]
}).compileComponents();
}));
To inject an ElementRef:
Create a mock
class MockElementRef implements ElementRef {
nativeElement = {};
}
Provide the mock to the component under test
beforeEachProviders(() => [Component, provide(ElementRef, { useValue: new MockElementRef() })]);
EDIT: This was working on rc4. Final release introduced breaking changes and invalidates this answer.
A good way is to use spyOn and spyOnProperty to instant mock the methods and properties as needed. spyOnProperty expects 3 properties and you need to pass get or set as third property. spyOn works with class and method and returns required value.
Example
const div = fixture.debugElement.query(By.css('.ellipsis-overflow'));
// now mock properties
spyOnProperty(div.nativeElement, 'clientWidth', 'get').and.returnValue(1400);
spyOnProperty(div.nativeElement, 'scrollWidth', 'get').and.returnValue(2400);
Here I am setting the get of clientWidth of div.nativeElement object.
It started showing up after I did a package update in my Angular project.
I tried all the solutions above and none of them worked for me. The problem occurred when I ran the npm run test command.
I managed to solve by updating all jest dependencies. In this case, the dependencies I updated were #briebug/jest-schematic, #types/jest, jest-preset-angular and jest