Angular simple form with async testing
Tags: angular, rxjs, testing, typescript
Published: 2021-05-10 18:27:48

## 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`:
```typescript
  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:
```typescript
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.
```typescript
  form = this.formBuilder.group({
    text: [null, Validators.required],
  });
``` 
In the constructor, I used two injected classes:
* `FormBuilder` for making Reactie Form
* `ApiService` for 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:
```typescript
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:
```HTML
<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:
```css
: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](https://rxjs.dev/api/index/class/Subject) which is used to pass data to HTTP call. We can use the `next` method to pass that data. 

Constructor has two injected classes:
* `HttpClient` to be able to make HTTP calls,
* `MatSnackBar` for displaying snack bar from Angular Material.
Subject is used to pass data:
```typescript
    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:
* `debounceTime` RxJS [operator](https://rxjs.dev/api/operators/debounceTime) will wait with emission in a given time and ignores data emitted in a shorter period.
* `switchMap` RxJS [operator](https://rxjs.dev/api/operators/switchMap) 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 a `Post saved!` message. If an error occurs, then `Something went wrong` is displayed. 

To pass data to subject I am using `next` method:
```typescript
  create(text: string): void {
    this.subject.next(text);
  }
```
Full service code:
```typescript
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.
![Code coverage 1](https://i.ibb.co/5Txqbst/Zrzut-ekranu-2021-05-10-o-18-25-44.png)
I generated service with:
```typescript
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:
```typescript
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
* `snackBar` for listening to method calls

### Test: should send http call
```typescript
  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. 
```typescript
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.
```typescript
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`). 
```typescript
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
```typescript
  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:
```typescript
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
```typescript
  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:
![Code coverage 2](https://i.ibb.co/pR2SrHG/Zrzut-ekranu-2021-05-10-o-20-04-04.png)

[Link](https://github.com/TomWebwalker/notification-example) to repo.