first_page

flippant remarks about Karma-Jasmine in Angular

The details below try to explain these fundamentals:

  • use NO_ERRORS_SCHEMA
  • initialize properties used in component-binding HTML to prevent [object ErrorEvent] thrown
  • call overrideComponent to null or mock services for the component spec
  • consider using inline mocks with useValue for providers
  • consider using the “Elvis operator” in component-binding HTML
  • be ready to use HttpClientTestingModule, HttpTestingController and/or RouterTestingModule
  • comment out fixture.detectChanges() to narrow down troubleshooting binding or initialization issues

One, small introduction to Karma-Jasmine outside of Angular reminds us Karma is the “test runner” (from the Angular team) and Jasmine is a behavior driven development framework. Behavior Driven Development is beyond the scope of these remarks apart from the consideration that we might make when use the word specification instead of test.

a Jasmine *.spec.ts file is generated by the Angular CLI by default

Unless the --spec=false option is explicitly used, ng generate component [docs] will auto-generate a Jasmine spec file like this:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { MyComponent } from './my.component';

describe('MyComponent', () => {
    let component: MyComponent;
    let fixture: ComponentFixture<MyComponent>;

beforeEach(async(() => {
    TestBed.configureTestingModule({
        declarations: [ MyComponent ]
    })
    .compileComponents();
    }));

beforeEach(() => {
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    });

it('should create', () => {
    expect(component).toBeTruthy();
    });
});

One way to easily be confused by the Karma-Jasmine combo is not really, really understanding that async, ComponentFixture and TestBed do not come from Jasmine—we must depend on the Angular team to tell us about these things. One of these things is the use of async with beforeEach. A StackOverflow.com answer asserts that async (from the Angular team) replaces the use of done() which is native to Jasmine [docs]. ComponentFixture and TestBed are covered reasonably well by the Angular team [docs].

using NO_ERRORS_SCHEMA

The auto-generated test above might be for a component with an HTML file like this:

<app-my-child [prop1]="foo.bar" [prop2]="fuBar.prop3"></app-my-child>

By default, the test will likely throw a very verbose error like this:

Failed: Template parse errors:
        'app-my-child' is not a known element:
        1. If 'app-my-child' is an Angular component, then verify that it is part of this module.
        …

We need to tell our TestBed to ignore “custom” elements like app-my-child:

import { NO_ERRORS_SCHEMA } from '@angular/core';

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { MyComponent } from './my.component';

describe(MyComponent.name, () => {
    let component: MyComponent;
    let fixture: ComponentFixture<MyComponent>;

beforeEach(async(() => {
    TestBed.configureTestingModule({
        declarations: [ MyComponent ],
        schemas: [ NO_ERRORS_SCHEMA ]
    })
    .compileComponents();
    }));

beforeEach(() => {
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    });

it('should create', () => {
    expect(component).toBeTruthy();
    });
});

For more detail on NO_ERRORS_SCHEMA, see the docs. Also note that we can cut down on the use of magic strings by using MyComponent.name which is convenience from the Angular team.

the mysterious [object ErrorEvent] thrown error

The NO_ERRORS_SCHEMA remedy might be followed by another, more mysterious Jasmine error:

[object ErrorEvent] thrown

What is happening in our HTML example above is the null value of foo in the binding foo.bar and the null value of fuBar in the binding fuBar.prop3. The typescript of a component backing these bindings might look like this:

import { Component, OnInit } from '@angular/core';
import { FooService } from '../../services/foo.service';

@Component({
    selector: 'app-my-widget',
    templateUrl: './my.component.html',
    styleUrls: ['./my.component.css']
})
export class ContextPanelComponent implements OnInit {
    @Input
    fuBar: {};

constructor(public foo: FooService) { }

ngOnInit() {
    foo.loadBar();
    }
}

We see that fuBar is an @Input property and foo is injected as a service. To address these members of the component, we update our Jasmine spec:

import { NO_ERRORS_SCHEMA } from '@angular/core';

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { FooService } from '../../services/foo.service';
import { MyComponent } from './my.component';

describe(MyComponent.name, () => {
    let component: MyComponent;
    let fixture: ComponentFixture<MyComponent>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [ MyComponent ],
            schemas: [ NO_ERRORS_SCHEMA ]
        })
        .overrideComponent(MyComponent, {
            set: { providers: [ { provide: FooService, useValue: null } ] }
        })
        .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(MyComponent);
        component = fixture.componentInstance;

        component.fuBar = { prop3: null };

        fixture.detectChanges();
    });

    it('should create', () => {
        expect(component).toBeTruthy();
    });
});

This updated spec should still throw the same cryptic error, [object ErrorEvent] thrown, because, while we did address fuBar, we did not provide a value for FooService. It is often useful to provide a null value for a service to show that the service might not be necessary and should be removed from the component. Let’s flippantly provide a value for FooService:

