Importance of TDD (Test Driven Development)
End to end Testing
Unit Testing in AngularJS
Angular has provided all the functionalities needed to test a web application, out of the box. They included few packages and utilities with which an Angular application can be tested end to end. AngularJS has recommended Karma as a task runner and suggested to use Jasmine to write unit test cases.
In this article, we are going to focus only on writing unit test cases for a very simple single page application "Address Book" which is using Angular V5. Complete application code and its unit test cases are available on Github.
It is very important to keep the scope of functionality, we want to test, in mind while writing unit test cases. The smallest unit test should be able to test a function.
We bootstrapped our Angular application using latest Angular CLI (v1.6.5) and also used CLI to generate the new class, component, and services. Angular CLI has a very good documentation on how to use it. I am using NodeJS version 8.9.4 which is the latest long-term support (LTS ) version available. Our app folder looks like the screenshot given below.
As per above structure, we have created a model class for Contact and one Service to manage Contacts. We have divided our application into following components.
As a recommendation, all test files should have a file name as XYZ.spec.ts where XYZ could be a normal class, a component, a service etc.
Test a Class
Let us start with a very simple test case for Contact class.
We have included three test cases to test Contact class
Should be able to create a new instance.
Its constructor should be able to accept values as a parameter and set the Contact class fields.
When "save" method of this class is called, it should update the derived field's full name for the contact instance.
Jasmine provides "describe", "it", "expect" and several matcher methods like "toEqual", "toBeNull" etc. to write the test cases. You can read the Jasmine documentation to learn more about Jasmine syntax.
Let’s go line by line to see how we are testing the Contact class.
First, we create a test suite named Contact by using describe the method. In a first test case, we create a new instance of Contact class and we expect its value to be truthy.
In a second test case, we test whether constructor of Contact class can accept the values and set those values to instance properties. For this, we are creating an instance and checking its properties value using expect and matcher methods.
In the third test, we create a Contact instance with some values and check that fullName field of instance be empty by default. Later when we call save method of the instance, it should set the full name property of the instance.
These test cases are the very basic set of unit test cases to verify whether everything is working as expected in Contact class. To execute the test cases, just run npm test which executes ng test command to start the execution.
Test an Angular Service
When we generate a service by using Angular CLI then a test file for service is also generated by default.
There are few things to notice in this test case.
TestBed: TestBed is a utility provided by @Angular/core/testing module to configure and create an Angular testing module in which we need to run the test case. We use TestBed.configuretestingModule() method to configure the testing module. This is very much similar to creating an Angular module using @NgModule. In this case, we provided AddressBookDataService as a provider for this testing module. Next, we use inject method to make AddressBookDataService available in every test case. The first argument of inject method is an array of Angular dependency injection tokens. The second argument is the test function whose parameters are the dependency injected via the first argument. We have included test cases for every method present in AddressBookDataService.
We should configure our test module in Jasmine's beforeEach method so that TestBed can reset itself to the base state before each test runs.
Test an Angular Component
Let’s write a test for a very simple Header component of our application. After configuring test bed, you tell it to create an instance of Header component and returns a component test fixture. The CreateComponent method returns a component fixture that gives us access to DOM structure of the component.
In this test case, we check whether title property of component gets displayed correctly on UI. For this, we get the <h2> element by querying component fixture's debug element using CSS selector and check its text value.
In next text case, we change the value of the title and check whether updated value of the title is being displayed. For this, we change the value of component “title” and invoke fixture.detectChange() method. This invocation tells Angular to recalculate component's properties and update the UI accordingly.
We can also check any element's class name by checking the DOM element. That is what we are doing in our last test case of Header component.
Test an Angular Component with Input and Output
Let’s look into the test case for Contact component. It accepts an Input value from its parent component and passes some values to its parent component by invoking output methods. In beforeEach() method, we are setting the contact property of Contact component which is very much similar as a contact property is passed to this component via @Input.
Contact component edit and delete operation with contact id to its parent component. We test this method by subscribing to the event we are emitting in the actual component. In case of edit contact, we need to subscribe to edit event. We invoke this event by programmatically clicking the edit button present in Contact component. In the end, we check whether the correct value is being passed with edit event.
Test an Angular Component which uses a service and has child components
Let’s consider the AddressBook Component, which uses AdressBookDataService and has included multiple components in its view. Ideally, a unit test case should have a minimum dependency on other modules so that we can limit its testing scope. As this component has child components, the testing module needs to be informed about child components in a similar way, we do with the angular module by populating declarations property in module configuration. To avoid including child component in the testing module, we ask module to not throw an error because of the dependent components. To do this, we set schemas property to NO_ERROR_SCHEMA in module configuration.
Ideally, we don't want to hit the actual service in our testing because it is not a good practice to interact with actual data in testing. Angular has provided a way by which we can configure our module to interact with test data in place of actual service. We created a addressBookDataServiceStub, which is a mock implementation of our actual service and then tell testing module while setting the provider to use addressBookDataServiceStub in place of actual service.
We can access addressBookDataServiceStub by calling get method of TestBed.
Test an Angular component with two-way binding
We have a save-contact component where we are using Angular's two-way binding with ([ngModel]). First, we import FormsModule while configuring Test module, as ngModel is not present by default in AngularJS. We are writing a test that verifies that input boxes should automatically get populated with values assigned to component's contact variable. Let’s look into the test "should show input field with contact info". First, we get all the DOM references for input boxes on UI by using CSS selector to query fixture's debug element. Then we set component's contact to some value and trigger detectChanges(). Since ngModel updates are asynchronous, fixture.detectChanges() won't be reflected instantly. Fixture gives a method called whenStable which returns a promise. This promise gets resolved once the detectChanges completes its calculation. So, we need to check our expected values when the promise gets resolved.
Now, we are going to test the other way in two-way binding, which means, when we input value in the text box, component's value should get updated. The only difference between this and the above is, here, we need to explicitly dispatch an input event so that Angular can bind these value to component's variable.
We tried to cover the testing of very basic scenarios while building an AngularJS application. It's a very good practice to always write unit test cases for components to avoid bugs and verify that the component is working as expected. Angular has also provided a very good documentation about testing an AngularJS application in. more details. (https://angular.io/guide/testing)