0

I'm trying to do component integration testing (i.e. including the Angular lifecycle events), but I'm having a hard time mocking a service with Jasmine, one that exposes a Subscribable.

To reproduce my scenario:

  1. ng new ng-comp-tests
  2. ng generate component my-screen
  3. ng generate service crud
  4. ng generate class item

Update the class to this:

export class Item {
  constructor(public name: string) { }
}

Then make the service look like this:

export class CrudService {
  private thing = new Subject<Item>();

  constructor() {
    setInterval(() => { this.thing.next(new Item('Thing ' + Math.random())); }, 1000);
  }

  getThingObservable() {
    return this.thing.asObservable();
  }
}

Change the component to be like this (and include it as the main thing in the app component):

<p *ngIf="!!thing">{{thing.name}}</p>
export class MyScreenComponent implements OnInit, OnDestroy {
  public thing: Item;
  private thingSubscription: Subscription;

  constructor(private service: CrudService) { }

  ngOnInit() {
    this.thingSubscription = this.service
      .getThingObservable()
      .subscribe(t => { this.thing = t; });
  }

  ngOnDestroy(): void {
    this.thingSubscription.unsubscribe();
  }
}

And finally, change the spec to be this:

describe('MyScreenComponent', () => {
  let component: MyScreenComponent;
  let fixture: ComponentFixture<MyScreenComponent>;

  beforeEach(async(() => {
    const serviceSpy = jasmine.createSpyObj('CrudService', {
      getThingObservable: () => new Subject<Item>().asObservable()
    });

    TestBed.configureTestingModule({
      declarations: [ MyScreenComponent ],
      providers: [ { provide: CrudService, useValue: serviceSpy } ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(MyScreenComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

This will give an exception for the test:

MyScreenComponent should create
[object ErrorEvent] thrown

The exception isn't really helpful, and the console from Chrome isn't either, because it shows this abbreviated log:

  • "Failed to load ng:///DynamicTestModule/...ngfactory.js: Cross origin requests..."
  • "Failed to load ng:///DynamicTestModule/...ngfactory.js: Cross origin requests..."
  • "Uncaught DOMException: Failed to execute 'send'...."
  • "Error during cleanup of component ... Cannot read property 'unsubscribe' of undefined..."

The top answer for a related question suggests I forgot to do fixture.detectChanges() (which prevents ngOnInit from firing), but as you can see I do have that in my code.

There must be something else wrong here, but what?

Jeroen
  • 60,696
  • 40
  • 206
  • 339

1 Answers1

0

The answer lies in the jasmine.createSpyObj(...) documentation, stating about the second parameter (emphasis mine):

Array of method names to create spies for, or Object whose keys will be method names and values the returnValue.

In your code you're providing a fake implementation for getThingObservable like this:

getThingObservable: () => new Subject<Item>().asObservable()

With that, the getThingObservable spy will return a lambda when the component calls it, which has no subscribe that the component tries to do, so the ngOnInit fails altogether. The ngOnDestroy only fails as a secondary effect.

You should instead directly pass the returnValue, like this:

getThingObservable: new Subject<Item>().asObservable()

Then, when the component calls getThingObservable, it wil get an actual observable, and things will be fine.

PS. You could also simplify to using { of } from 'rxjs/observable/of':

getThingObservable: of<Item>()

Finally, you could also resort to marble tests for more control over observables in tests.

Jeroen
  • 60,696
  • 40
  • 206
  • 339