Skip to content

Instantly share code, notes, and snippets.

@pjlamb12
Created May 29, 2019 22:20
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save pjlamb12/fc918f14594511a79883fe8a1099fd0f to your computer and use it in GitHub Desktop.

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.

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment