Last week's post showed you how to create a popup using Angular 4 and ng-bootstrap. This week, I'll show you how to test it. This post covers testing the popup component. Next week I'll test creating the popup.
Get a Testing Framework
You'll need a testing framework. I have my own scripts based on Karma, Jasmine, and karma-typescript. I write about this in full details in the companion book to my
Angular 4 book series. You could also use the Angular CLI or scripts of your own choosing. Specifics of the setup is beyond the scope of this article, because I want to focus on the tesing technique.
Configure the Test Module
Angular includes a testing construct called TestBed. This is a module created for testing other modules and is considered the foundation of all Angular tests. We're going to create a base.test.ts file to set up the TestBed to parallel the main application. This will parallel the main application from our source app.
It doesn't matter where you create the base.test.ts file as long as it is loaded by your testing code, and your apps main module is ignored. I place it in the root of a testing directory. When the app runs in a browser, the index loads a shim library and a ZoneJS library that are required by Angular. But the tests are not run in a browser, so we need to import these scripts manually, like this:
import "core-js"
import "zone.js/dist/zone";
import "zone.js/dist/long-stack-trace-zone";
import "zone.js/dist/proxy";
import "zone.js/dist/sync-test";
import "zone.js/dist/jasmine-patch";
import "zone.js/dist/async-test";
import "zone.js/dist/fake-async-test";
This will prevent a lot of confused errors about uncaught reflect-metadata and class decorators.
Now we need to import the Angular specific testing modules:
import { TestBed } from "@angular/core/testing";
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from "@angular/platform-browser-dynamic/testing";
This imports the TestBed which is the testing module. It also imports BrowserDynamicTestingModule and platformBrowserDynamicTesting. These are used to parallel the platformBrowserDynamic class which loads the initial application.
With these imported we can immediately initialize the TestBed:
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
Now import the BrowserModule which is the ng module for the browser, and the NgbModule, which contains the ng-bootstrap libraries:
import { BrowserModule } from '@angular/platform-browser';
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
Now, import the custom components from our demo:
import { AppComponent } from '../src/com/dotComIt/learnWith/main/app.component';
import { PopupComponent } from '../src/com/dotComIt/learnWith/views/popup/popup.component';
Now, configure the TestBed:
beforeEach(() => {
TestBed.configureTestingModule({
imports : [BrowserModule, NgbModule.forRoot()],
declarations: [ AppComponent, PopupComponent ]
}).overrideModule(BrowserDynamicTestingModule, {
set: {
entryComponents: [ PopupComponent ]
}
})
});
The first thing to notice is that I put the TestBed configuration in a beforeEach() function. What is beforeEach()? It is a special function that is part of the Jasmine testing framework. The function allows you to run code before the tests are executed. We use it to create and configure the testing module which will be used by most of our other unit tests. You can have multiple beforeEach() functions if needed, but here we only need one.
The configureTestingModule() accepts a configuration object which sets up imports for other modules and declarations for components. This is all like the @NgModule metadata in our main application. The configureTestingModule() does not support entryComponents, unfortunately. We used the entryComponents metadata to set up the component that ng-bootstrap will use to create the modal.
Thankfully there is a workaround. We daisy chain an overrideModule() method after the configureTestingModule is created to add the entryComponents metadata.
That's all we need in the base.test.ts file. Notice there is no formal export of a class. It isn't needed, since no other classes will use this explicitly. Be sure this file is compiled into your tests before your other code to avoid compile errors.
Test the Popup
Now, we're going to test the PopupComponent. There are two methods that we care to test:
onDismiss(reason : String):void {
this.activeModal.dismiss(reason);
}
onClose():void {
this.activeModal.close('closed');
}
The first is the onDismiss() method, which will occur if the user cancels the popup. It means all changes are cancelled. The second method is the onClose() method and it occurs if the user formally closes the popup with the close button.
Create a file named popup.componment.test.ts. Start with imports:
import {async, TestBed} from '@angular/core/testing';
import {NgbModal, NgbModalRef, NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
import {PopupComponent} from "../../../../../../src/com/dotComIt/learnWith/views/popup/popup.component";
It imports the async and TestBed modules. The async import is used to run a beforeEach() or it() test inside an async test zone that mocks the mechanics of asynchronous processing. Then some modal specific classes are imported from ng-bootstrap. Finally our custom PopupComponent is loaded.
Now, create a describe function:
describe('popupcomponent', function () {
});
The describe() function is a Jasmine function that defines a test suite, which is a collection of related tests. The function takes two arguments. The first argument is a string that is the name of the test suite, in this case 'Sample Tests'. The second argument is a function, which runs the tests. You can embed a describe() function inside other describe() functions if you feel it is relevant to your testing.
Create two variables inside the describe() block:
let comp: PopupComponent;
let activeModal : NgbActiveModal;
We'll define these values in a beforeEach() block and reuse them in individual tests. Look at the beforeEach() next:
beforeEach(async(() => {
TestBed.compileComponents().then(() => {
let modalService : NgbModal = TestBed.get(NgbModal);
const modalRef : NgbModalRef = modalService.open(PopupComponent);
comp = modalRef.componentInstance;
activeModal = comp.activeModal;
});
}));
We compile the components on the TestBed. This returns a promise, which we use to get access to the global variables our tests will need. First it creates a local instance of the modalService. It uses that modalService to open the popup and create the modalRef variable. The component instance is saved off the modalRef. And the activeModal value is pulled out of the component for easy access.
The first test:
it('should create component', () => expect(comp).toBeDefined() );
The it() function defines the test. It has two arguments, and the first is the descriptive name of the test. If it fails, you can use this name to determine which test actually failed. The second argument is a function that performs the test.
The test is written to be easily parsable:
expect(comp).toBeDefined()
This is called an assertion. We expect the component to be defined. This will return true if the component exists, or false if the component does not exist. This is often an easy test to run to make sure you have your configuration set up correctly.
Let's write a test for the onDismiss() method:
it('Call onDismiss()', () => {
spyOn(activeModal, 'dismiss');
comp.onDismiss('Some Reason');
expect(activeModal.dismiss).toHaveBeenCalled();
expect(activeModal.dismiss).toHaveBeenCalledWith('Some Reason');
});
This test uses a new command, spyOn(). The spyOn() method is used to watch an object for a method call. You can use this in tests to intercept the method and return specific values. Alternatively, you could use it to verify that a certain method was called. The spyOn() is looking at the activeModal as the first argument, and the dismiss method as the second argument. Then the onDismiss() method is called on the component. Two tests follow. One to make sure that the dismiss() function was called. And the second to make sure it was called with a specific argument, 'Some Reason'.
The test of the onClose() method operates using a similar fashion:
it('Call onClose()', () => {
spyOn(activeModal, 'close');
comp.onClose();
expect(activeModal.close).toHaveBeenCalled();
expect(activeModal.close).toHaveBeenCalledWith('closed');
});
Instead of spying on the dismiss method of the active modal we spy on the close method. And that is the method we check in the two assertions.
Try this out by running your tests. If you use my seed, use this:
Gulp test
And you'll see something like this:
![]()
Final Thoughts
This concludes this article, but I have another one coming out next week. It will examine the code I use to test the component that creates the popup.
If you want all this information and more, get my
the bonus book to my Angular 4 book series. It shows you how to create an app from scratch, and covers Angular CLI, unit testing, debugging techniques, and how to build your own project seed.