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:
onEditSaved
method on the AutoComplete
component that is calledAutoComplete
component should emit a selectedItemChange
event with a null
valueAutoComplete
component, an itemCreated
event should be emittedLet'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.