Topic
The developer should test the code. In this example, I will create a simple form with an HTTP request after submission and test.
Project
I used Angular CLI to create the project (default CLI answers):
ng new notification-example
I used Material Angular to provide propper styling by typing (default answers):
ng add @angular/material
Main module
To be able to use required Material modules I added them in imports in AppModule:
imports: [
BrowserModule,
BrowserAnimationsModule,
ReactiveFormsModule,
HttpClientModule,
MatInputModule,
MatFormFieldModule,
MatButtonModule,
MatSnackBarModule,
],
I also added HttpClientModule to be able to make HTTP calls. ReactiveFormsModule is for making Reactive forms.
Full Module code:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatButtonModule } from '@angular/material/button';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
ReactiveFormsModule,
HttpClientModule,
MatInputModule,
MatFormFieldModule,
MatButtonModule,
MatSnackBarModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Component
In AppComponent I defined simple form with one field which I set as required.
form = this.formBuilder.group({
text: [null, Validators.required],
});
In the constructor, I used two injected classes:
-
FormBuilderfor making Reactie Form -
ApiServicefor sending data via an HTTP request (Service description is placed lower). On form submission, I am checking if form is valid and if it is then I am passing field value to the service. Full component code:
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { ApiService } from './api.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
form = this.formBuilder.group({
text: [null, Validators.required],
});
constructor(
private readonly formBuilder: FormBuilder,
private readonly apiService: ApiService
) {}
onSubmit(): void {
if (this.form.invalid) {
return;
}
this.apiService.create(this.form.get('text').value);
}
}
HTLM part is really simple, It has form with one field and the submit button.
Full HTML code:
<form [formGroup]="form" (submit)="onSubmit()">
<mat-form-field appearance="fill">
<mat-label>Text</mat-label>
<input matInput formControlName="text">
</mat-form-field>
<button mat-raised-button color="primary" [disabled]="form.invalid">Send</button>
</form>
To place form in center of the window I added some flexbox styling:
:host {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
form {
display: flex;
flex-direction: column;
width: 400px;
}
:host applies styling to the component root element, so angular will apply styling to the <app-root> element.
Service
At the beginning of the service, I defined two variables:
-
url- URL address where service will send data -
subject- RxJS class which is used to pass data to HTTP call. We can use thenextmethod to pass that data.
Constructor has two injected classes:
-
HttpClientto be able to make HTTP calls, -
MatSnackBarfor displaying snack bar from Angular Material. Subject is used to pass data:
this.subject
.pipe(
debounceTime(500),
switchMap((text) => this.http.post(`${this.url}posts`, { text }))
)
.subscribe(
() => this.snackBar.open('Post saved!', null, { duration: 3000 }),
() =>
this.snackBar.open('Something went wrong.', null, { duration: 3000 })
);
I am using Subject as an observable by calling the pipe method to work on stream:
-
debounceTimeRxJS operator will wait with emission in a given time and ignores data emitted in a shorter period. -
switchMapRxJS operator takes data from the outer observable and passes it to the inner observable. Angular Service from default is a singleton, so We don't have to unsubscribe the subject inside the constructor. If no error occurs during emission snack bar is opened with aPost saved!message. If an error occurs, thenSomething went wrongis displayed.
To pass data to subject I am using next method:
create(text: string): void {
this.subject.next(text);
}
Full service code:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Subject } from 'rxjs';
import { debounceTime, switchMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class ApiService {
private readonly url = 'https://jsonplaceholder.typicode.com/';
private readonly subject = new Subject<string>();
constructor(
private readonly http: HttpClient,
private readonly snackBar: MatSnackBar
) {
this.subject
.pipe(
debounceTime(500),
switchMap((text) => this.http.post(`${this.url}posts`, { text }))
)
.subscribe(
() => this.snackBar.open('Post saved!', null, { duration: 3000 }),
() =>
this.snackBar.open('Something went wrong.', null, { duration: 3000 })
);
}
create(text: string): void {
this.subject.next(text);
}
}
Service tests
To check code coverage of our project, I typed in the command line:
ng test --code-coverage
It uses a karma reporter to generate test coverage, which I can check in the coverage directory. My Service test is missing some checks, so that I will add them.

I generated service with:
ng g service api
so I have a service file and *.spec.ts file, which contains tests.
describe block is for wrapping tests in group. beforeEach method is triggered before each test. In this method in imports, I have:
describe('Service: Api', () => {
let service: ApiService;
let http: HttpClient;
let snackBar: MatSnackBar;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ApiService],
imports: [HttpClientTestingModule, MatSnackBarModule, NoopAnimationsModule],
});
service = TestBed.inject(ApiService);
http = TestBed.inject(HttpClient);
snackBar = TestBed.inject(MatSnackBar);
});
-
HttpClientTestingModule- for faking HTTP request (I don't want to make real calls) -
MatSnackBarModule- component needs it to construct -
NoopAnimationsModule- component needs it to construct, faking animations next, I am taking required instances in tests: -
service- my service instance allows me to use service methods -
http- HTTP service, for mocking responses -
snackBarfor listening to method calls
Test: should send http call
it('should send http call', fakeAsync(() => {
const spy = spyOn(http, 'post').and.callThrough();
service.create('test');
service.create('test1');
tick(500);
expect(spy).toHaveBeenCalledOnceWith('https://jsonplaceholder.typicode.com/posts', { text: 'test1' });
}));
it wraps a single unit test. fakeAsync allows me to wait for some time in the test.
const spy = spyOn(http, 'post').and.callThrough();
I want to check if post method will be called. I am passing http instance to check that and .and.callThrough(); to execute code normally like inside service.
service.create('test');
service.create('test1');
tick(500);
I am passing value to the create method like the component is doing. tick waits for the time in given milliseconds (reason to wrap test with fakeAsync).
expect(spy).toHaveBeenCalledOnceWith('https://jsonplaceholder.typicode.com/posts', { text: 'test1' });
}));
In the end, I am checking if my spy (post method from HTTP service instance) is called only once with the same values as in service.
Test: should call open on snack bar positive
it('should call open on snack bar positive', fakeAsync(() => {
spyOn(http, 'post').and.returnValue(of(true));
const openSpy = spyOn(snackBar, 'open');
service.create('test');
tick(500);
expect(openSpy).toHaveBeenCalledOnceWith('Post saved!', null, { duration: 3000 });
}));
Main difference from first test is:
spyOn(http, 'post').and.returnValue(of(true));
I used .and.returnValue(of(true)); to fake response from HTTP service and I am returning new observable by using of operator with value true. The rest of the test is similar to the first one. In the end, I am checking if a "positive" snack bar was called.
Test: should call open on snack bar negative
it('should call open on snack bar negative', fakeAsync(() => {
spyOn(http, 'post').and.returnValue(throwError('err'));
const openSpy = spyOn(snackBar, 'open');
service.create('test');
tick(500);
expect(openSpy).toHaveBeenCalledOnceWith('Something went wrong.', null, { duration: 3000 });
}));
Like the second one, but I am checking if the "negative" snack bar was called.
Now, after checking code coverage, I have 100% code covered in my service, and all tests passed:

Link to repo.
Top comments (0)