ng mocks 库来测试表单组件(模板驱动表单)

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

我浏览了大量文档,包括他们自己的 ng-mocks 库 here。我对这个图书馆比较陌生。

PS:我知道像 spectator 这样的其他库可以这样做,或者使用普通的 jasmine / jest,但我正在尝试使用 ng-mocks 来查看它是如何完成的使用这个库。

eg: 有旁观者,这么容易写

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);
  })

对于 mat-select,我从他们的 github 回购问题 here

中找到了一个参考

是否有一种简单的方法来测试具有选择、单选按钮和输入的简单表单?这是一个如此普遍的要求,我期望一个没有太多麻烦的工作示例,但事实并非如此。我有一个非常简单的模板驱动表单

  <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>

和 class 文件

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();
      }
    )
  }

}

并且我正在尝试测试用户是否在输入字段中键入了任何内容,如果是,则获取它

这是我目前的测试用例。

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', () => {.. })
})

我原以为,在使用 ngMocks.change 键入元素、调用 detectChanges 并单击提交按钮后,表单提交将被触发,我将能够看到只是控制台中的标题值。

像这样 { title: 'cool cap', price: '', description: '', image: '', category: '' }

UUf!表格很难测试!!

这可能是使用 angular 默认测试框架对文本输入进行的简单测试。

html:

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

组件:

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();
});

这里是一些其他有用的测试函数:

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');

我联系了作者,他的回复很快。 这是工作答案

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');
  });
});

我深挖了一下,发现问题出在fixture.whenStable()的延迟调用上。当使用 FormModule 时,必须在 MockRender 之后立即调用它。

在这种情况下,MatInput可以删除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',
    }));
  });
});