DEV Community

Preston Lamb
Preston Lamb

Posted on • Originally published at prestonlamb.com on

Unit Testing in Angular

A few months ago I wrote an intro to unit testing in Angular article. In it, I promised that I'd be writing more unit tests and would follow up with another article and more details after I had some more experience. So, here I am! It's a few months later and I've written many, many tests for an internal Angular library for work. The library has a mix of services, pipes, guards, components, and directives. There are isolated tests, shallow tests, and deep tests. I've learned a lot, and had to ask a lot of questions about unit testing on Twitter and StackOverflow to figure everything out. Luckily for me, I received a lot of help from the community, and I'm really grateful for everyone that helped! Now, hopefully, this will help someone else out as well. We'll go over testing several pieces of Angular. Let's go!

Testing Utilities

It's not uncommon in an Angular app to have utility files full of functions to manipulate data throughout our applications. It's important to test these utilities to make sure that they are predictable and give us the correct output each time they're used. Even those these functions aren't exactly Angular, we can still test them in the same way we would any other Angular piece. Let's use the following function in our array.util.ts file:

export function stringArrayContainsPartialStringMatch(arr: string[], strMatch: string) {
    return arr.filter(item => item.includes(strMatch)).length > 0;
}
Enter fullscreen mode Exit fullscreen mode

The function takes in a string array and a string to match. It returns true if any items in the array contain any portion of the strMatch variable. It's important to note that the array doesn't need to contain the entire string. Here are some tests for this utility function:

it('should return true if the value is included in the string array', () => {
    const arr = ['value 1'];
    const str = 'value 1';
    const includedInArray = stringArrayContainsPartialStringMatch(arr, str);

    expect(includedInArray).toBe(true);
});

