0

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.

vamsiampolu
  • 6,328
  • 19
  • 82
  • 183
  • Are you accessing the service directly in the template? – Paul Samsotha Mar 14 '17 at 04:18
  • @peeskillet I am trying to use the stub and retrieve it in `beforeEach` using `TestBed.get(UserService);`. It takes the stub and turns it into a service but does not allow me make changes to the value. I am creating a property in `ngOnInit` which accesses the service. – vamsiampolu Mar 14 '17 at 04:24
  • Yeah but what makes you think that change the value in the service should affect the template, unless you are accessing the service directly in the template. If you are assigning a variable in the component based on the value from the service, this is only an initial initialization, and will not update the component variable on service updates. You might want to look into using a Subject for your service instead and subscribe the value in the component. – Paul Samsotha Mar 14 '17 at 04:29
  • When do you expect `ngOnInit` to be called again? Put a `console.log` in and see. – jonrsharpe Mar 14 '17 at 04:29
  • @peeskillet, can you provide an example of using a `Subject` within my component. – vamsiampolu Mar 14 '17 at 04:33
  • @jonrsharpe, I see that `ngOnInit` is called once for each `it` block. However, it does not detect changes to the service, I believe the code for the service does not correspond to the stub or has been configured incorrectly with the component. There is no working example within the testing docs – vamsiampolu Mar 14 '17 at 04:38
  • No, it doesn't "detect changes", and I don't see why you expected it would. It's just standard assignment, a one-off calculation of the resulting value. Nothing is incorrectly configured; your mental model of what you've implemented is incorrect. – jonrsharpe Mar 14 '17 at 05:13

1 Answers1

2

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(...)
  })
}))
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720