This is the test example for a Component
with a Service
using stubs provided in the angular2 documentation.
When I am trying to build it out and run it, I find that the component does not pick up the changes for the second test case. I always see the message.
The service looks like this:
import { Injectable } from '@angular/core';
@Injectable()
export class UserService {
isLoggedIn: true;
user: { name: string };
}
The component looks like this:
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
@Component({
moduleId: module.id,
selector: 'app-welcome',
templateUrl: './welcome.component.html'
})
export class WelcomeComponent implements OnInit {
welcome = '--- not initialized yet';
constructor (private userService: UserService) {}
ngOnInit () {
this.welcome = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name :
'Please log in.';
}
}
This is the unit test in question:
import { async, TestBed, ComponentFixture, ComponentFixtureAutoDetect } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { UserService } from './user.service';
import { WelcomeComponent } from './welcome.component';
let fixture: ComponentFixture<WelcomeComponent>;
let comp: WelcomeComponent;
let de: DebugElement;
let el: HTMLElement;
let userService: UserService;
describe('Welcome Component (testing a component with a service)', () => {
beforeEach(async(() => {
const userServiceStub = {
isLoggedIn: true,
user: {
name: 'Test User'
}
};
return TestBed.configureTestingModule({
declarations: [
WelcomeComponent
],
providers: [
{
provide: ComponentFixtureAutoDetect,
useValue: true
},
{
provide: UserService,
useValue: userServiceStub
}
]
}).compileComponents(); // DO NOT USE WITH WEBPACK
}));
beforeEach(() => {
fixture = TestBed.createComponent(WelcomeComponent);
userService = TestBed.get(UserService);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css('.welcome'));
el = de.nativeElement;
});
it('should welcome the user', () => {
fixture.detectChanges();
const content = el.textContent;
expect(content).toContain('Welcome', '"Welcome..."');
});
it('should welcome Bubba', () => {
userService.user.name = 'Bubba';
fixture.detectChanges();
expect(el.textContent).toContain('Bubba');
});
});
The error is always:
Expected 'Welcome, Test User' to contain 'Bubba'.
Error: Expected 'Welcome, Test User' to contain 'Bubba'.
When debugging, I found that the service stub is updated with the appropriate value.
What you're doing here
welcome = '--- not initialized yet';
ngOnInit () {
this.welcome = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name :
'Please log in.';
}
is only a one time initialization. Any subsequent updates to the service will not cause a re-initialization.
What you can do instead is use a subscription system, where you can subscribe to updates. Maybe something like
welcome = '--- not initialized yet';
ngOnInit () {
this.userService.status$.subscribe(status) => {
let loggedIn = status.isLoggedIn;
let user = status.user;
this.welcome = '...'
})
}
You would then need to change the service to use an Subject
or maybe better a BehaviorSubject
where you can emit new values. Those new emissions would be subscribed to by the component
class UserService {
private _isLoggedIn: boolean = false;
private _user = { name: 'Anonymous' };
private _status = new BehaviorSubject({
isLoggedIn: this._isLoggedIn
user: this._user
});
get status$() {
return this._status.asObservable();
}
updateUserName(name: string) {
this._user.name = name;
this.emit();
}
updateIsLoggedIn(loggedIn: boolean) {
this.isLoggedIn = loggedIn;
if (!loggedIn) {
this._user.name = 'Anonymous;
}
this.emit();
}
private emit() {
this._status.next({
isLoggedIn: this._isLoggedIn,
user: this._user
})
}
}
With the Subject
, you can emit new values by calling next(..)
, and what ever you pass to it, will be emitted to subscribers.
Now in the test, you can just call the service updateUserName
. If you want to stub the service for test, then you can do something like
let mockService = {
status$: new Subject<>();
}
mockService.status$.next(...)
But really, with the service as is, not using any outside dependencies, there is really no need to mock it.
Note also because now the service is asynchronous, you should use async
or fakeAsync
for the tests, e.g.
import { fakeAsync, async, tick + from '@angular/core/testing';
it('..', fakeAsync(() => {
service.updateUserName(..)
tick();
expect(...)
}))
it('..', async(() => {
service.updateUserName(..)
fixture.whenStable().then(() => {
expect(...)
})
}))