it('should return true if the value is included in the string array', () => {
    const arr = ['value 1'];
    const str = 'val';
    const includedInArray = stringArrayContainsPartialStringMatch(arr, str);

    expect(includedInArray).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

We can also do the inverse of each of those tests to make sure the function returns false when the value is not included in the array. For consistency's sake, these tests can be placed in a *.spec.ts file, just like the Angular CLI creates for directives, pipes, etc.

Testing Pipes

Testing Angular pipes may be one of the better places to start, because pipes have fairly straightforward inputs and outputs. Let's take the following pipe for an example:

export class StatusDisplayPipe implements PipeTransform {
    transform(id: number): string {
        if (typeof id !== 'number') {
            throw new Error('The ID passed in needs to be a number.');
        }

        switch (id) {
            case 2:
                return 'Sent';
            case 3:
                return 'Delivered';
            default:
                return 'Pending';
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The pipe takes in a status ID for an item and returns whether the item's status is Pending, Sent, or Delivered. Testing this will not be too involved; it will be an isolated test and we can test it like this:

describe('StatusDisplayPipe', () => {
    it('should return Sent for an ID of 2', () => {
        const pipe = new StatusDisplayPipe();
        const statusDisplay = pipe.transform(2);

        expect(statusDisplay).toBe('Sent');
    });
});
Enter fullscreen mode Exit fullscreen mode

Let's break it down line by line. We start by creating an instance of the pipe, by using the new keyword. Next, we call the transform method on the pipe and pass it the ID we want to test. Then, we use the expect statement to check if the return value from the pipe is what we expect it to be.

We can continue on this pipe by checking the other conditions, like an ID of 1, or what happens when we don't pass in a number to the pipe. Now, due to using TypeScript and because we typed the input to the transform method, we will likely see an error in our IDE if we didn't pass in a number for the ID, but it's still worth testing it in my opinion.

That's pretty much all it takes to test a pipe! Not too bad, right?

Testing Services

Testing services, in many cases, will be very similar to pipes. We'll be able to test them in isolation by creating an instance of the service and then calling methods on it. It may be more complicated than a pipe, because it may include a Subject, for example, but it's still fairly straightforward. Some services do have dependencies and require us to pass those dependencies in when creating the instance of the service. We'll look at a service like that, as it's more complicated than one that doesn't have any dependencies.

It's important to remember though that we don't want to test those dependencies; we are assuming they're being tested elsewhere. That's where using Jasmine to mock these services will come in handy. Take the following service as an example:

export class ConfigurationService {
    constructor(private _http: HttpClient) {}

    loadConfiguration() {
        return this._http
            .get('https://my-test-config-url.com')
            .toPromise()
            .then((configData: any) => {
                this.configData = configData;
            })
            .catch((err: any) => {
                this.internalConfigData = null;
            });
    }
}
Enter fullscreen mode Exit fullscreen mode

This function is one that is used to load configuration for the Angular application before the application bootstraps using the APP_INITIALIZER token. That's why there's the .toPromise() method on the Observable.

So, how do we test this? We'll start by creating a mock HttpClient application and then create an instance of the service. We can use the mocked HttpClient app to return the data we want it to or to throw an error or whatever we may need to test. The other thing to keep in mind is that many functions in a service are asynchronous. Because of that, we need to use the fakeAsync and tick methods from @angular/core/testing or just the async method from the same library. Let's look at some examples:

describe('ConfigurationService', () => {
    const mockConfigObject = { apiUrl: 'https://apiurl.com' };
    let mockHttpService;
    let configurationService;

    beforeEach(() => {
        mockHttpService = jasmine.createSpyObj(['get']);
        configurationService = new ConfigurationService(mockHttpService);
    });

    it('should load a configuration object when the loadConfiguration method is called', () => {
        mockHttpService.get.and.returnValue(of(mockConfigObject));
        configurationService.loadConfiguration();
        tick();

        expect(Object.keys(configurationService.configData).length).toBe(Object.keys(mockConfigObject).length);
    });

    it('should handle the error when the loadConfiguration method is called and an error occurs', () => {
        mockHttpService.get.and.returnValue(throwError(new Error('test error')));
        configurationService.loadConfiguration();
        tick();

        expect(configurationService.configData).toBe(null);
    });
});
Enter fullscreen mode Exit fullscreen mode

Let's break these tests down a little. At the top of the describe, we set some "global" variables for the service. The mockHttpService and the configurationService are initialized in the beforeEach method. We use jasmine.createSpyObj to create a mock HttpClient instance. We tell it that we are going to mock the get method. If we needed other functions from HttpClient, we would add them to that array.

In each of the two unit tests, we tell the mockHttpService what to return when the get method is called. In the first one, we tell it to return our mockConfigObject as an Observable. In the second, we use throwError from RxJS. Again in both, we call the loadConfiguration method. We then do a check to see if the internal configData variable for the service is set to what we expect it to be.

Now, a real service for our app likely does many other things, like having a method to return that configData object, or an attribute on the object, or any number of other functions. All of them can be tested in the same way as the above functions. If the service requires more dependencies, you can create each of them just like we created the HttpClient dependency.

I learned something else while writing the tests for a service in the library, and it came from Joe Eames. By default, the CLI creates the *.spec.ts file with the TestBed imported and set up. But many times you don't need that. As Joe put it, all these things in Angular are just classes and you can create instances of them. Many times that is sufficient and more simple than using the TestBed. What I've learned is that you'll know when you need the TestBed when you need it; until then just do what we've done here.

Testing Directives

The next Angular element we're going to go test is a directive. In this example, the test does get more complicated here. But don't worry, it's only overwhelming at first. I had someone demonstrate this to me and then I was able to use that example on a couple other directives and components. Hopefully this can be that example for you going forward.

The directive we're going to use here turns a text input into a typeahead input, outputting the new value after a specified debounce time. Here's that directive:

export class TypeaheadInputDirective implements AfterContentInit {
    @Input() debounceTime: number = 300;
    @Output() valueChanged: EventEmitter<string> = new EventEmitter<string>();

    constructor(private searchInput: ElementRef) {}

    ngAfterContentInit() {
        this.setupTypeaheadObservable();
    }

    setUpTypeaheadObservable() {
        fromEvent(this.searchInput.nativeElement, 'keyup')
            .pipe(
                debounceTime(this.debounceTime),
                distinctUntilChanged(),
                tap(() => this.valueChanged.emit(this.searchInput.nativeElement.value)),
            )
            .subscribe();
    }
}
Enter fullscreen mode Exit fullscreen mode

The goal in testing this directive is that when something is typed into the input element, the value is emitted. So let's take a look at what the test looks like. This one will be different; to test that typing in the input emits a value means creating a TestHostComponent which has the input element and the directive. We'll create a typing event, and then check that the value is output.

@Component({
    selector: 'app-test-host',
    template: `
        <input typeaheadInput [debounceTime]="debounceTime" (valueChanged)="valueChanged($event)" type="text" />
    `,
})
class TestHostComponent {
    @ViewChild(TypeaheadInputDirective) typeaheadInputDirective: TypeaheadInputDirective;
    public debounceTime: number = 300;
    valueChanged(newValue: string) {}
}
Enter fullscreen mode Exit fullscreen mode

This is just the TestHostComponent. We have access to the directive via the @ViewChild decorator. Then we use the debounceTime input to control that in case we want to test what happens when we change that. Lastly we have a valueChanged function that will handle the output from the directive. We will use spy on that function for our test. Now for an actual test of the directive:

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

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

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

    it('should emit value after keyup and debounce time', fakeAsync(() => {
        spyOn(component, 'valueChanged');

        const input = fixture.debugElement.query(By.css('input'));
        input.nativeElement.value = 'Q';
        input.nativeElement.dispatchEvent(
            new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: 'Q', shiftKey: true }),
        );

        tick(component.debounceTime);

        expect(component.valueChanged).toHaveBeenCalledWith('Q');
    }));
});
Enter fullscreen mode Exit fullscreen mode

In the two beforeEach functions, we use the TestBed to create the testing fixture and get access to the component. Then, in the test, we add the spyOn on our valueChanged function. We then find the input element and set the value to 'Q', and then dispatch the KeyboardEvent. We use tick to wait for the debounceTime to pass, and then we check that the valueChanged function has called with the string Q.

As I said before, testing this directive was more involved than the other tests. But it's not too bad once we learn what's going on. We can use this same methodology on many other tests for more complicated components and directives. But remember: we should shoot for the most simple tests we can write to start. It will make it easier to maintain and write the tests and more likely for us to continue writing them.

Testing Components

The next Angular item we'll test is a component. This is going to be very similar to the directive we just tested. But, even though it'll look almost the exact same, I think it'll be worth going through the exercise of testing the component.

This component's purpose is to display a list of alerts that we want to show to our users. There is a related service that adds and removes the alerts and passes them along using a Subject. It is slightly complicated because we're going to use a TemplateRef to pass in the template that the ngFor loop should use for the alerts. That way the implementing application can determine what the alerts should look like. Here's the component:

@Component({
    selector: 'alerts-display',
    template: '<ng-template ngFor let-alert [ngForOf]="alerts$ | async" [ngForTemplate]="alertTemplate"></ng-template>',
    styleUrls: ['./alerts-display.component.scss'],
})
export class AlertsDisplayComponent implements OnInit {
    public alerts$: Subject<Alert[]>;
    @ContentChild(TemplateRef)
    alertTemplate: TemplateRef<NgForOfContext<Alert>>;

    constructor(private _alertToaster: AlertToasterService) {}

    ngOnInit() {
        this.alerts$ = this._alertToaster.alerts$;
    }
}
Enter fullscreen mode Exit fullscreen mode

That's all the component consists of. What we want to test is that when the Subject emits a new value, the template updates and shows that many items. We'll be able to simulate all this in our test. Let's look at our TestHostComponent again in this test:

@Component({
    selector: 'app-test-host',
    template: `
        <alerts-display>
            <ng-template let-alert>
                <p>{{ alert.message }}</p>
            </ng-template>
        </alerts-display>
    `,
})
class TestHostComponent {
    @ViewChild(AlertsDisplayComponent) alertsDisplayComponent: AlertsDisplayComponent;
}
Enter fullscreen mode Exit fullscreen mode

In this TestHostComponent, we put the <alerts-display> component in the template, and provide the template for the ngFor loop. Now let's look at the test itself:

describe('AlertsDisplayComponent', () => {
    let component: TestHostComponent;
    let fixture: ComponentFixture<TestHostComponent>;
    let mockAlertsToasterService: AlertToasterService;

    beforeEach(async(() => {
        mockAlertsToasterService = jasmine.createSpyObj(['toString']);
        mockAlertsToasterService.alerts$ = new Subject<Alert[]>();
        TestBed.configureTestingModule({
            declarations: [AlertsDisplayComponent, TestHostComponent],
            providers: [{ provide: AlertToasterService, useValue: mockAlertsToasterService }],
        }).compileComponents();
    }));

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

    it('should show an element for each item in the array list', fakeAsync(() => {
        mockAlertsToasterService.alerts$.next([{ message: 'test message', level: 'success' }]);

        tick();
        fixture.detectChanges();

        const pTags = fixture.debugElement.queryAll(By.css('p'));

        expect(pTags.length).toBe(1);
    }));
});
Enter fullscreen mode Exit fullscreen mode

Let's break down what we've got here. We're going to mock the AlertToasterService and get access to the fixture and component in the beforeEach functions. Then in the test we emit a new array of alerts. This is what will happen in the service after the addAlert function is called. Then all the places where the Subject is subscribed to will get the new list and output the results. We throw in a tick to make sure that any necessary time has passed, and then (and this is important) we tell the fixture to detectChanges. It took me a while to remember that part, but if you forget it then the template won't update. After that, we can query the fixture to find all the p tags. Now, because we emitted an array with only one alert item, we will expect there to only be one p tag visible.

Again, this is a little more complicated than some components may be. Maybe on some components we don't want to test what the output in the template will be. We just want to test some functions on the component. In those cases, just create the component like this:

const component = new MyComponent();
Enter fullscreen mode Exit fullscreen mode

We can still mock services if needed, and pass them in to the constructor, but that should be our goal whenever possible. But don't be afraid when your test requires a more complicated test setup. It looks scary at first but after doing it a couple of times you'll get the hang of it.

Testing Guards

I debated whether or not I should include this section, because guards are essentially specialized services, but figured if I was going to spend all this time mapping out how to test all these different parts of our Angular app I might as well include this one specifically. So let's take a look at a guard. Here it is:

@Injectable()
export class AuthenticationGuard implements CanActivate {
    constructor(private _authenticationService: AuthenticationService) {}

    canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
        const preventIfAuthorized: boolean = next.data['preventIfAuthorized'] as boolean;
        const redirectUrl: string = state.url && state.url !== '/' ? state.url : null;
        return preventIfAuthorized
            ? this._authenticationService.allowIfAuthorized(redirectUrl)
            : this._authenticationService.allowIfNotAuthorized(redirectUrl);
    }
}
Enter fullscreen mode Exit fullscreen mode

