Developer’s guide to Unit Testing in Angular — Part 1
Explore how to do unit testing and improve the quality of various pieces of code in Angular using Karma/Jasmine.
What is Unit Testing and why do it?
As a new developer, my biggest mistake was not to take unit testing seriously. The educational institutes focus on subjects like algorithms, data structures, and various languages. While these subjects are required for making the apps/software fast and efficient, unit testing is required to make those apps more reliable.
A unit test, by definition, is testing the smallest piece of code that can be logically isolated in the system.
The purpose is to validate that each piece of code is performing its task well without any external system.
In Angular, we use Jasmine to write the test cases and then run those test cases in Karma task runner.
Unit Testing improves the quality of code significantly. It also makes it easier for a developer to add new features by modifying the existing piece of code and not worry much about testing it if the test cases are successful and thus reduce cost and time.
Unit Test Setup in Angular
While installing Angular CLI, configurations of Karma and Jasmine are automatically installed with it. We’ll find the following configs in package.json file under devDependencies:
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "^4.0.1",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "^2.1.1",
"karma-jasmine": "~1.1.1",
"karma-jasmine-html-reporter": "^0.2.2"
To run the test cases, open the terminal and run the command ng test. This command will open the default browser window and start running the test cases one by one from spec.ts files in the Angular project.
Let’s take an example by testing a Form!
Below is a simple HTML snippet for a login form:
<form (ngSubmit)="submitForm()" [formGroup]="loginForm" novalidate>
<label>Email address</label>
<input type="email" placeholder="Enter email" formControlName="email"> <label>Password</label>
<input type="password" placeholder="Password" formControlName="password"> <input type="checkbox" class="form-check-input" formControlName="rememberMe">
<label class="form-check-label">Remember Me</label> <button type="submit" class="btn btn-primary" [disabled]="!loginForm.valid">Submit</button>
</form>
Now let’s add the validators in app.component.ts so we can test them later.
export class AppComponent {
constructor(private fb: FormBuilder) { } loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
rememberMe: [false]
}); formSubmitted = false; submitForm() {
this.formSubmitted = true;
}
}
Now, our form is complete and we are ready to test it. The Angular CLI generates a test file for the AppComponent named app.component.spec.ts. Let’s first start with setting up all the dependencies and angular testing tools required in our spec.ts file.
import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { DebugElement } from '@angular/core';
import { By, BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AppComponent],
imports: [
FormsModule,
BrowserModule,
ReactiveFormsModule
]
}).compileComponents();
})); beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
de = fixture.debugElement.query(By.css("form"));
el = de.nativeElement;
})
});
Let’s go through each item individually to get an idea about why are they used.
- TestBed: It is the most important Angular testing tool that configures an Angular environment where we can test our components and services.
- async: We need to define async in beforeEach, so that the asynchronous task is finished first before moving on to another.
- ComponentFixture: It is a fixture for debugging and testing a component. It contains various methods like detectChanges(), destroy() etc.
Next, we’ll import various dependencies for this component like FormsModule and ReactiveFormsModule.
Note: We’ll have to import all the dependencies required for a component. In case, the component is dependent on any service or pipe, we’ll have to mock that service or pipe. We’ll come to this in Part 2.
Now, let us start creating our test cases.
it('should test if submit button is disabled when the form is invalid -- Required fields are empty', async(() => {
component.loginForm.controls['email'].setValue('');
component.loginForm.controls['password'].setValue('');
fixture.detectChanges();
expect(el.querySelector('button').disabled).toBeTruthy();
}));it('should test if submit button is disabled when the form is invalid -- Wrong format of email', async(() => {
component.loginForm.controls['email'].setValue('abc');
component.loginForm.controls['password'].setValue('abc@123');
fixture.detectChanges();
expect(el.querySelector('button').disabled).toBeTruthy;
}));it('should test if submit button is enabled when the form is valid', async(() => {
component.loginForm.controls['email'].setValue('abc@abc.com');
component.loginForm.controls['password'].setValue('abc@123');
fixture.detectChanges();
expect(el.querySelector('button').disabled).toBeFalsy;
}));it('should test if formSubmitted is true', async(() => {
component.submitForm();
fixture.detectChanges();
expect(component.formSubmitted).toBeTruthy;
}));it('should test if submitForm method has been called 0 times', async(() => {
fixture.detectChanges();
spyOn(component, 'submitForm');
el.querySelector('button').click()
expect(component.submitForm).toHaveBeenCalledTimes(0);
}));
Some points to note here:
- fixture.detectChanges() is very important here, otherwise, the testing environment won’t read the values that have been entered and the default values will be an empty string as defined in the component.ts.
- Since we’ve added Validators.email as well as a part of the validator for the email field, we can test if the format of the email is valid or not.
- toBeTruthy() is used if the expect statement returns true and toBeFalsy() for a false return.
- In the last test case, we are creating a jasmine spy on the submitForm method and then we’ll click the button to submit the form using DOM. We’ll expect that the submitForm method shouldn’t be triggered as the button is disabled because there is no input in the form.
For the complete app.component.spec.ts file, you can check out this GitHub gist:
References:
To write test cases for services, pipes, and directives, check out the part 2 of this series, Developer’s Guide to Unit Testing in Angular — Part 2 (Services, Pipes, Directives)