Angular Testing - Workshop

Contents

What is Automated Testing

  • Unit Test- tests a specific "unit" of code, isolated from other units of code
    • Fast & reliable, easy to maintain
  • Integration Test- tests multiple units of code
    • Fast & reliable, less common generally
  • End to End Test
    • Tests a live, working system. Allows a level of verification not possible with the other levels at the expense of speed & reliability

Benefits of Testing

  • Documented Intentions - tests say what you want the code to do. They are GUARANTEED to be up to date. Other docs aren’t
  • Improved Design - more time is spent working with the code. Encourages small, decoupled designs, since they are easier to test.
  • Fewer bugs - obviously, the more you test, the fewer bugs you have
  • No regressions - after a bug is found, if a test is written, the bug can’t reappear
  • Safer refactoring - changing code is safer because tests don’t allow you to break functionality

Setup

Angular CLI

This project was generated with Angular CLI version 6.0.8.

Tools Needed

Configure Project

  • fork this repo to your GitHub account
  • clone your fork locally
  • inside angular-testing-workshop, install dependencies: npm install

Verify

  • npm start: should open your browser and display the app.
  • npm test: should yield output of test runner (no errors).

Unit Testing With Jasmine

Jasmine - I - Suites

  • A unit testing framework for JavaScript
  • Standalone, no DOM required
  • Clean syntax: describe, it, expect
  • Others: Mocha, QUnit, Jest.
                                
                                        const SuperAwesomeModule = {
                                            featureA: () => {
                                              ...
                                            },
                                            featureB: () => {
                                              ...
                                            }
                                          }
                                
                            
                                
                                        describe('SuperAwesomeModule', () => {
                                            describe('featureA', () => {
                                          
                                            });
                                          
                                            describe('featureB', () => {
                                          
                                            });
                                          });
                                
                            

