An Angular Testing Cheatsheet
Tags: angular, javascript, testing, tdd
Published: 2018-10-22 16:04:50

---
title: An Angular Testing Cheatsheet
published: true
description: A cheat sheet containing common test cases in Angular web-applications development.
tags: angular, javascript, testing, tdd
---

## 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](#isolating-logic)
- [Async Behavior](#async-behavior)
- [Spies and Mocks](#spies-and-mocks)
- [User Input Events](#user-input-events)
- [Inherited Functionality](#inherited-functionality)
- [Application Events](#application-events)
- [Services](#services)
- [Input Variables](#input-variables)
- [Output Variables](#output-variables)
- [Life Cycle Methods](#life-cycle-methods)
- [Mock Method Chains](#mock-method-chains)
- [HTTP Calls](#http-calls)

---

## 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
```typescript
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
```typescript
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
```typescript
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
```typescript
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.
- **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.
- **tick**: 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_.
- **flush**: Force the completion of all pending __macrotasks__ such as _setInterval_, _setTimeout_, etc.
#### Code to Test
```typescript
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()`
```typescript
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()`
```typescript
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.
```typescript
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.
```typescript
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.
```typescript
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.
```typescript
const obj = { mustBeTrue: () => false };
spyOn( obj, 'mustBeTrue' ).and.returnValue( true );
expect( obj.mustBeTrue() ).toBe( true );
```

#### Case 5: Mock several responses for an existing method.
```typescript
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.
```typescript
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
```typescript
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
```typescript
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
```typescript
@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
```typescript
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
```typescript
class ResourceComponent {

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

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

#### Test Example
```typescript
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
```typescript
class NameService {

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

#### Test Example
```typescript
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
```typescript
class CounterComponent implements OnChanges {
    
    @Input() value: string;
    changeCounter: number = 0;
    
    ngOnChanges() {
        changeCounter++;
    }
    
}
```

#### Test Example
```typescript
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
```typescript
class EmittingComponent {
    
    @Output() valueUpdated: EventEmitter<string> = new EventEmitter<>();
    
    updateValue( value: string ) {
        this.valueUpdated.emit( value );
    }
    
}
```

#### Test Example
```typescript
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
```typescript
class ListeningComponent {

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

#### Test Example
```typescript
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](#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
```typescript
class DatabseService {

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

}
```

#### Test Example
```typescript
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_.
- **MockBackend**: Test API for configuring how XHRBackend will interact with intercepted requests.
- **MockConnection**: Test API for configuring individual, intercepted requests and response.

#### Code to Test
```typescript
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
```typescript
let backend: MockBackend;
let lastConnection: MockConnection;

beforeEach( () => {

    TestBed.configureTestingModule( {
        imports: [HttpModule],
        providers: [
            { provide: XHRBackend, useClass: MockBackend },
            SearchService
        ]
    } );
    
    backend = TestBed.get(XHRBackend) as MockBackend;
    backend.connections.subscribe( ( connection: MockConnection ) => {
        lastConnection = connection;
    } );

} );

it( 'When a search request is sent, Then receive an array of string search results.', 
    fakeAsync( inject( [SearchService], ( searchService: SearchService ) => {
    
        // GIVEN - Prepare mock search results in the form of a HTTP Response
        const expectedSearchResults = [ ... ];
        const mockJSON = JSON.stringify( { data: expectedSearchResults } );
        const mockBody = new ResponseOptions( { body: mockJSON } );
        const mockResponse = new Response( mockBody );
        
        // WHEN - Perform the call and intercept the connection with a mock response.
        let receivedSearchResults: string[];   
        searchService.search( 'reso' ).subscribe( ( searchResults: string[] ) => {
            receivedSearchResults = searchResults;
        } );
        lastConnection.mockRespond( mockResponse );
        
        // THEN - Complete the pending transaction and assert that the mock response
        // was received and processed correctly.
        flushMicrotasks();
        expect( receivedSearchResults ).toBeDefined();
        expect( receivedSearchResults ).toEqual( expectedSearchResults );
    } ) )
);
```