A few months ago I wrote anintro 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 aboutunit 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 exactlyAngular, we can still test them in the same way we would any other Angular piece. Let's use the following function in ourarray.util.ts
file:
exportfunctionstringArrayContainsPartialStringMatch(arr:string[],strMatch:string){returnarr.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 thestrMatch
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',()=>{constarr=['value 1'];conststr='value 1';constincludedInArray=stringArrayContainsPartialStringMatch(arr,str);expect(includedInArray).toBe(true);});it('should return true if the value is included in the string array',()=>{constarr=['value 1'];conststr='val';constincludedInArray=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:
exportclassStatusDisplayPipeimplementsPipeTransform{transform(id:number):string{if(typeofid!=='number'){thrownewError('The ID passed in needs to be a number.');}switch(id){case2:return'Sent';case3: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',()=>{constpipe=newStatusDisplayPipe();conststatusDisplay=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 thenew
keyword. Next, we call thetransform
method on the pipe and pass it the ID we want to test. Then, we use theexpect
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 thetransform
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 aSubject
, 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:
exportclassConfigurationService{constructor(private_http:HttpClient){}loadConfiguration(){returnthis._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 mockHttpClient
application and then create an instance of the service. We can use the mockedHttpClient
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 thefakeAsync
andtick
methods from@angular/core/testing
or just theasync
method from the same library. Let's look at some examples:
describe('ConfigurationService',()=>{constmockConfigObject={apiUrl:'https://apiurl.com'};letmockHttpService;letconfigurationService;beforeEach(()=>{mockHttpService=jasmine.createSpyObj(['get']);configurationService=newConfigurationService(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(newError('test error')));configurationService.loadConfiguration();tick();expect(configurationService.configData).toBe(null);});});
Let's break these tests down a little. At the top of thedescribe
, we set some "global" variables for the service. ThemockHttpService
and theconfigurationService
are initialized in thebeforeEach
method. We usejasmine.createSpyObj
to create a mockHttpClient
instance. We tell it that we are going to mock theget
method. If we needed other functions fromHttpClient
, we would add them to that array.
In each of the two unit tests, we tell themockHttpService
what to return when theget
method is called. In the first one, we tell it to return ourmockConfigObject
as an Observable. In the second, we usethrowError
from RxJS. Again in both, we call theloadConfiguration
method. We then do a check to see if the internalconfigData
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 thatconfigData
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 theHttpClient
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:
exportclassTypeaheadInputDirectiveimplementsAfterContentInit{@Input()debounceTime:number=300;@Output()valueChanged:EventEmitter<string>=newEventEmitter<string>();constructor(privatesearchInput: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 theinput
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 theinput
emits a value means creating aTestHostComponent
which has theinput
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" /> `,})classTestHostComponent{@ViewChild(TypeaheadInputDirective)typeaheadInputDirective:TypeaheadInputDirective;publicdebounceTime:number=300;valueChanged(newValue:string){}}
This is just theTestHostComponent
. We have access to the directive via the@ViewChild
decorator. Then we use thedebounceTime
input to control that in case we want to test what happens when we change that. Lastly we have avalueChanged
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',()=>{letcomponent:TestHostComponent;letfixture: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');constinput=fixture.debugElement.query(By.css('input'));input.nativeElement.value='Q';input.nativeElement.dispatchEvent(newKeyboardEvent('keyup',{bubbles:true,cancelable:true,key:'Q',shiftKey:true}),);tick(component.debounceTime);expect(component.valueChanged).toHaveBeenCalledWith('Q');}));});
In the twobeforeEach
functions, we use theTestBed
to create the testing fixture and get access to the component. Then, in the test, we add thespyOn
on ourvalueChanged
function. We then find theinput
element and set the value to 'Q', and then dispatch theKeyboardEvent
. We usetick
to wait for thedebounceTime
to pass, and then we check that thevalueChanged
function has called with the stringQ
.
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 aSubject
. It is slightly complicated because we're going to use aTemplateRef
to pass in the template that thengFor
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'],})exportclassAlertsDisplayComponentimplementsOnInit{publicalerts$: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 theSubject
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 ourTestHostComponent
again in this test:
@Component({selector:'app-test-host',template:` <alerts-display> <ng-template let-alert> <p>{{ alert.message }}</p> </ng-template> </alerts-display> `,})classTestHostComponent{@ViewChild(AlertsDisplayComponent)alertsDisplayComponent:AlertsDisplayComponent;}
In thisTestHostComponent
, we put the<alerts-display>
component in the template, and provide the template for thengFor
loop. Now let's look at the test itself:
describe('AlertsDisplayComponent',()=>{letcomponent:TestHostComponent;letfixture:ComponentFixture<TestHostComponent>;letmockAlertsToasterService:AlertToasterService;beforeEach(async(()=>{mockAlertsToasterService=jasmine.createSpyObj(['toString']);mockAlertsToasterService.alerts$=newSubject<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();constpTags=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 theAlertToasterService
and get access to the fixture and component in thebeforeEach
functions. Then in the test we emit a new array of alerts. This is what will happen in the service after theaddAlert
function is called. Then all the places where theSubject
is subscribed to will get the new list and output the results. We throw in atick
to make sure that any necessary time has passed, and then (and this is important) we tell thefixture
todetectChanges
. 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 thefixture
to find all thep
tags. Now, because we emitted an array with only one alert item, we will expect there to only be onep
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:
constcomponent=newMyComponent();
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()exportclassAuthenticationGuardimplementsCanActivate{constructor(private_authenticationService:AuthenticationService){}canActivate(next:ActivatedRouteSnapshot,state:RouterStateSnapshot):Observable<boolean>{constpreventIfAuthorized:boolean=next.data['preventIfAuthorized']asboolean;constredirectUrl:string=state.url&&state.url!=='/'?state.url:null;returnpreventIfAuthorized?this._authenticationService.allowIfAuthorized(redirectUrl):this._authenticationService.allowIfNotAuthorized(redirectUrl);}}
There's only one function we're going to test here: thecanActivate
function. We'll need to mock theAuthenticationService
] and theActivatedRouteSnapshot
andRouterStateSnapshots
for these tests. Let's take a look at the tests for this guard:
describe('AuthenticationGuard',()=>{letauthenticationGuard:AuthenticationGuard;letmockAuthenticationService;letmockNext:Partial<ActivatedRouteSnapshot>={data:{preventIfAuthorized:true,},};letmockState:Partial<RouterStateSnapshot>={url:'/home',};beforeEach(()=>{mockAuthenticationService=jasmine.createSpyObj(['allowIfAuthorized','allowIfNotAuthorized']);authenticationGuard=newAuthenticationGuard(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 theRouterStateSnapshot
and such. We create the mockAuthenticationService
and create an instance of theAuthenticationGuard
. We then test thecanActivate
function whenallowIfAuthorized
returns true and when it returns false. We call thecanActivate
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 useasync
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)
For further actions, you may consider blocking this person and/orreporting abuse