Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 Unit Test (TestBed) service with .asObservable()

I have a component that calls a service to see if a subscription has been announced from another component.

Component:

this.activateProcessReadySubscription =  this.returnService.processReadySubscriptionAnnouced$.subscribe(
            itemsInCart => {
                this.itemsInCart = itemsInCart;
            });

When I try to test this I get an error:

TypeError: Cannot read property 'subscribe' of undefined

SPEC

it('should call constructor', fakeAsync(() => {
        mockReturnsService.setResponse(0, true);
        tick();
        fixture.detectChanges();
        expect(mockReturnsService.processReadySubscriptionAnnouced$Spy).toHaveBeenCalledTimes(1);
    }));

Service:

    private activateProcessReadySubscriptionSource = new Subject<number>();
    processReadySubscriptionAnnouced$ = this.activateProcessReadySubscriptionSource.asObservable();

    announceProcessReady(itemsInCart: number) {
        this.activateProcessReadySubscriptionSource.next(this.returnCartDataLength);
    }

I can't seem to figure out how to get the subscription to test properly.

like image 688
K Thomas Avatar asked Jun 23 '17 19:06

K Thomas


1 Answers

(This is very basic stuff in the end, but it took me days to figure it out... Hope to help someone out there to save some time :)... )

I had the same problem and the only way I could fix it, was using a getter, in order to be able to return a mocked value while testing...

So in your Service, you would have to change the property to a getter:

private activateProcessReadySubscriptionSource = new Subject<number>();

processReadySubscriptionAnnouced$ () { return this.activateProcessReadySubscriptionSource.asObservable();}

After that, you would have to modify the way you access that property in order to (now) execute it on your component.

And now you can have access to the observable subscribe function on the .spec.ts file...

I'll tell you my similar history in code now:

I had:

/* * * * MyData.service.ts * * * */
//   TYPING ASUMPTIONS...
//   - DI = [Your defined interface]
//   - BS = BehaviorSubject 
@Injectable()
export class MyDataService {
  private searchToggleSource: BS<DI> = new BS<DI>({ search: false });
  public currentToggleSearchStatus: Observable<DI> = this.searchToggleSource.asObservable();
}


/* * * * My.component.ts * * * */ 
@Component({
  selector: 'my-component',
  template: './my.component.html',
  styleUrls: ['./my.component.css'],
})
export class MyComponent implements OnInit, OnDestroy {
  private searchToggleSubscription: Subscription;
  public search: boolean;

  // DataService being the injected, imported service 
  constructor(dataService: DataService){ }

  ngOnInit(){
    this.searchToggleSubscription = this.dataService.currentToggleSearchStatus
    .subscribe(
      ({ search }) => {
        this.search = search;
      });
  }

  ngOnDestroy(){
    this.searchToggleSubscription.unsubscribe();
  }
}

/* * * * My.component.spec.ts * * * */ 
// ASUMPTIONS
// - USING 'jest'
describe('MyComponent', () => {
  let fixture: ComponentFixture<MyComponent>;
  let mockDataService;

  beforeEach(() => {
    mockDataService = createSpyObj('DataService', ['currentToggleSearchStatus', ]);

    TestBed.configureTestingModule({
      declarations: [
        MyComponent,
      ],
      providers: [
        { provide: DataService, useValue: mockDataService },
      ]
    });
    fixture = TestBed.createComponent(MyComponent);
  });

  it('should get state from DataService when ngOnInit', () => {
    mockDataService
    .currentToggleSearchStatus
    .mockReturnValue(of({search: true}));

    //... to call ngOnInit()

    // ****************** THE ERROR **************************
    // **** subscribe is not a function...
    // **** Since I have no access to a real Observable from
    // **** a fake DataService property...

    fixture.detectChanges();

    // *** SERIOUSLY I SPEND ALMOST 3 DAYS RESEARCHING AND PLAYING
    // *** WITH THE CODE AND WAS NOT ABLE TO FIND/CREATE A SOLUTION...
    // *** 'TILL LIGHT CAME IN...
    // *******************************************************
    expect(fixture.componentInstance.search).toBe(false)
  });
});

The solution... use a getter... I'll use comments '-' to show 'the fix'...

/* * * * MyData.service.ts * * * */
//   TYPING ASUMPTIONS...
//   - DI = [Your defined interface]
//   - BS = BehaviorSubject 
@Injectable()
export class MyDataService {
  private searchToggleSource: BS<DI> = new BS<DI>({ search: false });
  //------- CHANGED ---
  // public currentToggleSearchStatus: Observable<DI> = this.searchToggleSource.asObservable();

  //------- A GETTER ------ (MUST RETURN THE OBSERVABLE SUBJECT)
  public currentToggleSearchStatus(){
    return this.searchToggleSource.asObservable();
  }
}


/* * * * My.component.ts * * * */ 
@Component({
  selector: 'my-component',
  template: './my.component.html',
  styleUrls: ['./my.component.css'],
})
export class MyComponent implements OnInit, OnDestroy {
  private searchToggleSubscription: Subscription;
  public search: boolean;

  // DataService being the injected, imported service 
  constructor(dataService: DataService){ }

  ngOnInit(){
    //------------ CHANGED  -------
    //this.searchToggleSubscription = this.dataService.currentToggleSearchStatus
    //.subscribe(
    //  ({ search }) => {
    //    this.search = search;
    //  });

    //------------ EXECUTE THE SERVICE GETTER -------
    this.searchToggleSubscription = this.dataService.currentToggleSearchStatus()
    .subscribe(
      ({search}) => {
        this.search = search;
      }
    );
  }

  ngOnDestroy(){
    this.searchToggleSubscription.unsubscribe();
  }
}

/* * * * My.component.spec.ts * * * */ 
// ASUMPTIONS
// - USING 'jest'
describe('MyComponent', () => {
  let fixture: ComponentFixture<MyComponent>;
  let mockDataService;

  beforeEach(() => {
    mockDataService = createSpyObj('DataSharingSearchService', ['showHideSearchBar', 'currentToggleSearchStatus', ]);

    TestBed.configureTestingModule({
      declarations: [
        MyComponent,
      ],
      providers: [
        { provide: DataService, useValue: mockDataService },
      ]
    });
    fixture = TestBed.createComponent(MyComponent);
  });

  it('should get state from DataService when ngOnInit', () => {
    mockDataService
    .currentToggleSearchStatus
    .mockReturnValue(of({search: true}));

    //... to call ngOnInit()
    // ------------- NO ERROR :3!!!  -------------------
    fixture.detectChanges();
    expect(fixture.componentInstance.search).toBe(false)
  });
});

*** NOTES: jest API is pretty similar to jasmine...

// jest:                    jasmine:
//  createSpyObj       <=>  jasmine.createSpyObj
// .mockReturnValue()  <=>  .and.returnValue()

Don't forget to import only the 'of' function, to return observable objects in your mocked services...

import { of } from "rxjs/observable/of";
like image 187
Jesus D Leon Avatar answered Oct 29 '22 19:10

Jesus D Leon