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

[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

コメント

このブログの人気の投稿

[Angular][ASP.NET Core] Upload chunked files

Intro I wanted to send files to Web application (made by ASP.NET Core). If the file size had been small, I didn't need do any special things. But when I tried to send a large file, the error was occurred by ASP.NET Core's limitation. Though I could change the settings, but I didn't want to do that, because I hadn't known the file sizes what would been actually using. So I splitted the data into chunks first, and sent them. After receiving all chunks, I merged them into one file. There might be some libraries or APIs (ex. Stream API) what did them automatically, but I couldn't find them. What I did [ASP.NET Core] Make CORS enabled [Angular] Split a large file into chunks [Angular][ASP.NET Core] Send and receive data as form data [ASP.NET Core] Merge chunks into one file [ASP.NET Core] Make CORS enabled Because the client side application(Angular) and the server side application(ASP.NET Core) had been separated, I had to make CORS(Cross-Origin Requests)

[Nest.js] Use WebSocket with ws

Intro Until last time , I had used node-web-rtc to try WebRTC. But because the example was a little complicated for I understood the core functions of using WebRTC. So I look for other frameworks or libraries. PeerJS is a famous library for WebRTC. peers/peerjs: Peer-to-peer data in the browser. - GitHub peers/peerjs-server: Server for PeerJS - GitHub PeerJS - Simple peer-to-peer with WebRTC A problem is I don't know how to integrate to the Nest.js project. I couldn't find examples. So I don't choose at least this time. What shall I choose? According MDN, WebRTC doesn't specify strictly what technology is used on server application for connecting two devices. Signaling and video calling - Web APIs | MDN But in many examples include MDN's one use WebSocket. samples-server/s/webrtc-from-chat at master · mdn/samples-server · GitHub So I try WebSocket in the Nest.js project. Use WebSocket in a Nest.js project Nest.js has a function for using We

[Nest.js] Show static files

Intro I wanted to use Nest.js and WebRTC(node-webrtc). NestJS - A progressive Node.js framework Documentation | NestJS - A progressive Node.js framework And because I wanted to try with simple page(not use JavaScript frameworks), I added static HTML, CSS, JavaScript into a Nest.js project. Prepare Install First, I installed @nestjs/cli. First steps | NestJS - A progressive Node.js framework As same as last time , I couldn't do global install because I had used Volta. But I could installed by volta. volta install @nestjs/cli Create project nest new nest-web-rtc-sample volta pin node@12 Run npm start After doing "npm start", I could getting "Hello World!" from http://localhost:3000. Add static files I could add static files by two ways. @nestjs/serve-static First one of them was using "serve-static". Serve Static | NestJS - A progressive Node.js framework npm install --save @nestjs/serve-static And I needed adding a module into app.modu