There's only one function we're going to test here: the canActivate function. We'll need to mock theAuthenticationService] and the ActivatedRouteSnapshot and RouterStateSnapshots for these tests. Let's take a look at the tests for this guard:

describe('AuthenticationGuard', () => {
    let authenticationGuard: AuthenticationGuard;
    let mockAuthenticationService;
    let mockNext: Partial<ActivatedRouteSnapshot> = {
        data: {
            preventIfAuthorized: true,
        },
    };
    let mockState: Partial<RouterStateSnapshot> = {
        url: '/home',
    };

    beforeEach(() => {
        mockAuthenticationService = jasmine.createSpyObj(['allowIfAuthorized', 'allowIfNotAuthorized']);
        authenticationGuard = new AuthenticationGuard(mockAuthenticationService);
    });

    describe('Prevent Authorized Users To Routes', () => {
        beforeEach(() => {
            mockNext.data.preventIfAuthorized = true;
        });

        it('should return true to allow an authorized person to the route', async(() => {
            mockAuthenticationService.allowIfAuthorized.and.returnValue(of(true));
            authenticationGuard
                .canActivate(<ActivatedRouteSnapshot>mockNext, <RouterStateSnapshot>mockState)
                .subscribe((allow: boolean) => {
                    expect(allow).toBe(true);
                });
        }));

        it('should return false to not allow an authorized person to the route', async(() => {
            mockAuthenticationService.allowIfAuthorized.and.returnValue(of(false));
            authenticationGuard
                .canActivate(<ActivatedRouteSnapshot>mockNext, <RouterStateSnapshot>mockState)
                .subscribe((allow: boolean) => {
                    expect(allow).toBe(false);
                });
        }));
    });
}
Enter fullscreen mode Exit fullscreen mode

To begin with we have some mock data that we will use, like the for the RouterStateSnapshot and such. We create the mock AuthenticationService and create an instance of the AuthenticationGuard. We then test the canActivate function when allowIfAuthorized returns true and when it returns false. We call the canActivate function, subscribe to the value, and then check that value to make sure it is what we expect it to be. To run these tests, since they're asynchronous, we can't forget to import and use async from @angular/core/testing.

Conclusion

I hope that if you've made it this far, you've learned something new. I know I have over the past couple weeks as I've written these tests. It took me a long time to get started on writing unit tests for Angular because I felt overwhelmed. I didn't know where to start or what to test or how to write the tests. But I will absolutely say that I feel so much more confident in my Angular library with the tests than I've ever felt about any other application. I know immediately when I make a change if it's broken anything or not. It feels good to have that level of confidence. Hopefully this article can be a good reference for many people. I know it will be a good reference for me!

Top comments (0)