Personal note

This is a redacted version of an internal document I prepared for a client. It is based off the most recent revision and is not identical to the client's version.

Angular Unit Testing Cheat Sheet

The following is a quick reference to code examples of common Angular testing scenarios and some tips to improve our testing practices. Remember to test first!

Testing Scenarios

Isolating Logic

Use helper functions to encapsulate logic from the rest of the application. Avoid placing logic within life cycle methods and other hooks. Avoid referencing the component's state from within a helper method despite it being available. This will make it easier to test in isolation.

Bad

ngOnInit () { ... this . clientPhoneNumbers = this . allClients . filter ( ( client : Client ) => client . phone !== undefined && client . phone !== null ) . map ( ( client : Client ) => ( { name : client . name , phone : client . phone } ) ); ... }

The above code example is hard to test. We have provide and/or mock every dependency of every operation within the ngOnInit method to test just three lines of code.

Better

ngOnInit () { ... this . collectClientPhoneNumbers (); ... } collectClientPhoneNumbers () { this . clientPhoneNumbers = this . allClients . filter ( ( client : Client ) => client . phone !== undefined && client . phone !== null ) . map ( ( client : Client ) => ( { name : client . name , phone : client . phone } ) ); }

In our improved example, we no longer need to ensure that all other operations in ngOnInit are successful since we are only testing the collectClientPhoneNumbers method. However, we still have to mock or provide the component's state for the allClients field.

Best

ngOnInit () { ... this . clientPhoneNumbers = this . collectClientPhoneNumbers ( this . allClients ); ... } collectClientPhoneNumbers ( clients : Client [] ): Object [] { return clients . filter ( ( client : Client ) => client . phone !== undefined && client . phone !== null ) . map ( ( client : Client ) => ( { name : client . name , phone : client . phone } ) ); }

In our best implementation, the logic is completely independent of the component's state. We don't need to mock anything if our component compiles, just provide vanilla JS input.

Test Example