import { NO_ERRORS_SCHEMA } from '@angular/core';

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { FooService } from '../../services/foo.service';
import { MyComponent } from './my.component';

describe(MyComponent.name, () => {
    const fooService = { bar: null };

    let component: MyComponent;
        let fixture: ComponentFixture<MyComponent>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [ MyComponent ],
            schemas: [ NO_ERRORS_SCHEMA ]
        })
        .overrideComponent(MyComponent, {
            set: { providers: [ { provide: FooService, useValue: fooService } ] }
        })
        .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(MyComponent);
        component = fixture.componentInstance;

        component.fuBar = { prop3: null };

        fixture.detectChanges();
    });

    it('should create', () => {
        expect(component).toBeTruthy();
    });
});

The spec should now be valid. Now we can actually specify an interesting behavior (write an actual test). Let’s test whether foo.loadBar() was called by spying on it. We can replace fooService with the value of createSpyObj() [docs]:

import { NO_ERRORS_SCHEMA } from '@angular/core';

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { FooService } from '../../services/foo.service';
import { MyComponent } from './my.component';

describe(MyComponent.name, () => {
    const loadBarMethodName = 'loadBar';
    const fooService = jasmine.createSpyObj(FooService.Name, [loadBarMethodName]);
    fooService.bar = null;

    let component: MyComponent;
        let fixture: ComponentFixture<MyComponent>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [ MyComponent ],
            schemas: [ NO_ERRORS_SCHEMA ]
        })
        .overrideComponent(MyComponent, {
            set: { providers: [ { provide: FooService, useValue: fooService } ] }
        })
        .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(MyComponent);
        component = fixture.componentInstance;

        fooService[loadBarMethodName].calls.reset();
        component.fuBar = { prop3: null };

        fixture.detectChanges();
    });

    it('should create', () => {
        expect(component).toBeTruthy();
    });

    it(`should call ${loadBarMethodName}`, () => {
        expect(fooService[loadBarMethodName].calls.count()).toBe(1, 'The expected number of service calls is not here.');
    });

});

In typescript, fooService will be of type any. So, when we see the calls object (or “namespace”) hanging off it, we might need to go to the Jasmine documentation to see what is going on.

use of the “Elvis operator” in component-binding HTML

Had our HTML been like this:

<app-my-child prop1]="foo?.bar" prop2="fuBar?.prop3"></app-my-child>

We could have avoided writing these lines of spec code:

fooService.bar = null;
…
component.fuBar = { prop3: null };

The use the Elvis operator has some issues with async in bindings, see “The Angry Angular AsyncPipe & The Evil Elvis Operator” by Vitaliy Isikov.

the ‘right’ way to initialize @Input values

In my example above, you see me explicitly setting the value of fuBar. To test whether @Input is working (which is kind of like testing something that belongs to the Angular team) we can set up a mock component to host MyComponent. This is detailed in “Testing Angular components with @Input()” by Aiko Klostermann of Thoughtworks.

using formal mocks

I am very, very certain that Angular team does not recommend constructing a mock inline as we have seen above:

const loadBarMethodName = 'loadBar';
const fooService = jasmine.createSpyObj(FooService.Name, [loadBarMethodName]);
fooService.bar = null;

I find this helpful when I am not repeating myself in multiple *.spec.ts files and it is less intimidating to first-time Jasmine writers.

commenting out fixture.detectChanges()

Commenting out fixture.detectChanges() in specs might prevent the spec from throwing errors. This is a surefire way to consider that any failures with this line enabled are due to binding issues.

the auto-generated tests for services look different

When a test for a service is generated (with ng generate service [docs]) the main difference is the use of inject (also from the Angular team):

it('should be created', inject([MyService], (service: MyService) => {
    expect(service).toBeTruthy();
}));

The other difference is that you cannot use overrideComponent for a service spec; set providers in configureTestingModule:

beforeEach(async(() => {
    TestBed.configureTestingModule({
        imports: [HttpClientTestingModule, RouterTestingModule],
        providers: [ { provide: FooService, useValue: null } ]
    });
}));

the NullInjectorError: No provider for HttpClient! error

When the NullInjectorError: No provider for HttpClient! error is thrown, the knee-jerk response is to add imports: [HttpClientModule] to the TestBed. The better response is to import HttpClientTestingModule (with HttpTestingController) instead. This is detailed in “Testing with the Angular HttpClient API” by Ciro Nunes.

There is a similar Karma-Jasmine error that should lead us to importing RouterTestingModule as described in “Using Jasmine framework to test Angular Router” by Burak Tasci.

https://github.com/BryanWilhite/