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></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.
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
.
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!