Testing

Recap: Testing Pyramid

JavaScript Unit Testing

  • Angular is using Karma as test runner and Jasmine as testing framework
  • Jasmine is strongly oriented on BDD (Behavior Driven Development)
  • describe is used to describe a scenario
  • it is used to describe a test case
  • both can be nested arbitrarily

A first example

                    
                        describe('the pizza service', () => {
                            it('should have getPizze', () => {
                                let pizzaService = new PizzaService();

                                expect(service.getPizze).toBeDefined();
                            });
                            describe('getPizze should return salami pizza', () => {
                                it('should return some pizza', () => {
                                    let pizzaService = new PizzaService();
                                    let salami = new Pizza('Salami');

                                    let pizze: Pizza[] = service.getPizze();

                                    expect(pizze).toContain(salami);
                                });
                            });
                        });
                    
                

Jasmine vs JUnit

  • @Test => it('should ...')
  • @Ignore => xdescribe, xit
  • Execute one test => fdescribe, fit
  • Assert => expect
  • @BeforeClass => beforeAll
  • @AfterClass => afterAll
  • @Before => beforeEach
  • @After => afterEach

Matchers and Docs

  • Documentation with examples
  • Matcher Syntax: expect(object).toBe(expected) => object === expected
  • toBeDefined(), toBeTruthy(), toBeUndefined(), toEqual(object), toBeNull(), toMatch(regex)

Spys

  • spyOn(object, methodName) => Spy
  • let pizzaSpy = spyOn(pizzaService, 'getPizze');
  • pizzaSpy.and.returnValue(pizzaArr);
  • pizzaSpy.callFake(() => pizzaArr);
  • expect(pizzaSpy).toHaveBeenCalled();

Elaborate Example

  • can be found on branch Testing_Intro

Testing a simple component

  • There are multiple possibilities to run tests in Angular
  • we'll start with component tests
  • we test the logic of a component

Angular Testing Utils: TestBed

  • TestBed creates it's own Angular Module, just for this test
  • the method configureTestingModule expects a module configuration with declarations, providers etc.
  • In this test the component isn't attached to the AppModule of the application, but to the test module instead
  • At least the component and the test have to be declared

Angular Testing Utils: createComponent

  • TestBed.createComponent instantiates the component that should be tested
  • After creating the component, the TestBed configuration may not be changed anymore
  • the method returns a ComponentFixture
  • the fixture allows you to access the component instance, fixture.componentInstance
  • as well as the DebugElement: fixture.debugElement

Angular Testing Utils: DebugElement

  • Using DebugElement the DOM can be accessed
  • using debugElement.nativeElement the component's HTMLElement can be accessed
  • on a HTMLElement it's possible to retrieve its text using textContent
  • using debugElement.query(predicate) => DebugElement and queryAll(predicate) => DebugElement[] the DOM of the element can be searched for further HTMLElements.

ComponentFixture.detectChanges()

trigger change detection in a test

  • The initialization of bindings is triggered initially with fixture.detectChanges()
  • all changes to a component require a new call of fixture.detectChanges()

A first example

                    
                        describe('MyComp', () => {
                            let component: MyComp;
                            let fixture: ComponentFixture<MyComp>;
                            beforeEach(() => {
                                TestBed.configureTestingModule({
                                    declarations: [MyComp]
                                });
                                fixture = TestBed.createComponent(MyComp);
                                component = fixture.componentInstance;
                            });
                            it('should work', () => {
                                let de: DebugElement = fixture.debugElement.query(
                                    By.css('h3'));
                                let el: HTMLElement = de.nativeElement;
                                component.title = 'my title';
                                fixture.detectChanges();
                                expect(el.textContent).toEqual('my title');
                            });
                        });
                    
                

Task 12.1 - Component Testing

  • Branch: 11_Forms_3
  • Extract the h1 title into its own component
  • write a test that asserts, that the title contains the correct text

Task 12.1 - Possible Solution

  • Branch: 12_Testing_1
  • if you want to throw your local changes away and you want to see the solution: git reset --hard && git checkout 12_Testing_1

Using Test Host Components

  • if you're using @Input() and @Output() it's very easy to test the component with a dummy parent
  • create an inline component inside of the test
  • this component mounts the component that is to be tested
  • both have to be present in the declarations array
  • queries as usual via CSS
  • the dummy host component has to be created

Using Test Host Components

                    
                        describe('Header Component with a host component', () => {

                            @Component({
                                template: `<comp [prop]="'dummy value'"></comp>`
                            })
                            class HostComponent {
                            }

                            let fixture: ComponentFixture<HostComponent>;

                            beforeEach(() => {
                                TestBed.configureTestingModule({
                                    declarations: [HostComponent, MyComponent],
                                });
                                fixture = TestBed.createComponent(HostComponent);
                            });
                            // ....
                            // actual Test Code
                        });
                    
                

