Telerik blogs
Angular_870x220

Learn how to create a dynamic form in Angular and then create tests for the form to ensure it works as expected.

This article will cover testing of dynamic forms in Angular. Dynamic forms in Angular are forms that are created using reactive form classes like Form Group and Form Controls. We will write tests for these forms to ensure that they function as intended.

For this article, we’ll be testing a sign-up form. The form is generated dynamically by passing an array of objects describing the input elements to the component; then a FormControl will be generated for each element before the form is grouped using FormGroup.

To get started, you have to bootstrap an Angular project using the CLI. To follow this tutorial a basic understanding of Angular is required. Please ensure that you have Node and npm installed before you begin. If you have no prior knowledge of Angular, kindly follow the tutorial here. Come back and finish this tutorial when you’re done.

Initializing Application

To get started, we will use the CLI (command line interface) provided by the Angular team to initialize our project.

First, install the CLI by running npm install -g @angular/cli. npm is a package manager used for installing packages. It will be available on your PC if you have Node installed, if not, download Node here.

To create a new Angular project using the CLI, open a terminal and run:
ng new dynamic-form-tests

Enter into the project folder and start the Angular development server by running ng serve in a terminal in the root folder of your project.

Creating Sign-up Form

To get started, we’ll set up the sign-up form to get ready for testing. The form itself will be rendered by a component separate from the App component. Run the following command in a terminal within the root folder to create the component:

    ng generate component dynamic-form

Open the dynamic-form.component.html file and copy the following content into it:

    <!-- src/app/dynamic-form/dynamic-form.component.html -->
    
    <form [formGroup]="form" (submit)="onSubmit()">
      <div *ngFor="let element of formConfig">
        <div [ngSwitch]="element.inputType">
          <label [for]="element.id">{{ element.name }}</label
          ><span *ngIf="element?.required">*</span>
          <br />
          <div *ngSwitchCase="'input'">
            <div *ngIf="element.type === 'radio'; else notRadio">
              <div *ngFor="let option of element.options">
                <input
                  [type]="element.type"
                  [name]="element.name"
                  [id]="option.id"
                  [formControlName]="element.name"
                  [value]="option.value"
                />
                <label [for]="option.id">{{ option.label }}</label
                ><span *ngIf="element?.required">*</span>
              </div>
            </div>
            <ng-template #notRadio>
              <input
                [type]="element.type"
                [id]="element.name"
                [formControlName]="element.name"
              />
            </ng-template>
          </div>
          <select
            [name]="element.name"
            [id]="element.id"
            *ngSwitchCase="'select'"
            [formControlName]="element.name"
          >
            <option [value]="option.value" *ngFor="let option of element.options">{{
              option.label
            }}</option>
          </select>
        </div>
      </div>
      <button>Submit</button>
    </form>

We use the ngSwitch binding to check for the input type before rendering. The inputType of the select element is different, so it is rendered differently using the *ngSwitchCase binding. You can add several inputTypes and manage them using the *ngSwitchCase. The file input element, for example, might be rendered differently from the other input elements. In that case, the inputType specified can be file.

For each input element, we add a formControlName directive which takes the name property of the element. The directive is used by the formGroup to keep track of each FormControl value. The form element also takes formGroup directive, and the form object is passed to it.

Let’s update the component to generate form controls for each input field and to group the elements using the FormGroup class. Open the dynamic-form.component.ts file and update the component file to generate form controls for each input and a form group.

    // src/app/dynamic-form/dynamic-form.component.ts
    
    import { Component, OnInit, Input } from '@angular/core';
    import { FormControl, FormGroup } from '@angular/forms'
    @Component({
      selector: 'app-dynamic-form',
      templateUrl: './dynamic-form.component.html',
      styleUrls: ['./dynamic-form.component.css']
    })
    export class DynamicFormComponent implements OnInit {
      constructor() { }
      @Input()formConfig = []
      form: FormGroup;
      userGroup = {};
    
      onSubmit(){
        console.log(this.form.value);
      }
      ngOnInit(){
        for(let config of this.formConfig){
          this.userGroup[config.name] = new FormControl(config.value || '')
        }
        this.form = new FormGroup(this.userGroup);
      }
    }

The component will take an Input (formConfig) which will be an array of objects containing information about each potential input. In the OnInit lifecycle of the component, we’ll loop through the formConfig array and create a form control for each input using the name and value properties. The data will be stored in an object userGroup, which will be passed to the FormGroup class to generate a FormGroup object (form).

Finally, we’ll update the app.component.html file to render the dynamic-form component and also update the app.component.ts file to create the formConfig array:

    <-- src/app/app.component.html -->
    
    <section>
        <app-dynamic-form [formConfig]="userFormData"></app-dynamic-form>
    </section>

Next is the component file. Open the app.component.ts file and update it with the snippet below:

    import { Component, OnInit } from '@angular/core';
    @Component({
      selector: 'my-app',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      userFormData = [
        {
          name: 'name',
          value: '',
          type: 'text',
          id: 'name',
          inputType: 'input',
          required: true
        }, {
          name: 'address',
          value: '',
          type: 'text',
          id: 'address',
          inputType: 'input',
        }, {
          name: 'age',
          value: '',
          type: 'number',
          id: 'age',
          inputType: 'input',
        }, {
          name: 'telephone',
          value: '',
          type: 'tel',
          id: 'telephone',
          inputType: 'input',
        }, {
          name: 'sex',
          type: 'radio',
          inputType: 'input',
          options: [
            {
              id: 'male',
              label: 'male',
              value: 'male'
            },
            {
              id: 'female',
              label: 'female',
              value: 'female'
            }
          ]
        }, {
          name: 'country',
          value: '',
          type: '',
          id: 'name',
          inputType: 'select',
          options: [
            {
              label: 'Nigeria',
              value: 'nigeria'
            },
            {
              label: 'United States',
              value: 'us'
            },
            {
              label: 'UK',
              value: 'uk'
            }
          ]
        },
      ]
    }

The userForm array contains objects with properties like type , value, name. These values will be used to generate appropriate fields on the view. This lets us add more input fields in the template without manually updating the template. This array is passed to the dynamic-form component.

Don’t forget that to use Reactive Forms, you have to import the ReactiveFormsModule. Open the app.module.ts file and update it to include the ReactiveFormsModule:

    // other imports ...
    import { FormsModule, ReactiveFormsModule } from '@angular/forms';
    
    @NgModule({
      imports:      [
        // ...other imports 
        ReactiveFormsModule 
      ],
      //...
    })
    export class AppModule { }

Testing the Form

When generating components, Angular generates a spec file alongside the component for testing. Since we’ll be testing the dynamic-form component, we’ll be working with the dynamic-form.component.spec.ts file.

The first step is to set up the test bed for the component. Angular already provides a boilerplate for testing the component, and we’ll simply extend that. Open the dynamic-form.component.spec.ts and update the test bed to import the ReactiveFormsModule that the component depends on:

    import { async, ComponentFixture, TestBed } from '@angular/core/testing';
    import { ReactiveFormsModule } from '@angular/forms';
    import { DynamicFormComponent } from './dynamic-form.component';
    
    describe('DynamicFormComponent', () => {
      let component: DynamicFormComponent;
      let fixture: ComponentFixture<DynamicFormComponent>;
    
      beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [ DynamicFormComponent ],
          imports: [ ReactiveFormsModule ],
        })
        .compileComponents();
      }));
    
      beforeEach(() => {
        fixture = TestBed.createComponent(DynamicFormComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
      });
    
      it('should create', () => {
        expect(component).toBeTruthy();
      });
    });

We’ll be testing our form using the following cases:

  • Form rendering: here, we’ll check if the component generates the correct input elements when provided a formConfig array.
  • Form validity: we’ll check that the form returns the correct validity state
  • Input validity: we’ll check if the component responds to input in the view template
  • Input errors: we’ll test for errors on the required input elements.

To begin testing, run the following command in your terminal: yarn test or npm test

Form Rendering

For this test, we’ll pass an array of objects containing data about the input elements we wish to create, and we’ll test that the component renders the correct elements. Update the component’s spec file to include the test:

    describe('DynamicFormComponent', () => {
      // ... test bed setup
    
    beforeEach(() => {
        fixture = TestBed.createComponent(DynamicFormComponent);
        component = fixture.componentInstance;
        component.formConfig = [
          {
            name: 'name',
            value: '',
            type: 'text',
            id: 'name',
            inputType: 'input',
            required: true
          }, {
            name: 'address',
            value: '',
            type: 'text',
            id: 'address',
            inputType: 'input',
          },
        ]
        component.ngOnInit();
        fixture.detectChanges();
      });
    
      it('should render input elements', () => {
        const compiled = fixture.debugElement.nativeElement;
        const addressInput = compiled.querySelector('input[id="address"]');
        const nameInput = compiled.querySelector('input[id="name"]');
    
        expect(addressInput).toBeTruthy();
        expect(nameInput).toBeTruthy();
      });
    });

We updated the test suite with the following changes:

  1. We assigned an array to the formConfig property of the component. This array will be processed in the OnInit lifecycle to generate form controls for the input elements and then a form group.
  2. Then we triggered the ngOnInit lifecycle. This is done manually because Angular doesn’t do this in tests.
  3. As we’ve made changes to the component, we have to manually force the component to detect changes. Thus, the detectChanges method is triggered. This method ensures the template is updated in response to the changes made in the component file.
  4. We get the compiled view template from the fixture object. From there, we’ll check for the input elements that should have been created by the component. We expected two components — an address input and a name input.
  5. We’ll check if the elements exist using the toBeTruthy method.

Form Validity

For this test, we’ll check for the validity state of the form after updating the values of the input elements. For this test, we’ll update the values of the form property directly without accessing the view. Open the spec file and update the test suite to include the test below:

    it('should test form validity', () => {
        const form = component.form;
        expect(form.valid).toBeFalsy();
    
        const nameInput = form.controls.name;
        nameInput.setValue('John Peter');
    
        expect(form.valid).toBeTruthy();
      })

For this test, we’re checking if the form responds to the changes in the control elements. When creating the elements, we specified that the name element is required. This means the initial validity state of the form should be INVALID, and the valid property of the form should be false.

Next, we update the value of the name input using the setValue method of the form control, and then we check the validity state of the form. After providing the required input of the form, we expect the form should be valid.

Input Validity

Next we’ll check the validity of the input elements. The name input is required, and we should test that the input acts accordingly. Open the spec file and add the spec below to the test suite:

    it('should test input validity', () => {
        const nameInput = component.form.controls.name;
        const addressInput = component.form.controls.address;
    
        expect(nameInput.valid).toBeFalsy();
        expect(addressInput.valid).toBeTruthy();
    
        nameInput.setValue('John Peter');
        expect(nameInput.valid).toBeTruthy();
    })

In this spec, we are checking the validity state of each control and also checking for updates after a value is provided.

Since the name input is required, we expect its initial state to be invalid. The address isn’t required so it should be always be valid. Next, we update the value of the name input, and then we test if the valid property has been updated.

Input Errors

In this spec, we’ll be testing that the form controls contain the appropriate errors; the name control has been set as a required input. We used the Validators class to validate the input. The form control has an errors property which contains details about the errors on the input using key-value pairs.

s_B627623ACECF3DCB4B58A57BA31AF692B356F096E54A587B9792CBF5F5D10C9C_1551623628778_Screenshot+2019-03-03+at+3.33.33+PM

The screenshot above shows an example of how a form control containing errors looks. For this spec, we’ll test that the required name input contains the appropriate errors. Open the dynamic-form.component.spec.ts file and add the spec below to the test suite:

    it('should test input errors', () => {
        const nameInput = component.form.controls.name;
        expect(nameInput.errors.required).toBeTruthy();
    
        nameInput.setValue('John Peter');
        expect(nameInput.errors).toBeNull();
    });

First, we get the name form control from the form form group property. We expect the initial errors object to contain a required property, as the input’s value is empty. Next, we update the value of the input, which means the input shouldn’t contain any errors, which means the errors property should be null.

If all tests are passing, it means we’ve successfully created a dynamic form. You can push more objects to the formConfig array and add a spec to test that a new input element is created in the view.

Conclusion

Tests are vital when programming because they help detect issues within your codebase that otherwise would have been missed. Writing proper tests reduces the overhead of manually testing functionality in the view or otherwise. In this article, we’ve seen how to create a dynamic form and then we created tests for the form to ensure it works as expected.


One More Thing: End-to-End UI Test Automation Coverage

On top of all unit, API, and other functional tests that you create, it is always a good idea to add stable end-to-end UI test automation to verify the most critical app scenarios from the user perspective. This will help you prevent critical bugs from slipping into production and will guarantee superb customer satisfaction.

Even if a control is fully tested and works well on its own, it is essential to verify if the end product - the combination of all controls and moving parts - is working as expected. This is where the UI functional automation comes in handy. A great option for tooling is Telerik Test Studio. This is a web test automation solution that enables QA professionals and developers to craft reliable, reusable, and maintainable tests.


About the Author

Christian Nwamba

Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.

Related Posts

Comments

Comments are disabled in preview mode.