Testing Angular Output without spying on the component instance

1.3k views Asked by At

Let's suppose I have the following component:

@Component({
    selector: 'app-dumb',
    template: '<button (click)="increment()">Increment</button>'
})
export class DumbComponent {
    @Output() onIncrement = new EventEmitter<void>();


    increment() {
        this.onIncrement.emit();
    }
}

I am using Angular Testing Library, and my objective is to click on the button and assert the given output function has been invoked.

The project is using karma/jasmine, and it seems that is not straightforward to add jest.

The following describes the only way I was able to check what I needed, but I'd like avoid to spy on the componentInstance and instead inject the thing I want to spy on.

it("emits an event when the increment button is clicked", async () => {
  const { fixture } = await render(DumbComponent);
  spyOn(fixture.componentInstance.onIncrement, 'emit');
  await clickIncrementButton();
  expect(fixture.componentInstance.onIncrement.emit).toHaveBeenCalledTimes(1);
})

I tried using jasmine.createSpy but It doesn't seem to be a valid type to be injected.

const onIncrement = createSpy();
await render(DumbComponent, {
    componentProperties: {
        onIncrement: onIncrement
    }
})

Any idea?

1

There are 1 answers

0
th3n3rd On BEST ANSWER

So long story short, the only two things I can inject in there are either an instance of an event emitter or a mock/spy.

The former can be implemented in the following way:

// solution 1
it("emits an event when the increment button is clicked", async () => {
    let emitted = false;
    const onIncrement = new EventEmitter<void>();
    await render(DumbComponent, {
        componentProperties: {
            onIncrement: onIncrement
        }
    });
    onIncrement.subscribe(() => emitted = true)
    await clickIncrementButton();
    expect(emitted).toBeTrue();
})

That being said, as pointed out by jonrsharpe, the output is part of the public interface of the component, so practically we can achieve the same thing by directly subscribing to the public property:

// solution 2
it("emits an event when the increment button is clicked", async () => {
    let emitted = false;
    const {fixture} = await render(DumbComponent);
    fixture.componentInstance.onIncrement.subscribe(() => emitted = true)
    await clickIncrementButton();
    expect(emitted).toBeTrue();
})

And finally we could also inject a spy (jasmine), in the following way:

// solution 3
it("emits an event when the increment button is clicked", async () => {
    const emit = createSpy()
    await render(DumbComponent, {
        componentProperties: {
            onIncrement: {
                emit: emit
            } as any
        }
    });
    await clickIncrementButton();
    expect(emit).toHaveBeenCalledTimes(1)
})

My personal preference would be real instance over mock, and specifically I'd stick probably with the first solution.

That being said, the first and second solutions are equivalent, they are both coupled with the public interface, so if the name of the property does change both of those tests would need fixing anyway.

Hope this helps!