Spying on component outputs

A common practice in testing is to use spy function calls during the execution of tests and then evaluate these calls, checking whether all functions have been called correctly.

Jasmine provides us with some nice helpers in order to use spy function calls. We can use the spyOn function of Jasmine in order to replace the original function with a spy function. The spy function will record any calls, and we can later on evaluate how many times it was called and with what parameters.

Let's look at a simple example of how to use the spyOn function:

class Calculator {
  multiply(a, b) {
    return a * b;
  }
  
  pythagorean(a, b) {
    return Math.sqrt(this.multiply(a, a) + this.multiply(b, b));
  }
}

We will test a simple Calculator class that has two methods. The multiply method simply multiplies two numbers and returns the result. The pythagorean method calculates the hypotenuse of a right-angled triangle with two sides, a and b.

You might remember the formula for the Pythagorean theorem from your early school days:

a² + b² = c²

We will use this formula to produce c from a and b by getting the square root of the result of a*a + b*b. For the multiplications, we'll use our multiply method instead of using arithmetic operators directly.

Now, we'd want to test our calculator pythagorean method, and as it uses the multiply method to multiply a and b, we can spy on this method to verify our test result in depth:

describe('Calculator pythagorean function', () => {
  it('should call multiply function correctly', () => {
    // Given
    const calc = new Calculator();
    spyOn(calc, 'multiply').and.callThrough();
    // When
    const result = calc.pythagorean(6, 8);
    // Then
    expect(result).toBe(10);
    expect(calc.mul).toHaveBeenCalled();
    expect(calc.mul.calls.count()).toBe(2);
    expect(calc.mul.calls.argsFor(0)).toEqual([6, 6]);
    expect(calc.mul.calls.argsFor(1)).toEqual([8, 8]);
  });
});

The spyOn function of Jasmine takes an object as first parameter and the function name on the object which we'd like to spy on.

This will effectively replace the original multiply function on our class instance with a new spy function of Jasmine. By default, spy functions will only record function calls, and they won't delegate the call further to the original function. We can use the .and.callThrough() function to specify that we'd like Jasmine to call the original function. This way our spy function will act as a proxy and record any calls at the same time.

In the Then section of our test, we can then inspect the spy function. Using the toHaveBeenCalled matcher, we can check whether the spy function was called after all.

Using the calls property of the spy function, we can inspect in more detail and verify the call count as well as the arguments that individual calls received.

Using the knowledge that we gained about Jasmine spies, we can now apply that to our component tests. As we know that all output properties of components contain an EventEmitter, we can actually spy on them to check whether our component sends output.

Inside components, we call the next method on EventEmitter in order to send output to parent component bindings. As this is an asynchronous operation and we'd also like to test our components without needing to involve parent components, we can simply spy on the next method of our output properties.

In the next two tests for our AutoComplete component, we'd like to verify the functionality when we save an edit in the Editor child component. Let's quickly recap on this behavior:

  • On saved edits, we get the onEditSaved method on the AutoComplete component that is called
  • If the saved value is an empty string, the AutoComplete component should emit a selectedItemChange event with a null value
  • If the saved value is no empty string and the value is not present in the items of the AutoComplete component, an itemCreated event should be emitted

Let's create the tests for the previous expected behavior to the already existing lib/ui/auto-complete/auto-complete.spec.js test file:

  ...
  it('should emit selectedItemChange event with null on empty content being saved', () => {
    // Given
    const autoComplete = new AutoComplete();
    autoComplete.items = ['one', 'two', 'three'];
    autoComplete.selectedItem = 'three';
    spyOn(autoComplete.selectedItemChange, 'next');
    spyOn(autoComplete.itemCreated, 'next');

    // When
    autoComplete.onEditSaved('');

    // Then
    expect(autoComplete.selectedItemChange.next).toHaveBeenCalledWith(null);
    expect(autoComplete.itemCreated.next).not.toHaveBeenCalled();
  });

We create two Jasmine spies here. The first one spies on the selectedItemChange output property, while the second one spies on the itemCreated output property.

After simulation, the editor was saved with an empty string. We can start verifying our spies in the Then section of our test.

The next function of the selectedItemChange event, EventEmitter, should have been called with a null value, while next of itemCreated shouldn't have been called at all. We can use the not property on the returned expectation object to invert the matcher.

Let's add a second test for the behavior when an editor was saved with a value that does not yet exist in the AutoComplete component:

  it('should emit an itemCreated event on content being saved which does not match an existing item', () => {
    // Given
    const autoComplete = new AutoComplete();
    autoComplete.items = ['one', 'two', 'three'];
    autoComplete.selectedItem = 'three';
    spyOn(autoComplete.selectedItemChange, 'next');
    spyOn(autoComplete.itemCreated, 'next');

    // When
    autoComplete.onEditSaved('four');

    // Then
    expect(autoComplete.selectedItemChange.next).not.toHaveBeenCalled();
    expect(autoComplete.itemCreated.next).toHaveBeenCalledWith('four');
  });

This time, we simulate a saved edit with a value, which isn't an empty string and does not exist in the autocomplete items already.

In the Then section of our code, we evaluate the spies and expect that the itemCreated.next function was called with a four string.

Using Jasmine spies, we managed to test our component output successfully without the need to bootstrap Angular. We performed these tests solely on the component class and by creating spies on the EventEmitter that is present on all output properties.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset