Home > database >  Angular Jasmine - testing loaded BehaviorSubject as Observable in service
Angular Jasmine - testing loaded BehaviorSubject as Observable in service

Time:10-07

I have an injectable service for getting a user's roles and permissions over HTTP. To ensure the HTTP call is made only once, I set up a BehaviorSubject inside the service and expose a public Observable, so any component injecting the service can subscribe to it to check whether permissions have loaded.

user.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class UserService {

  private _loaded = new BehaviorSubject<boolean>(false);
  public $loaded = this._loaded.asObservable();
  userDetailsUrl = "/serviceurl";

  constructor(private _http: HttpClient) {
    this.getUserDetails();
  }

  getUserDetails() {
    this._http.get(this.userDetailsUrl).subscribe({
      next: (data) => {
        // set permissions...
        this._loaded.next(true);
      }
    });
  }

  // getters for roles and permissions...

some.component.ts

@Component({...})
export class SomeComponent implements OnInit {

  loadedRoles: Subscription;

  constructor(private _user: UserService) {}

  ngOnInit() {
    this.loadedRoles = this._user.$loaded.subscribe({
      next: (data) => // get roles and permissions to restrict component functionality...
    });
  }
  // ...

In the specfile, I was expecting to be able to test the loaded subscription by calling getUserDetails again after service creation.

user.service.spec.ts

describe('SessionService', () => {
  let service: SessionService;
  let httpClient: HttpClient;
  let httpTestingController: HttpTestingController;
  const userDetailsUrl = "/serviceurl";
  const data1 = {
    "roles": ["TESTER"]
  };
  let firstReq;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule]
    });
    service = TestBed.inject(SessionService);
    httpClient = TestBed.inject(HttpClient);
    httpTestingController = TestBed.inject(HttpTestingController);
    
    firstReq = httpTestingController.expectOne({
      'url': userDetailsUrl,
      'method': 'get'
    });
  }));
  afterEach(() => {
    httpTestingController.verify();
  });

  it('should create and get user details over HTTP', () => {
    expect(service).toBeTruthy();
    expect(firstReq.request.method).toEqual('GET');
  });

  it('should be loaded', () => {
    service.$loaded.subscribe({
      next: (data) => {
        console.log("post load", data);
        expect(data).toBeTruthy(); // since it passes true to the subscription
      },
      error: fail
    });
    service.getUserDetails();
    httpTestingController.expectOne({
      'url': userDetailsUrl,
      'method': 'get'
    }).flush(data1);
  });
  // ...

However, while it does update the $loaded Observable in the call, the 'should be loaded' test fails, apparently since the Observable initially has a false value, and I can see the log statement twice in the console output:

output

Browser application bundle generation complete.
LOG: 'post load', false
Chrome 105.0.0.0 (Windows 10): Executed 1 of 2 SUCCESS (0 secs / 0.022 secs)
LOG: 'post load', true
Chrome 105.0.0.0 (Windows 10): Executed 1 of 2 SUCCESS (0 secs / 0.022 secs)
Chrome 105.0.0.0 (Windows 10) UserService should be loaded FAILED
        Error: Expected false to be truthy.
            at <Jasmine>
            at Object.next (src/app/services/user.service.spec.ts:50:22)
            at ConsumerObserver.next (node_modules/rxjs/dist/esm/internal/Subscriber.js:91:1)
            at SafeSubscriber._next (node_modules/rxjs/dist/esm/internal/Subscriber.js:60:1)
Chrome 105.0.0.0 (Windows 10): Executed 2 of 2 (1 FAILED) (0 secs / 0.032 secs)
Chrome 105.0.0.0 (Windows 10) UserService should be loaded FAILED
        Error: Expected false to be truthy.
            at <Jasmine>
            at Object.next (src/app/services/user.service.spec.ts:50:22)
            at ConsumerObserver.next (node_modules/rxjs/dist/esm/internal/Subscriber.js:91:1)

What am I missing here?

CodePudding user response:

flush() needs to be called on your mock request before the subscription happens. The next() callback is executed with the initial value of your BehaviorSubject because the Mock response hasn't come back at the point of subscription.

  • Related