it ( ' When collectClientPhoneNumbers receives a list of Clients, then it should return a list of phone numbers ' , () => { // GIVEN - Load test data and define expected results. const clients = loadFromMockData ( ' valid-clients ' ); const firstClientPhoneNumber = { name : client [ 0 ]. name , phone : client [ 0 ]. number }; const clientsWithPhoneNumbers = clients . filter ( c => client . phone !== undefined && client . phone !== null ); // WHEN - Perform the operation and capture results. const filteredClients = component . collectClientPhoneNumbers ( clients ); // THEN - Compare results with expected values. expect ( filteredClients . length ). toEqual ( clientsWithPhoneNumbers . length ); expect ( filteredClients [ 0 ] ). toEqual ( firstClientPhoneNumber ); } );

Async Behavior

The Angular Testing module provides two utilities for testing asynchronous operations.

Notes on Async Testing Tools

async : The test will wait until all asynchronous behavior has resolved before finishing. Best to test simple async behavior that shouldn't block for much time. Avoid using with async behavior that could hang or last a long time before resolving.

: The test will wait until all asynchronous behavior has resolved before finishing. Best to test simple async behavior that shouldn't block for much time. Avoid using with async behavior that could hang or last a long time before resolving. fakeAsync : The test will intercept async behavior and perform it synchronously. Best for testing chains of async behavior or unreliable async behavior that might hang or take a long time to resolve.

: The test will intercept async behavior and perform it synchronously. Best for testing chains of async behavior or unreliable async behavior that might hang or take a long time to resolve. tick : Simulate the passage of time in a fakeAsync test. Expects a numeric argument representing elapsed time in milliseconds.

: Simulate the passage of time in a fakeAsync test. Expects a numeric argument representing elapsed time in milliseconds. flushMicrotasks : Force the completion of all pending microtasks such as Promises and Observables.

: Force the completion of all pending such as Promises and Observables. flush: Force the completion of all pending macrotasks such as setInterval, setTimeout, etc. #### Code to Test

class SlowService { names : BehaviorSubject < string [] > = new BehaviorSubject <> ( [] ); getNames (): Observable < string [] > { return this . names ; } updateNames ( names : string [] ) { setTimeout ( () => this . names . next ( names ), 3000 ); } } class SlowComponent implements OnInit { names : string []; constructor ( private slowService : SlowService ) {} ngOnInit () { this . slowService . getNames (). subscribe ( ( names : string [] ) => { this . names = names ; } ); } }

Test Example async()

it ( ' When updatedNames is passed a list of names, Then the subscription will update with a list of names ' , async ( inject ( [ SlowService ], ( slowService ) => { // GIVEN - Create test data, initialize component and assert component's initial state const names = [ " Bob " , " Mark " ]; component . ngOnInit (); fixture . whenStable () . then ( () => { expect ( component . names ). toBeDefined (); expect ( component . names . length ). toEqual ( 0 ); // WHEN - Simulate an update in the service's state and wait for asynchronous operations to complete slowService . updateNames ( names ); return fixture . whenStable (); } ) . then ( () => { // THEN - Assert changes in component's state expect ( component . names . length ). toEqual ( 2 ); expect ( component . names ). toEqual ( names ); } ); } ) ) );

TestExample fakeAsync() , tick() , flush() , flushMicrotasks()

it ( ' When updatedNames is passed a list of names, Then the subscription will update with a list of names ' , fakeAsync ( inject ( [ SlowService ], ( slowService ) => { // GIVEN - Create test data, initialize component and assert component's initial state const names = [ " Bob " , " Mark " ]; component . ngOnInit (); flushMicrotasks (); expect ( component . names ). toBeDefined (); expect ( component . names . length ). toEqual ( 0 ); // WHEN - Simulate an update in the service's state and wait for asynchronous operations to complete slowService . updateNames ( names ); tick ( 3001 ); // THEN - Assert changes in component's state expect ( component . names . length ). toEqual ( 2 ); expect ( component . names ). toEqual ( names ); } ) ) );

Spies and Mocks

Spying on functions allows us to validate that interactions between components are ocurring under the right conditions. We use mock objects to reduce the amount of code that is being tested. Jasmine provides the spyOn() function which let's us manage spies and mocks.

Case 1: Assert that a method was called.

const obj = { method : () => null }; spyOn ( obj , ' method ' ); obj . method (); expect ( obj . method ). toHaveBeenCalled ();

Warning: Spying on a method will prevent the method from actually being executed.

Case 2: Assert that a method was called and execute method.

const obj = { getName : () => ' Sam ' }; spyOn ( obj , ' getName ' ). and . callThrough (); expect ( obj . getName () ). toEqual ( ' Sam ' ); expect ( obj . getName ). toHaveBeenCalled ();

Case 3: Assert that a method was called and execute a function.

const obj = { getName : () => ' Sam ' }; spyOn ( obj , ' getName ' ). and . callFake (( args ) => console . log ( args )); expect ( obj . getName () ). toEqual ( ' Sam ' ); expect ( obj . getName ). toHaveBeenCalled ();

Case 4: Mock a response for an existing method.

const obj = { mustBeTrue : () => false }; spyOn ( obj , ' mustBeTrue ' ). and . returnValue ( true ); expect ( obj . mustBeTrue () ). toBe ( true );

Case 5: Mock several responses for an existing method.

const iterator = { next : () => null }; spyOn ( iterator , ' next ' ). and . returnValues ( 1 , 2 ); expect ( iterator . next ). toEqual ( 1 ); expect ( iterator . next ). toEqual ( 2 );

Case 6: Assert that a method was called more than once.

const obj = { method : () => null }; spyOn ( obj , ' method ' ); for ( let i = 0 ; i < 3 ; i ++ { obj . method (); } expect ( obj . method ). toHaveBeenCalledTimes ( 3 );

Case 7: Assert that a method was called with arguments

const calculator = { add : ( x : number , y : number ) => x + y }; spyOn ( calculator , ' add ' ). and . callThrough (); expect ( calculator . add ( 3 , 4 ) ). toEqual ( 7 ); expect ( calculator . add ). toHaveBeenCalledWith ( 3 , 4 );

Case 8: Assert that a method was called with arguments several times

const ids = [ ' ABC123 ' , ' DEF456 ' ]; const db = { store : ( id : string ) => void }; spyOn ( db , ' store ' ); ids . forEach ( ( id : string ) => db . store ( id ) ); expect ( db . store ). toHaveBeenCalledWith ( ' ABC123 ' ); expect ( db . store ). toHaveBeenCalledWith ( ' DEF456 ' );

User Input Events

We can simulate user input without having to interact with the DOM by simulating events on the DebugElement . The DebugElement is a browser-agnostic rendering of the Angular Component as an HTMLElement . This means we can test elements without a browser to render the actual HTML.

Component to Test

@ Component ({ selector : ' simple-button ' , template : ` <div class="unnecessary-container"> <button (click)="increment()">Click Me!</button> </div> ` }) class SimpleButtonComponent { clickCounter : number = 0 ; increment () { this . clickCounter += 1 ; } }

Test Example

it ( ' When the button is clicked, then click counter should increment ' , () => { // GIVEN - Capture reference to DebugElement not NativeElement and verify initial state const buttonDE = fixture . debugElement . find ( By . css ( ' button ' ) ); expect ( component . clickCounter ). toEqual ( 0 ); // WHEN - Simulate the user input event and detect changes. buttonDE . triggerEventHandler ( ' click ' , {} ); fixture . detectChanges (); // THEN - Assert change in component's state expect ( component . clickCounter ). toEqual ( 1 ); } );

Inherited Functionality

We shouldn't test a parent class's functionality in it's inheriting children. Instead, this inherited functionality should be mocked.

Parent Class

class ResourceComponent { protected getAllResources ( resourceName ): Resource [] { return this . externalSource . get ( resourceName ); } }

Child Class

class ContactsComponent extends ResourceComponent { getAvailableContacts (): Contact [] { return this . getAllResources ( ' contacts ' ) . filter ( ( contact : Contact ) => contact . available ); } }

Test Example

it ( ' When the getAvailableContacts method is called, Then it should return contacts where available is true ' , () => { // GIVEN - Intercept call to inherited method and return a mocked response. spyOn ( component , ' getAllResources ' ). and . returnValue ( [ { id : 1 , name : ' Charles McGill ' , available : false }, { id : 2 , name : ' Tom Tso ' , available : true }, { id : 3 , name : ' Ruben Blades ' , available : true } ] ); // WHEN - Perform operation on inheriting class const contacts = component . getAvailableContacts (); // THEN - Assert that interaction between inherited and inheriting is correctly applied. expect ( component . getAllResources ). toHaveBeenCalledWith ( ' contacts ' ); expect ( contacts . length ). toEqual ( 2 ); expect ( contacts . any ( c => name === ' Charles McGill ' ) ). toBe ( false ); } );

Services

Service objects are tested with the inject() function. TestBed will inject a new instance of the service object for each test. Use the async() function when testing asynchronous behavior such as Observables and Promises. Use of() to mock observables.

Code to Test

class NameService { constructor ( private cache : CacheService ) {} getNames (): Observable < string [] > { return this . cache . get ( ' names ' ); } }

Test Example

it ( ' When getNames is called Then return an observable list of strings ' , async ( inject ( [ CacheService , NameService ], ( cache , nameService ) => { // GIVEN - Mock service dependencies with expected value const testNames = [ " Raul " , " Fareed " , " Mark " ]; spyOn ( cache , ' get ' ). and . returnValue ( of ( testNames ) ); // WHEN - Subscribe to observable returned by service method nameService . getNames (). subscribe ( ( names : string [] ) => { // THEN - Assert result matches expected value expect ( names ). toMatch ( testNames ); } ); } ) );

Input Variables

As of Angular 5, Component inputs behave just like normal properties. We can test changes using the fixture change detection.

Code to Test

class CounterComponent implements OnChanges { @ Input () value : string ; changeCounter : number = 0 ; ngOnChanges () { changeCounter ++ ; } }

Test Example

it ( ' When the value input is changed, the changeCounter incrementsByOne ' , () => { // GIVEN - Spy on the ngOnChanges lifecycle method and assert initial state. spyOn ( component , ' ngOnChanges ' ); expect ( component . value ). toBeUndefined (); expect ( component . changeCouner ). toEqual ( 0 ); // WHEN - Set the input variable and call on fixture to detect changes. component . value = ' First Value ' ; fixture . detectChanges (); // THEN - Assert that lifecycle method was called and state has been updated. expect ( component . ngOnChanges ). toHaveBeenCalled (); expect ( component . changeCounter ). toEqual ( 1 ); } );

Output Variables

Components often expose event emitters as output variables. We can spy on these emitters directly to avoid having to test asynchronous subscriptions.

Code to Test

class EmittingComponent { @ Output () valueUpdated : EventEmitter < string > = new EventEmitter <> (); updateValue ( value : string ) { this . valueUpdated . emit ( value ); } }

Test Example

it ( ' When the updateValue() method is called with a string, Then the valueUpdated output will emit with the string ' , () => { // GIVEN - Create a test argument and spy on the emitting output variable. const value = ' Test Value ' ; spyOn ( component . valueUpdated , ' emit ' ); // WHEN - Call a method that will trigger the output variable to emit. component . updateValue ( value ); // THEN - Assert that the output variable has emitted correctly with the test argument. expect ( component . valueUpdated . emit ). toHaveBeenCalledWith ( value ); } );

Application Events

Testing event fired by a global object or parent component can be done by simulating the event dispatch in a fakeAsync environment. We can use the flush() function to resolve all pending, asynchronous operations in a synchronous manner.

Code to Test

class ListeningComponent { focus : string ; @ HostListener ( ' window:focus-on-dashboard ' , [ ' $event ' ] ) onFocusOnDashboard () { this . focus = ' dashboard ' ; } }

Test Example

it ( ' When the window dispatches a focus-on-dashboard event, Then the focus is set to dashboard ' , fakeAsync ( () => { // GIVEN - Prepare spy for callback and validate initial state. spyOn ( component , ' onFocusOnDashboard ' ); expect ( component . focus ). not . toEqual ( ' dashboard ' ); // WHEN - Dispatch the event, resolve all pending, asynchronous operations and call fixture to detect changes. window . dispatchEvent ( new Event ( ' focus-on-dashboard ' ) ); flush (); fixture . detectChanges (); // THEN - Assert that callback was called and state has changed correctly. expect ( component . onFocusOnDashboard ). toHaveBeenCalled (); expect ( component . focus ). toEqual ( ' dashboard ' ); } ) );

Life Cycle Methods

There is no real reason to test a life cycle method. This would be testing the framework, which is beyond our responsability. Any logic required by a life cycle method should be encapsulated in a helper method. Test that instead. See Async Behavior for tests that require calling the ngOnInit() life cycle method.

Mock Method Chains

We may occassionally need to mock a series of method calls in the form of a method chain. This can be achieved using the spyOn function.

Code to Test

class DatabseService { db : DatabaseAdapter ; getAdultUsers (): User [] { return this . db . get ( ' users ' ). filter ( ' age > 17 ' ). sort ( ' age ' , ' DESC ' ); } }

Test Example

it ( ' When getAdultUsers is called, Then return users above 17 years of age ' , inject ([ DatabaseService ], ( databaseService ) => { // GIVEN - Mock the database adapter object and the chained methods const testUsers = [ { id : 1 , name : ' Bob Odenkirk ' }, { id : 2 , name : ' Ralph Morty ' } ]; const db = { get : () => {}, filter : () => {}, sort : () => {} }; spyOn ( db , ' get ' ). and . returnValue ( db ); spyOn ( db , ' filter ' ). and . returnValue ( db ); spyOn ( db , ' sort ' ). and . returnValue ( testUsers ); databaseService . db = db ; // WHEN - Test the method call const users = databaseService . getAdultUsers (); // THEN - Test interaction with method chain expect ( db . get ). toHaveBeenCalledWith ( ' users ' ); expect ( db . filter ). toHaveBeenCalledWith ( ' age > 17 ' ); expect ( db . sort ). toHaveBeenCalledWith ( ' age ' , ' DESC ' ); expect ( users ). toEqual ( testUsers ); } ) );

HTTP Calls

Angular provides several utilities for intercepting and mocking http calls in the test suite. We should never perform a real, http call during tests. A few important objects:

XHRBackend : Intercepts requests performed by HTTP or HTTPClient.

: Intercepts requests performed by HTTP or HTTPClient. MockBackend : Test API for configuring how XHRBackend will interact with intercepted requests.

: Test API for configuring how XHRBackend will interact with intercepted requests. MockConnection: Test API for configuring individual, intercepted requests and response.

Code to Test

class SearchService { private url : string = ' http://localhost:3000/search?query= ' ; constructor ( private http : Http ) {} search ( query : string ): Observable < string [] > { return this . http . get ( this . url + query , { withCredentials : true } ). pipe ( catchError ( ( error : any ) => { UtilService . logError ( error ); return of ( [] ); } ) ); } }

Text Example