1

So I'm currently writing a Jasmine/Karma Unit Test for an Angular 9 Component.

A short summary how my application works: I've written a little Funnel with D3 that displays given data in a funnel-like diagram. Then I've written a FunnelComponent that contains this Funnel and also displays some meta information next to the actual diagram.

This is my to be tested Component:

funnel.component.ts

import { Component } from '@angular/core';
import { Funnel } from './d3charts/funnel.ts';
import { FunnelData } from './funnel.data';

@Component({
  selector: 'v7-funnel-component',
  templateUrl: './funnel.html'
})
export class FunnelComponent {

  private funnel: Funnel = null;

  constructor() {}

  public createFunnel(funnelData: FunnelData): void {
      this.funnel = new Funnel();
      this.funnel.setData(funnelData);
      this.funnel.draw();
  }
}

And here is my karma-jasmine unit-test for that Component:

funnel.component.spec.ts

import { TestBed, ComponentFixture } from '@angular/core/testing';
import { } from 'jasmine';
import { FunnelComponent } from './funnel.component';
import { Component } from '@angular/core';
import { FunnelData } from './funnel.data';
import { Funnel } from './d3charts/funnel.ts';

@Component({selector: 'funnel', template: ''})
class FunnelStub {
  private data: FunnelData = null;

  public setData(data: FunnelData): void
  {this.data = data;}
  {}
  public draw(): void
  {}
  public update(funnelData: FunnelData): void
  {}
}

/**
 * Testing class for FunnelComponent.
 */
describe('Component: Funnel', () => {

  let component: FunnelComponent;
  let fixture: ComponentFixture<FunnelComponent>;

  beforeEach(async(() => {
    TestBed
    .configureTestingModule({
      declarations: [
        FunnelComponent,
        FunnelStub
      ],
      providers: [
        { provide: Funnel, useValue: FunnelStub}
      ]
    })
    .compileComponents()
    .then(() => {
      fixture = TestBed.createComponent(FunnelComponent);
      component = fixture.componentInstance;
    });
  }));

it('#createFunnel should set data of funnel. Filled data should set filled funnel.', () => {
    expect(component["funnel"]).toBeNull();

    let exampleFunnelData = new FunnelData("testcaption", "testdescription", 8);
    component.createFunnel(exampleFunnelData);

    expect(component["funnel"]).toBeDefined();
    expect(component["funnel"]["data"]).toBeDefined();
    expect(component.data.caption).toEqual("testcaption");
    expect(component.data.description).toEqual("testsubtext");
    expect(component.data.value).toEqual(8);
  });
});

I want to test the createFunnel method here. But I don't want that my createFunnel method assigns a real Funnel to this.funnel, but uses my FunnelStub instead. Any idea how to do this?

Adding { provide: Funnel, useValue: FunnelStub} to my providers array didn't help :(

Best regards, Sebastian

Bishares
  • 67
  • 1
  • 13

1 Answers1

2

This does not work, because Funnel is not a provider:

providers: [
  { provide: Funnel, useValue: FunnelStub}
]

What would help you the most for testing, would be to create a service for the Funnel creation.

export abstract class FunnelProvider {
  abstract createFunnel(): Funnel;
}

This service can then have 2 implementations, one implementation you use in your app and one that you use in your tests.

@Injectable()
export class DefaultFunnelProvider {
  createFunnel() {
    return new Funnel(); // Return real funnel object.
  }
}
@Injectable()
export class MockFunnelProvider {
  createFunnel() {
     // Return stub funnel. Assuming that they share the same interface.
    return new FunnelStub() as Funnel;
  }
}

Now you have to define the DefaultFunnelProvider in your FunnelModule (the module that declares your FunnelComponent):

@NgModule({
  ... // other stuff
  providers: [{provide: FunnelProvider, useClass: DefaultFunnelProvider}]
})
export class FunnelModule {}

Then you inject the FunnelProvider in your FunnelComponent and use it to create the funnel:

export class FunnelComponent {

  private funnel: Funnel = null;

  constructor(private funnelProvider: FunnelProvider) {}

  public createFunnel(funnelData: FunnelData): void {
      this.funnel = this.funnelProvider.createFunnel();
      this.funnel.setData(funnelData);
      this.funnel.draw();
  }
}

A lot of work, but now the cool thing is that you can inject the Mock implementation of the FunnelProvider in your TestBed:

TestBed
    .configureTestingModule({
      declarations: [
        FunnelComponent,
        FunnelStub
      ],
      providers: [
        { provide: FunnelProvider, useValue: MockFunnelProvider }
      ]
    })

Now when funnelProvider.createFunnel is called, it will not return a real funnel, but your funnel stub.

If you must during your test, you can cast the components funnel field to FunnelStub (e.g. if it has some special method):

const funnelStub = component.funnel as FunnelStub;
code-gorilla
  • 2,231
  • 1
  • 6
  • 21
  • 1
    Your suggested answer is probably a solution to my problem. However, I would prefer not editing my `FunnelComponent` just to make the unit test work. I was hoping there is some TestBed Karma/Jasmine-ish way to tell the TestBed that I want to use another import for a Component created with `TestBed.createComponent`. – Bishares Oct 08 '20 at 12:56
  • 2
    @Bishares : The suggested answer is not to make just the unit test work. Its about using dependency injection and delegating the task to a service so that they both (component & service) can be tested separately. This way you can isolate the dependency of component and replace with Mock. https://medium.com/@shashankvivek.7/testing-a-component-with-stub-services-and-spies-in-jasmine-1428d4242a49 – Shashank Vivek Oct 10 '20 at 18:37
  • @ShashankVivek convinced me. My Component should only contain template/gui logic, everything else can be delegated to a service. My inner funnel creation can therefore be encapsulated into a service. – Bishares Oct 12 '20 at 11:47
  • 1
    @Bishares : Glad to help you. Cheers ! – Shashank Vivek Oct 12 '20 at 11:48