スキップしてメイン コンテンツに移動

[Angular] Test Components

Intro

This time, I tested a component of Angular. 
[Angular]Test with Azure DevOps

Sample

Test target was here.

first-page.component.html


<p>first-page works!</p>
<div id="product-list" *ngFor="let p of products">
    <div class="product-id">ID: {{p.id}}</div>
    <div class="product-name">NAME: {{p.name}}</div>
</div>
<button id="load-button" (click)="loadProducts()"></button>
<input id="input-file" type="file" (change)="fileChanged($event.target)" >
<button id="save-button" (click)="saveFile()">Save</button>

first-page.component.ts


import { Component, OnInit } from '@angular/core';
import { FirstPageService } from './first-page.service';
import { Product } from '../products/product';
import { FileUploadService } from '../file-upload/file-upload.service';

@Component({
  selector: 'app-first-page',
  templateUrl: './first-page.component.html',
  styleUrls: ['./first-page.component.css']
})
export class FirstPageComponent implements OnInit {
  public products: Product[] = [];
  private uploadFile: File;
  constructor(private firstPageService: FirstPageService,
      private fileUploaderService: FileUploadService) { }

  ngOnInit() {
  }
  public loadProducts() {
    this.firstPageService
      .getProducts()
      .subscribe(p => {
        this.products = [];
        this.products.push(...p);
      });
  }
  public fileChanged(target: EventTarget) {
    const fileElement = target as HTMLInputElement;
    if (fileElement == null ||
      fileElement.files == null ||
      fileElement.files.length <= 0) {
      return;
    }
    // the first file is set as upload target.
    this.uploadFile = fileElement.files[0];
  }
  public saveFile() {
    if (window.confirm('Upload file?') === false) {
      return;
    }
    if (this.uploadFile == null) {
      return;
    }
    this.fileUploaderService.upload(this.uploadFile)
      .subscribe(result => console.log(result), // onNext
        error => console.error(error),           // onError
        () => alert('finished'));         // onComplete
  }
}

product.ts


export type Product = {
    id: number,
    name: string,
};

upload-result.ts


export type UploadResult = {
    succeeded: boolean,
    errorMessage: string
};

This component had 2 functions.
  1. Getting "Product" list and putting into DOM
  2. Uploading files after confirming
And it depended on 2 services.
  1. FirstPageService: Getting "Product" list
  2. FileUploadService: Uploading files

Stub

First, I added stubs of the services to resolve dependencies and return dummy data.

first-page.component.spec.ts


import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { FirstPageComponent } from './first-page.component';
import { FirstPageService } from './first-page.service';
import { of } from 'rxjs';
import { By } from '@angular/platform-browser';
import { FileUploadService } from '../file-upload/file-upload.service';

describe('FirstPageComponent', () => {
  let component: FirstPageComponent;
  let fixture: ComponentFixture<FirstPageComponent>;
  let firstPageService: FirstPageService;
  let fileUploadService: FileUploadService;

  beforeEach(async(() => {
    const firstPageServiceStub: Partial<FirstPageService> = {
      getProducts: () => {
        return of([
          { id: 1, name: 'hello' }
        ]);
      }
    };
    const fileUploadServiceStub: Partial<FileUploadService> = {
      upload: (file) => of({ succeeded: true, errorMessage: '' })
    };
    TestBed.configureTestingModule({
      declarations: [ FirstPageComponent ],
      providers: [{ provide: FirstPageService, useValue: firstPageServiceStub },
          { provide: FileUploadService, useValue: fileUploadServiceStub }],
    })
    .compileComponents();
  }));

  beforeEach(() => {
    // firstPageServiceStub was set
    firstPageService = TestBed.get(FirstPageService);
    // print [{ id: 1, name: 'hello' }]
    firstPageService.getProducts()
        .subscript(p => console.log(p));

    // fileUploadServiceStub was set
    fileUploadService = TestBed.get(FileUploadService);
    // firstPageServiceStub and fileUploadServiceStub were set as the services
    fixture = TestBed.createComponent(FirstPageComponent);

    component = fixture.componentInstance;
    fixture.detectChanges();
  });
});

When I needed calling before testing, I could create instances of the services like above.

Get DOM by CSS ID

For testing, I wanted to get DOM. I could get them by some ways.

first-page.component.spec.ts


...
describe('FirstPageComponent', () => {
...
  let loadButton: HTMLButtonElement;
  let saveButton: HTMLButtonElement;

  beforeEach(async(() => {
...
  }));

  beforeEach(() => {
...
    fixture = TestBed.createComponent(FirstPageComponent);
    component = fixture.componentInstance;
    
    // 1. fixture.debugElement.query + By.css
    loadButton = fixture.debugElement.query(By.css('#load-button')).nativeElement;
    // 2. fixture.nativeElement.querySelector
    saveButton = fixture.nativeElement.querySelector('#save-button') as HTMLButtonElement;
    // 3. document.getElementById (I shouldn't use because I couldn't get any changes of fixture)
    saveButton = document.getElementById('save-button') as HTMLButtonElement;
    
    fixture.detectChanges();
  });
});

An important thing was if I changed DOM, I had to call "fixture.detectChanges();" or the changes wouldn' be reflected. So some of the DOM what were created by DOM events had to be gotten their instances in test cases.

Add test cases

Call click event and test DOM values

first-page.component.spec.ts


...
  it('should load and set products', () => {
    loadButton.click();
    fixture.detectChanges();
    productList = fixture.debugElement.query(By.css('#product-list')).nativeElement;
    expect(productList.childElementCount).toEqual(2);
    productList.childNodes.forEach(n => {
      const childElement = n as HTMLElement;
      switch(childElement.className) {
        case 'product-id':
          expect(childElement.innerText).toEqual('ID: 1');
          break;
        case 'product-name':
          expect(childElement.innerText).toEqual('NAME: hello');
          break;
      }
    });
  });
...

Spy alerts

To avoid showing window.confirm, or getting alerms in tests, I could use "spyOn".

first-page.component.spec.ts


...
  it('should not show alert when upload file is null', () => {
    spyOn(window, 'confirm');
    spyOn(window, 'alert');
    component.saveFile();
    expect(window.alert).not.toHaveBeenCalled();
  });
...

Set files into an HTMLInputElement

first-page.component.spec.ts

Although I wanted to set files into an HTMLInputElement, I couldn't do this.

const file = new File([new Blob(['test'])], 'sample.txt');
const fileList: FileList = {
  0: file,
  length: 1,
  item: (index) => file,
};
const inputElement = document.createElement('input');
inputElement.type = 'file';
// Error
inputElement.files = fileList;

To do that, I used "Object.setPrototypeOf" to overwite "files" property.

  it('show alert after file uploading', () => {
    spyOn(window, 'confirm');
    spyOn(window, 'alert');
    
    const file = new File([new Blob(['test'])], 'sample.txt');
    const fileList: FileList = {
      0: file,
      length: 1,
      item: (index) => file,
    };
    Object.setPrototypeOf(fileList, Object.create(window.FileList.prototype));
    const inputElement = document.createElement('input');
    inputElement.type = 'file';
    
    Object.defineProperty(inputElement, 'files', {
      value: fileList,
      writable: false,
    });
    
    component.fileChanged(inputElement);
    component.saveFile();
    expect(window.alert).toHaveBeenCalled();
  });

Resources

Angular - Testing

コメント

このブログの人気の投稿

[PostgreSQL] Play with TypeORM 1

Intro This time, I tried accessing Database by TypeORM. Because I wanted to manage Database tables by ORM, I created a table first.  GitHub - typeorm/typeorm   TypeORM - Amazing ORM for TypeScript and JavaScript (ES7, ES6, ES5). Installation and creating a project Installation TypeORM needed "reflect-metadata" and database driver. npm install --save typeorm reflect-metadata pg typescript tsc npm install --save-dev @types/node And I also installed "ts-node" to skip compiling. npm install --save ts-node Preparing References I could generate a TypeORM project by TypeORM command. npx typeorm init --name gen-typeorm-sample --database postgres I refered Most of all settings from it. Adding files and folders First, I added tsconfig.json. npx tsc --init And I edited like below. tsconfig.json { "compilerOptions": { /* Basic Options */ "incremental": true, "target": "es5", "module": "comm...

[TypeScript] Try React 1

Intro This time I tried React. First, I referred Documents and Tutorial. React – A JavaScript library for building user interfaces GitHub - facebook/react: A declarative, efficient, and flexible JavaScript library for building user interfaces. Getting started with React - Learn web development | MDN Create a project To create a new React project, I used "Create React App". GitHub - facebook/create-react-app: Set up a modern web app by running one command. It created these files. .git node_modules public L favicon.ico L index.html L logo192.png L logo512.png L manifest.json L robots.txt src L App.css L App.test.tsx L App.tsx L index.css L index.test.tsx L index.tsx L logo.svg L react-app-env.d.ts L serviceWorker.ts L setupTests.ts .gitignore package.json package-lock.json README.md tsconfig.json Make the page work with IE11 I could view the page by "npm start" or "npm run start".   ...

[Angular 10 + NgRx + Nest.js] Create project

Intro This time, I added server-side. I choosed Nest.js because I love TypeScript and I had been interested in using TypeScript in server-side programming. Environments Angular ver.10.0.2 @ngrx/store ver.9.2.0 @ngrx/effects ver.9.2.0 @nestjs/ng-universal ver.2.0.1 Couldn't add @ngrx/store into the Angular 10 project First, I created an Angular project and tried adding @ngrx/store into it. And I got an error. Installing packages for tooling via npm. Installed packages for tooling via npm. The package that you are trying to add does not support schematics. You can try using a different version of the package or contact the package author to add ng-add support. According to the issue below, I should add "@latest". npx ng add @ngrx/store@latest Because if I didn't add it, the @ngrx/store version was too low.    cannot use ng add to install ngrx store · Issue #2604 · ngrx/platform · GitHub Though I didn't know I should add "@latest" into ...