This is the second half of my article on testing a Bootstrap popup. Before reading this, you should read my post about creating a popup with Angular and Bootstrap, and the one about testing the popup component. This post will focus on testing the code that creates the popup.
Review Code
In my test sample, the code to create the popup is in the app.component.ts file. This is it:
2 const modalRef = this.modalService.open(PopupComponent );
3
4 modalRef.result.then((result) => {
5 console.log(result);
6 console.log('closed');
7 }).catch( (result) => {
8 console.log(result);
9 console.log('cancelling');
10 });
11}
This is triggered by button click in the view. Create an instance of the modal using the modalService, which is an instance of the NgbModal. It saves that instance in another variable, modalRef. The modalRef.result is a promise, and we can use that to run code whenever the modal is closed or dismissed. The promise then() method represents a successful closure of the modal. The promise catch() method represents a dismissal. Since this is a test app, the close and dismiss methods don't do anything other than log items out to the console, but a real app may save data or update a view.
We're going to write a few tests against this code. The first will just verify that the modal opened. Then we'll open and close the modal, verifying that the result function was called. Then we'll open and dismiss the modal, verifying that the catch function was called.
Write the Tests
I put my tests in a file named app.component.test.ts. As always, we'll start with the imports. These are the Angular testing imports and the ng-bootstrap imports:
2import {NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap";
Now, load our custom components:
2import {PopupComponent} from "../../../../../src/com/dotComIt/learnWith/views/popup/popup.component";
We import the AppComponent, which contains the code we'll be testing and the PopupComponent which contains the code we'll be testing. Now, create a describe function to create the set of unit tests:
2});
Next, create a bunch of variables inside the describe:
2let appComponent: AppComponent;
3let modalService: NgbModal;
4let modalRef: NgbModalRef;
The ComponentFixture is used to create an instance of a component so you can access all properties, methods, and DOM elements of its HTML template. The appComponent is an instance of the component's class. The modalService and modalRef relate to the popup window that will be created.
Here is a beforeEach():
2 TestBed.compileComponents().then(() => {
3 modalService = TestBed.get(NgbModal);
4 modalRef = modalService.open(PopupComponent);
5 fixture = TestBed.createComponent(AppComponent);
6 appComponent = fixture.componentInstance;
7 spyOn(modalService, "open").and.returnValue(modalRef);
8 spyOn(console,'log').and.callThrough();
9 });
10}));
It compiles the components on the TestBed. Remember the TestBed was configured in a base.test.ts file, which our scripts know to compile first. After the components are compiled, we get the modalService and create a modalRef by opening the component. The fixture instance of the AppComponent is stored as is its componentInstance.
Finally the beforeEach() creates two spyOn() methods. It looks at the open method of the modalService, and tells it to return the modalRef as a specific value. Then it spys on the console.log() method. Normally I would try to avoid spying on this, but since no real functionality exists inside our app this is the best way to determine if the close or dismiss methods will run on the modal popup.
Let's start off with an easy test, to see that the modal opened:
2 appComponent.onModalRequest();
3 expect(modalService.open).toHaveBeenCalled();
4});
This is pretty self-explanatory. It calls the appComponent's onModalRequest() creates the modal, and checks to see that the open method had been called, insinuating that the modal had been created.
Let's create the modal, then close it:
First thing you notice is that a done() function is passed into the test function. This is a function that tells Jasmine to run the code asynchronously. This is a special Jasmine construct, and is different than the async or fakeAsync test beds which are Angular constructs. The Angular constructs have all sorts of issues if you use any sort of timer under the hood, and gave me lots of problems when I was writing the test code for the learn with app. This approach avoids those issues.
Next, the onModalRequest() method is called on the appComponent. Then we access the fixture to detectChanges(). This tells the component to process a digest cycle. It will redraw whatever needs redrawing. In our case, it waits for the modal to actually be created and opened. Then we wait until the fixture is stable using the whenStable() function. This means the modal was created and it is all good to continue. The whenStable() method returns a promise, and we execute the success function on it. Inside the success function we call a close() method on the modalRef value. We do not need to call the detectChanges() again, but we do need to wait until code is stable before running our assertion. The assertion excepts that console.log() will have been called with the value "closed". This is the hard coded result value inside the main app component code. Finally it calls the done() function to tell Jasmine that we are done running async code. This completes the test
To test the dismiss function, we use a similar approach, but I needed an extra level of waiting until the component fixture was stable before I could check for the dismiss function to have been called:
2 appComponent.onModalRequest();
3 fixture.detectChanges();
4 fixture.whenStable().then(() => {
5 modalRef.dismiss();
6 fixture.whenStable().then(() => {
7 fixture.whenStable().then(() => {
8 expect(console.log).toHaveBeenCalledWith('cancelling')
9 done();
10 });
11 });
12 });
13});
The code opens the modal with appcomponent.onModalRequest(). Then it calls fixture.detectChanges() to watch for the DOM changes. Then it uses whenStable() to make sure the modal opened. When it is open, it is dismissed. Then it waits until it's stable twice before running the assertion. The assertion checks that the console.log() method was called with the 'cancelling' string. Then it calls the done() function, finishing off this method.
Run it:
And you'll see results like this: