ng mocks library to test form component ( template driven form )

1k views Asked by At

I have been going through a ton of documentations including their own ng-mocks library here . I am relatively new to this library.

PS: I know other libraries like spectator to do this, or using plain jasmine / jest, but i was trying the same using ng-mocks to see how its done using this library.

eg: with spectator, it is so easy to write this

it('should enter values on input fields and call the service method', () => {

    const service = spectator.inject(StoreService);
    const spy = service.addDataToDB.mockReturnValue(of({ id: 45 }));

    spectator.typeInElement('cool cap', byTestId('title'));
    spectator.typeInElement('33', byTestId('price'));
    spectator.typeInElement('try it out', byTestId('desc'));
    spectator.typeInElement('http://something.jpg', byTestId('img'));
    const select = spectator.query('#selectCategory') as HTMLSelectElement;
    spectator.selectOption(select, 'electronics');

    spectator.dispatchFakeEvent(byTestId('form'), 'submit');

    expect(spy).toHaveBeenCalledWith(mockAddForm);
  })

For mat-select i found a reference from their github repo issues here

Is there a simple way of testing a simple form that has selects, radio buttons and inputs? It is such a common requirement, that I expected a working example without much hassle, but that wasnt the case. I have a very simple template driven form

  <form #f="ngForm" (ngSubmit)="onSubmit(f)">

    <mat-form-field appearance="fill">
      <mat-label>Title</mat-label>
      <input data-testid="titleControl" name="title" ngModel matInput />
    </mat-form-field>

    <mat-form-field appearance="fill">
      <mat-label>Price</mat-label>
      <input data-testid="priceControl" name="price" ngModel matInput />
    </mat-form-field>

    <mat-form-field appearance="fill">
      <mat-label>Description</mat-label>
      <input data-testid="descControl" name="description" ngModel matInput />
    </mat-form-field>

    <mat-form-field appearance="fill">
      <mat-label>Image</mat-label>
      <input data-testid="imageControl" name="image" ngModel matInput />
    </mat-form-field>

    <mat-form-field appearance="fill">
      <mat-label>Select Category</mat-label>
      <mat-select data-testid="categoryControl" name="category" ngModel>
        <mat-option value="electronics">Electronics</mat-option>
        <mat-option value="jewelery">Jewelery</mat-option>
        <mat-option value="men's clothing">Men's clothing</mat-option>
        <mat-option value="women's clothing">Women's clothin</mat-option>
      </mat-select>

    </mat-form-field>
    <div class="submit-btn">
      <button type="submit" mat-raised-button color="primary">Submit</button>
    </div>

  </form>

and the class file

export class AddProductComponent implements OnInit {
  isAdded = false;
  @ViewChild('f') addForm: NgForm;

  constructor(private productService: ProductService) { }

  onSubmit(form: NgForm) {
    const product = form.value;
    this.productService.addProductToDB(product).subscribe(
      _data => {
        this.isAdded = true;
        this.addForm.resetForm();
      }
    )
  }

}

and I am trying to test if the user typed anything into the input field, and if so, get it

This is the test case I have so far.

import { EMPTY } from 'rxjs';
import { ProductService } from './../../services/product.service';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { AddProductComponent } from './add-product.component';
import { MockBuilder, MockInstance, MockRender, ngMocks } from 'ng-mocks';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatSelect } from '@angular/material/select';
import { MatOption } from '@angular/material/core';
import { Component, forwardRef } from '@angular/core';


describe('AddProductComponent', () => {
  beforeEach(() => {
    return MockBuilder(AddProductComponent)
      .keep(FormsModule)
      .mock(MatFormField)
      .mock(MatSelect)
      .mock(MatLabel)
      .mock(MatOption)
      .mock(ProductService, {
        addProductToDB: () => EMPTY
      })
  })
  it('should be defined', () => {
    const fixture = MockRender(AddProductComponent);
    expect(fixture.point.componentInstance).toBeDefined();
  })

 // THIS IS THE PLACE WHERE I GOT FULLY STUCK..

  it('should test the Title control', async () => {

    const fixture = MockRender(AddProductComponent);
    const component = fixture.componentInstance;

    const titleEl = ngMocks.find(['data-testid', 'titleControl']);
    ngMocks.change(titleEl, 'cool cap');
    fixture.detectChanges();
    await fixture.whenStable();
    const el = ngMocks.find(fixture, 'button');
    ngMocks.click(el);
    expect(component.addForm.value).toBe(...)
  })

  it('should test the image control', () => {.. })

  it('should test the price control', () => {.. })
})

i was expecting that, after using the ngMocks.change to type into the element, calling detectChanges and upon clicking the submit button, the form submit would have triggered and i will be able to see just the title value in the console.

Something like this { title: 'cool cap', price: '', description: '', image: '', category: '' }

UUf! forms are hard to test!!

3

There are 3 answers

0
satanTime On BEST ANSWER

I dug a bit deeper, and it turned out that the problem is with the late call of fixture.whenStable(). It has to be called right after MockRender when FormModule is used.

In this case, MatInput can be removed MockBuilder.

import {EMPTY} from 'rxjs';
import {ProductService} from './../../services/product.service';
import {AddProductComponent} from './add-product.component';
import {MockBuilder, MockRender, ngMocks} from 'ng-mocks';
import {AppModule} from "../../app.module";
import {FormsModule} from "@angular/forms";

ngMocks.defaultMock(ProductService, () => ({
  addProductToDB: () => EMPTY,
}));

describe('AddProductComponent', () => {
  beforeEach(() => MockBuilder(AddProductComponent, AppModule).keep(FormsModule));

  it('should be defined', () => {
    const fixture = MockRender(AddProductComponent);
    expect(fixture.point.componentInstance).toBeDefined();
  })

  it('should test the Title control', async () => {
    const fixture = MockRender(AddProductComponent);

    await fixture.whenStable(); // <- should be here.

    const component = fixture.point.componentInstance;

    // default
    expect(component.addForm.value).toEqual(expect.objectContaining({
      title: '',
    }));

    const titleInputEl = ngMocks.find(['data-testid', 'titleControl']);
    ngMocks.change(titleInputEl, 'cool cap');

    // updated
    expect(component.addForm.value).toEqual(expect.objectContaining({
      title: 'cool cap',
    }));
  });
});
1
AmirHossein Rezaei On

This could be a simple test for text input with angular default test framework.

html:

<input type="text" class="my-simple-input" [(ngModel)]="username"> 

component:

public username:string = '';

component.spec.ts:

import { By } from '@angular/platform-browser';

let component: MyCustomComponent;
let fixture: ComponentFixture<MyCustomComponent>;

beforeEach(() => {
 fixture = TestBed.createComponent(MyCustomComponent);
 component = fixture.componentInstance;
 fixture.detectChanges();
});

it('testing two way binding', () => {
 const textInput = fixture.debugElement.query(By.css('input[type="text"]')).nativeElement as HTMLInputElement;
 textInput.value= "usernameValue";
 fixture.detectChanges();

 expect(component.username === "usernameValue").toBeTruthy();
});

it('testing two way binding 2', () => {
 component.username= "usernameValue";
 fixture.detectChanges();
 const textInput = fixture.debugElement.query(By.css('input[type="text"]')).nativeElement as HTMLInputElement;

 expect(textInput.value === "usernameValue").toBeTruthy();
});

Here is some other useful testing functions:

const button = fixture.debugElement.query(By.css('app-custom-button')).nativeElement;

const element: MockCustomDropdownComponent = fixture.debugElement.query(By.css('app-custom-dropdown')).componentInstance;

const sourceRadios = fixture.debugElement.nativeElement.querySelectorAll('.source-radio');
0
Harikrishnan KayKay On

I had reached out to the author who gave a quick awesome response. Here is the working answer

import { EMPTY } from 'rxjs';
import { ProductService } from './../../services/product.service';
import { AddProductComponent } from './add-product.component';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';
import { AppModule } from "../../app.module";
import { FormsModule } from "@angular/forms";
import { MatInput } from "@angular/material/input";

ngMocks.defaultMock(ProductService, () => ({
  addProductToDB: () => EMPTY,
}));

describe('AddProductComponent', () => {
  beforeEach(() => MockBuilder(AddProductComponent, AppModule)
    .keep(FormsModule)
    .keep(MatInput));

  it('should be defined', () => {
    const fixture = MockRender(AddProductComponent);
    expect(fixture.point.componentInstance).toBeDefined();
  })

  it('should test the Title control', () => {
    const fixture = MockRender(AddProductComponent);
    const component = fixture.point.componentInstance;

    const titleInputEl = ngMocks.find(['data-testid', 'titleControl']);
    ngMocks.change(titleInputEl, 'cool cap');
    expect(component.addForm.value.title).toBe('cool cap');
  });
});