Task 12.2 - Host Component Testing

  • Branch: 12_Testing_1
  • Pass the title via @Input() to the header component
  • write a test that uses a dummy host component and passes a title to the header component via [propertyBinding]

Task 12.2 - Possible Solution

  • Branch: 12_Testing_2
  • if you want to throw your local changes away and you want to see the solution: git reset --hard && git checkout 12_Testing_2

Testing Pipes

  • ... is as easy as it gets ;-)
  • instantiate the pipe inside of the test with new and call the transform function
                    
                        import {KebabPipe} from "./kebab.pipe";

                        describe('KebabPipe', () => {

                            let pipe = new MyPipe();

                            it('should make camel to kebab"', () => {
                                expect(pipe.transform('aBcdE')).toBe('a-bcd-e');
                            });
                        });
                    
                

Task 12.3 - Testing Pipes

  • Branch: 12_Testing_2
  • you are guessing it ;-)
  • write a test for the phonenumber pipe
  • use the number 0564410808 once without CountryCode and once with CountryCode and assert the results with an expectation

Task 12.3 - Possible Solution

  • Branch: 12_Testing_3
  • if you want to throw your local changes away and you want to see the solution: git reset --hard && git checkout 12_Testing_3

Testing Services

  • a service without dependencies can be instantiated like a pipe and then the logic can be tested
  • especially services including HTTP calls are interesting
  • Angular offers the so called in-memory-web-api-module for this
  • Very new, hence relatively bad documentation
  • Original MockBackend method was deprecated.

Dependency Injection in Tests

  • can be done in a beforeEach block
  • using the async function
  • it expects as parameter an Array of dependencies and a function to which the instances are passed
  • They have to be registered as providers in the testing module
                    
                        beforeEach(inject([myService, MockBackend],
                            (myService, mockBackend) => {
                            service = myService;
                            backend = mockBackend;
                        }));
                    
                

Usage of the MockBackend

  • we instantiate the Http provider using the MockBackend and the BaseRequestOptions
  • with useFactory we signal, that we provide a factory method to instantiate the provider
  • Important here is the MockBackend: It allows us to check connections and reply with mocked replies
                    
                        beforeEach(() => {
                            TestBed.configureTestingModule({
                                imports: [HttpModule],
                                providers: [VisitorService, MockBackend,
                                    BaseRequestOptions,
                                    {
                                        provide: Http,
                                        useFactory: (backend, defaultOptions) =>
                                            new Http(backend, defaultOptions),
                                        deps: [MockBackend, BaseRequestOptions]
                                    }
                                ]
                            });
                        });
                    
                

HTTP Method and URL Assertion

                    
                        backend.connections.subscribe(connection => {
                            expect(connection.request.url).toBe('api/visitors');
                            expect(connection.request.method)
                                .toEqual(RequestMethod.Get);
                        });
                    
                

Defining a Mock Reply

                    
                        backend.connections.subscribe(connection => {
                            let responseOptions = new ResponseOptions(
                                {
                                    body: mockResponse
                                }
                            );
                            connection.mockRespond(new Response(responseOptions));
                        });
                    
                

calling the service and checking the result

                    
                        it('should get the Visitors as an Observable', () => {
                            service.getAllVisitors().subscribe(visitors => {
                                expect(visitors.length).toBe(1);
                                expect(visitors[0].firstName).toEqual('Petra');
                            });
                        });
                    
                

Everything in Action

  • See branch 12_Testing_4
  • in the file src/app/cinema/visitors/visitor.service.spec.ts
  • Important: Most of the testing API's have the status @Experimental
  • From my point of view, there's still much room for improvement regarding the testing API's
  • many common use cases could be covered in a simpler way

Simplification Example

                    
                        const mockedHttpProvider: FactoryProvider = {
                            provide: Http,
                            deps: [MockBackend, BaseRequestOptions],
                            useFactory: (backend: MockBackend,
                                defaultOptions: BaseRequestOptions) => {
                                return new Http(backend, defaultOptions);
                            }
                        };
                        beforeEach(() => {
                            TestBed.configureTestingModule({
                                imports: [HttpModule],
                                providers: [
                                    VisitorService,
                                    MockBackend,
                                    BaseRequestOptions,
                                    mockedHttpProvider
                                ]
                            });
                        });
                    
                

Testing complex components

  • is not part of a basic course
  • On the branch 12_Testing_5 you can find the movies.component.spec.ts test
  • the test shows different ways how to mock services as a dependency of a component
  • more info's about testing is available in the docs

You've made it!

Thank you for your participation :-)