Preston Lamb
Posted on May 31, 2019
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;
}
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);
});
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';
}
}
}
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');
});
});
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;
});
}
}
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);
});
});
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 theTestBed
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 theTestBed
. What I've learned is that you'll know when you need theTestBed
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();
}
}
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) {}
}
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');
}));
});
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$;
}
}
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;
}
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);
}));
});
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();
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);
}
}
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);
});
}));
});
}
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!
Posted on May 31, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 18, 2024