Jasmine - II - Specs

  • call global Jasmine function it (string, fn)
  • a spec contains one or more expectations
  • expectation: an assertion that is either true or false.
  • spec with all true expectations: pass
  • spec with one or more false expectations: fail
                        
                                describe('SuperAwesomeModule', () => {
                                    describe('featureA', () => {
                                      it('should calculate some super awesome calculation', () => {
                                          expect(SuperAwesomeModule.featureA([1, 2, 4]).toEqual(7);
                                      });
                                  
                                      it('should also do this correctly', () => {
                                          expect(SuperAwesomeModule.featureB('...').toBe(true);
                                      });
                                    });
                                  });
                            
                        

Jasmine - III - Included Matchers

                                        
                                                expect(foo).toBe(true); // uses JS strict equality
                                                expect(foo).not.toBe(true);
                                                expect(foo).toEqual(482); // uses deep equality
                                                expect(foo).toBeDefined();
                                                expect(foo).not.toBeDefined();
                                                expect(foo).toBeUndefined();
                                                expect(foo).toBeTruthy(); // boolean cast testing
                                                expect(foo).toBeFalsy();
                                                expect(foo).toContain('student'); // find item in array
                                                expect(e).toBeLessThan(pi);
                                                expect(pi).toBeGreaterThan(e);
                                                expect(a).toBeCloseTo(b, 2); // a to be close to b by 2 decimal points
                                        
                                    
                                        
                                                expect(() => {
                                                    foo(1, '2')
                                                  }).toThrowError();
                                                  
                                                  expect(() => {
                                                    foo(1, '2')
                                                  }).toThrow(new Error('Invalid parameter type.')
                                        
                                    

Jasmine - IV - Setup and Teardown

                                            
                                                    describe('ApiService', function() {
                                                        const serviceInTest;
                                                      
                                                        beforeEach(function() {
                                                          serviceInTest = new ApiService();
                                                        });
                                                      
                                                        afterEach(function() {
                                                         ...
                                                        });
                                                      
                                                        it('retrieves data', function() {
                                                          ...
                                                        });
                                                      
                                                        it('updates data', function() {
                                                          ...
                                                        });
                                                      });
                                            
                                        

Jasmine - V - Test Doubles

Use Test Doubles to isolate dependencies
  • Mocks: objects pre-configured with details of the calls they expect
  • Spies: record information about calls
  • Stubs: provide canned answers to calls made during the test
  • Dummies: objects that are passed around but never actually used.
                                                
                                                        describe('SuperAwesomeModule', function() {
                                                            beforeEach(function() {
                                                              // track all calls to SuperAwesomeModule.asyncHelperFunction()
                                                              // and return a mock response
                                                              spyOn(SuperAwesomeModule, 'asyncHelperFunction').and.returnValue(Promise.resolve(mockData))
                                                            });
                                                          
                                                            describe('featureA', function() {
                                                              it('should ...', function() {
                                                                expect(SuperAwesomeModule.featureA(x)).toBe(y);
                                                          
                                                                // matchers for spies
                                                                expect(SuperAwesomeModule.asyncHelperFunction).toHaveBeenCalled();
                                                              });
                                                            });
                                                          });
                                                          
                                                
                                            

Jasmine - VI - Async

  • Adding a done parameter to an it clause makes a spec async
  • spec will not complete until its done is called.
  • Default timeout is 5 seconds, can override: jasmine.DEFAULT_TIMEOUT_INTERVAL
                                                    
                                                            it('should do something async', done => {
                                                                let value;
                                                                setTimeout(() => value = 42, 100);
                                                                setTimeout(() => {
                                                                  expect(value).toBe(42);
                                                                  done();
                                                                }, 200);
                                                                expect(value).toBeUndefined();
                                                              });
                                                                                                                           
                                                    
                                                

Jasmine - VII - Exercise

We will test drive the implementation of a scoreCalculator function (sums up scores) that satisfies the following:
  • should work with one number
  • should work with more than one score
  • should treat negative scores as 0
  • should return zero with empty input

Basic Components Test

Basic Components Test - I

There are 3 standard methods of testing Angular components
  • Isolated tests: we treat the component class as vanilla JS. Don't render the component.
  • Shallow tests: use the Angular testing utilities to render the component, but don't render children components.
  • Integration tests: not end-to-end tests here. In this method we render children components also.

Basic Components Test - II - Setup

We first need to import a few of the testing utilities from @angular:
                            
                                    import { async, ComponentFixture, TestBed } from '@angular/core/testing';
                                    import { MenuComponent } from './menu.component';                                                                                                 
                            
                        
We want to configure the testing module
                                    
                                            let component: MenuComponent;
                                            let fixture: ComponentFixture< MenuComponent >;
                                            
                                            beforeEach(async(() => {
                                            TestBed.configureTestingModule({
                                                declarations: [ MenuComponent ]
                                            })
                                            .compileComponents();
                                            }));
                                    
                            
  • compileComponents() will ensure that external templates and styles are inlined. This is an async operation.
                                            
                                                    beforeEach(() => {
                                                        fixture = TestBed.createComponent(MenuComponent);
                                                        component = fixture.componentInstance;
                                                        fixture.detectChanges();
                                                      });
                                                
                                                
  • fixture: A fixture for debugging and testing a component. Provides access to the component instance and also the DebugElement, a handle on the component's DOM element.
  • component : The component instance fixture.detectChanges() initializes the component (calling ngOnInit()) and runs the change detection cycle.

Basic Components Test - III - First Test

                            
                                    it('should render two menu items', () => {
                                        const menuItems = fixture.debugElement.queryAll(By.css('a'));
                                        expect(menuItems.length).toBe(2);
                                      });
                                
                                
Running this, you will get an error: Can't bind to 'routerLink' since it isn't a known property of 'a'. Since we aren't importing the module for routing , Angular doesn't recognize this directive.

Shallow test

                            
                                    import { NO_ERRORS_SCHEMA } from '@angular/core';
                                
                                
                                        
                                                schemas: [NO_ERRORS_SCHEMA]
                                            
                                            

Another test

                            
                           
                    it('should render a different Dashboard link title', () => {
                        component.dashboard = 'Spies';
                    
                        fixture.detectChanges();
                    
                        const dashboardLink = fixture.debugElement.queryAll(By.css('a'))[0];
                        expect(dashboardLink.nativeElement.textContent).toBe('Spies');
                      });
                    
                    

Angular Testing Utilities

Angular Testing Utilities - I

Angular Testing Utilities

  • async() & fakeAsync() - async Zone control
  • TestBed - a harness for compiling modules
  • inject() & TestBed.get - provides access to injectables

Component Fixture

  • componentInstance - the instance of the component created by TestBed
  • debugElement - provides insight into the component and its DOM element
  • nativeElement - the native DOM element at the root of the component
  • detectChanges() - trigger a change detection cycle for the component
  • whenStable()- returns a promise that resolves when the fixture is stable

Debug Elements

  • parent / children - the immediate parent or children of this DebugElement
  • query(predicate) - search for one descendant that matches
  • queryAll(predicate) - search for many descendants that match
  • injector - this component's injector
  • listeners- this callback handlers for this component's events and @Outputs triggerEventHandler(listener)

Testing Components with (Async) Services Dependencies

Testing Components with (Async) Services Dependencies - I

We must specify the injected services in the providers property when configuring the testing module:
                                                    
                                                            TestBed.configureTestingModule({
                                                                declarations: [HeroesComponent],
                                                                schemas: [NO_ERRORS_SCHEMA],
                                                                providers: [
                                                                  {
                                                                    provide: HeroService,
                                                                    useValue: mockHeroService,
                                                                  },
                                                                ],
                                                              }).compileComponents();
                            
                                                    
                                                                
                                                                        const mockHeroService = {
                                                                            getHeroes: () => of([]),
                                                                            addHero: () => {},
                                                                            delete: () => {},
                                                                          };
                    
                                                                    
                                                                
                                                
                                                        let component: HeroesComponent;
                                                        let fixture: ComponentFixture< HeroesComponent >;
                                                        let heroService: HeroService;                             
                                                
                                                            
                                                                    beforeEach(() => {
                                                                        fixture = TestBed.createComponent(HeroesComponent);
                                                                        heroService = fixture.debugElement.injector.get(HeroService);
                                                                        component = fixture.componentInstance;
                                                                        fixture.detectChanges();
                                                                      });
                                                                
                
                                                                
                                                            

Testing Components with (Async) Services Dependencies - II

Suppose one of your components method performs async work:
                            
                                    ngOnInit() {
                                        this.getHeroes();
                                      }
                                    
                                      getHeroes(): void {
                                        this.heroService.getHeroes().subscribe(heroes => (this.heroes = heroes));
                                      }                                           
                                
                            
You should first spy on the service mock and return a response:
                            
                                    spyOn(heroService, 'getHeroes').and.returnValue(of(mockHeroes));                              
                                
                            
Then, there are two methods of testing this:
  • use async and fixture.whenStable
  • use fakeAsync and tick

Testing Components with (Async) Services Dependencies - III - async()

  • Use the async testing utility which becomes the second argument to the it call.
  • You must then uses the fixture's whenStable method which returns a promise when all async work within this test is complete.
                            
                    it("should assign the heroes' list to the variable (using async) ", async (() => {
                        //Arreange
                        spyOn(heroService, 'getHeroes').and.returnValue(of(mockHeroes));
                  
                        //Act
                        component.getHeroes();
                  
                        //Asserts
                        fixture.whenStable().then(() => {
                          expect(component.heroes).toBe(mockHeroes);
                        });
                      }));
                      
                      

Testing Components with (Async) Services Dependencies - IV - fakeAsync

It allows you to write a test in a more linear fashion:
                            
                    it('...', fakeAsync(() => {
                      spyOn(heroService, 'getHeroes').and.returnValue(of(mockHeroes));
                    
                      component.ngOnInit();
                    
                      flush(); // "flushes" asynchronous tasks
                    
                      expect(...).toEqual(...);
                    }));
                    
                    
If you need fine time control, the tick function simulates the passage of time, and it can take in an optional argument of milliseconds.

Services

Services - I

  • When it comes to testing services in Angular, you could write isolated tests (no Angular testing utilities).
  • Shallow tests using Angular utilities like the TestBed and the inject function.
                                                        
                                                                  it('should be created', inject([HeroService], (service: HeroService) => {
                                                                    expect(service).toBeTruthy();
                                                                  }));
                                                            
                                                        
                                
                                        beforeEach(inject(
                                            [HeroService, HttpClient, MessageService],
                                            (_heroService: HeroService, 
                                            _httpService: HttpClient, 
                                            _messageService: MessageService) => {
                                              heroService = _heroService;
                                              httpService = _httpService;
                                              messageService = _messageService;
                                            },
                                          ));
                                                                        
                                                                    

Services - II

Spies and Observables
                                                            
                                                                    describe('#getHeroes',() => {
                                                                        it('should return heroes from backend getHeroes', () => {
                                                                          const heroesUrl = 'api/heroes';
                                                                          const message = 'HeroService: fetched heroes';
                                                                          spyOn(httpService, 'get').and.returnValue(of(true));
                                                                          spyOn(messageService, 'add');
                                                                    
                                                                          heroService.getHeroes().subscribe();
                                                                    
                                                                          expect(httpService.get).toHaveBeenCalledWith(heroesUrl);
                                                                          expect(messageService.add).toHaveBeenCalledWith(message);
                                                                        });
                                                                        it('should catchError when an error is provoked', () => {
                                                                          const heroesUrl = 'api/heroes';
                                                                          const message = 'HeroService: getHeroes failed: undefined';
                                                                          spyOn(httpService, 'get').and.returnValue(_throw(3));
                                                                          spyOn(messageService, 'add');
                                                                    
                                                                          heroService.getHeroes().subscribe();
                                                                    
                                                                          expect(httpService.get).toHaveBeenCalledWith(heroesUrl);
                                                                          expect(messageService.add).toHaveBeenCalledWith(message);
                                                                        });
                                                                      });
                                    
                    getHeroes(): Observable< Hero[] > {
                        return this.http.get< Hero[] >(this.heroesUrl).pipe(
                            tap(heroes => this.log('fetched heroes')),
                            catchError(this.handleError('getHeroes', [])),
                        );
                    }
                    private log(message: string) {
                        this.messageService.add(`HeroService: ${message}`);
                    }
                    private handleError< T >(operation = 'operation', result?: T){
                        return (error: any): Observable< T > => {
                            // TODO: send the error to remote logging infrastructure
                            console.error(error); // log to console instead
                    
                            // TODO: better job of transforming error for user consumption
                            this.log(`${operation} failed: ${error.message}`);
                    
                            // Let the app keep running by returning an empty result.
                            return of(result as T);
                        };
                    }
                    

Testing Directives (hackathon)

MyDirective: Hackathon

  • An attribute directive is used to modify behavior of an existing element or component.
  • Suppose we have a directive that can be added to an input element to prevent numeric input.
  • We can easily achieve this using a @HostListener and listening for the keydown event.
                                
                                        import { Directive, HostListener, ElementRef } from '@angular/core';

                                        @Directive({
                                          selector: '[myDirective]',
                                        })
                                        export class myDirective {
                                          constructor(private element: ElementRef) {}
                                        
                                          @HostListener('keydown', ['$event'])
                                          onKeydown(event) {
                                            const numberRegex = /[0-9]/;
                                        
                                            if (numberRegex.test(event.key)) {
                                              event.preventDefault();
                                            }
                                          }
                                        }
                                
                            
                                    
< input myDirective type="text" placeholder="Search..." >
                            
                        

MyDirective: Hackathon

A test host component can look like this:
                                
                        @Component({
                          template: `< input myDirective type="text"/>
                                     < textarea myDirective>`
                        })
                        class TestHostComponent {
                        }
                        
                        
When testing this, we can use the debugElement and By to query for the input. DebugElements have a useful triggerEventHandler that you can call. In this case, we would trigger the keydown event.

Tasks:

  • should allow regular text input: You should query for the input element, and trigger the keydown event handler.
    • Create a mock event, and call input.triggerEventHandler('keydown', event).
  • should not allow numeric text input for input elements: Similar setup to the first one, except the event's key property should be a string containing a number
  • should allow regular text input for textarea elements
  • should not allow numeric text input for textarea elements

Testing Pipes (hackathon)

Testing Pipes (hackathon)

  • There is really no set up as we are writing vanilla jasmine tests.
  • You should write these kind of isolated tests for both services and pipes.
                            
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
    name: 'uppercase'
})
export class UppercasePipe implements PipeTransform {

    transform(input: string): any {
    return input.toUpperCase();
    }

}
                    
                    

import { UppercasePipe } from './uppercase.pipe';

describe('UppercasePipe', () => {
  let pipe: UppercasePipe;

  beforeEach(() => {
    pipe = new UppercasePipe();
  });

  it('creates an instance', () => {
    expect(pipe).toBeTruthy();
  });

  it('transforms input string to uppercase', () => {
    expect(pipe.transform('angular rocks!')).toBe('ANGULAR ROCKS!');
  });
});

Testing Pipes (hackathon)

Task

  • Transform an input ISO date string and return a "short date" format.
  • '1960-06-01T11:01:12.720Z' ----> '06/01/1960, 11:01am'
  • Complete the following tests:
    • creates an instance
    • should not throw error
    • returned value should contain date format dd/mm/yyyy
    • returned value should contain time hh:mm[am|pm]
    • should convert ISO string to correct date format (am)
    • should convert ISO string to correct date format (pm)
  • Examples
    • '1972-08-23T15:22:34.694Z' ----> '06/01/1960, 11:01am'
    • '1980-10-04T21:35:51.869Z' ----> '10/04/1980, 09:35pm'

More more more more ...

  • Testing Components With I/O and Change Detection
  • Routed Components (hackathon)
  • Test e2e
  • Test Ngrx:
    • Actions
    • Reducers
    • Selectors
    • Effects

Thank You