Angular8 で Web アプリを作ろう - Jasmine - Serviceのテスト
今日すること
こんにちは、ふるてつです。
前回は基本に立ち返る意味で簡単なマスタメンテ画面を作ってみました。
今回はさらにユニットテストをこの画面に追加していきたいと思います。
AngularのテストツールはJasmine
が標準となっていますので、それをわたしも使用したいと思います。
ちなみにテストランナーはKarma
が標準となります。
わたしはJavaScriptのユニットテストは初めてなので、結構な時間がかかりました。
今日はまずはServiceのユニットテストについて書いてみます。
まずテストを実行
まずはng test
もしくはnpm test
コマンドでテストを実行してみます。
いきなり最初からエラーが出ました。
10% building 3/3 modules 0 active06 08 2019 21:14:10.755:WARN [karma]: No captured browser, open http://localhost:9876/
06 08 2019 21:14:10.770:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
06 08 2019 21:14:10.771:INFO [launcher]: Launching browser Chrome with unlimited concurrency
10% building 5/6 modules 1 active ...ront\sanrokumaru\src sync /\.spec\.ts$/06 08 2019 21:14:10.890:INFO [launcher]: Starting browser Chrome
92% additional asset processing scripts-webpack-plugin× 「wdm」: Error: ENOENT: no such file or directory, open 'C:\papa\00.default\01.develop\Sanrokumaru\02_front\sanrokumaru\node_modules\jquery\dist\jquery.slim.min.js'
at Object.openSync (fs.js:447:3)
at Object.readFileSync (fs.js:349:35)
at Storage.provideSync (C:\papa\00.default\01.develop\Sanrokumaru\02_front\sanrokumaru\node_modules\enhanced-resolve\lib\CachedInputFileSystem.js:98:13)
at CachedInputFileSystem.readFileSync (C:\papa\00.default\01.develop\Sanrokumaru\02_front\sanrokumaru\node_modules\enhanced-resolve\lib\CachedInputFileSystem.js:259:32)
~ 中略 ~
at C:\papa\00.default\01.develop\Sanrokumaru\02_front\sanrokumaru\node_modules\webpack\lib\Compilation.js:1171:4
at AsyncSeriesHook.lazyCompileHook (C:\papa\00.default\01.develop\Sanrokumaru\02_front\sanrokumaru\node_modules\tapable\lib\Hook.js:154:20) at Compilation.finish (C:\papa\00.default\01.develop\Sanrokumaru\02_front\sanrokumaru\node_modules\webpack\lib\Compilation.js:1163:28)
06 08 2019 21:15:10.893:WARN [launcher]: Chrome have not captured in 60000 ms, killing.
06 08 2019 21:15:11.099:INFO [launcher]: Trying to start Chrome again (1/2).
06 08 2019 21:16:11.102:WARN [launcher]: Chrome have not captured in 60000 ms, killing.
06 08 2019 21:16:11.299:INFO [launcher]: Trying to start Chrome again (2/2).
06 08 2019 21:17:11.304:WARN [launcher]: Chrome have not captured in 60000 ms, killing.
06 08 2019 21:17:11.488:ERROR [launcher]: Chrome failed 2 times (timeout). Giving up.
原因をネットで調べるとKarma
やJasmine
のバージョンが低いのではないか?
と思い、npm outdated
コマンドで最新のバージョンを確認しました。
npm outdated
Package Current Wanted Latest Location
@angular-devkit/build-angular 0.800.4 0.800.6 0.802.1 sanrokumaru
@angular/animations 8.0.2 8.2.1 8.2.1 sanrokumaru
@angular/cdk 8.0.1 8.1.2 8.1.2 sanrokumaru
@angular/cli 8.0.4 8.2.1 8.2.1 sanrokumaru
@angular/common 8.0.2 8.2.1 8.2.1 sanrokumaru
@angular/compiler 8.0.2 8.2.1 8.2.1 sanrokumaru
@angular/compiler-cli 8.0.2 8.2.1 8.2.1 sanrokumaru
@angular/core 8.0.2 8.2.1 8.2.1 sanrokumaru
@angular/forms 8.0.2 8.2.1 8.2.1 sanrokumaru
@angular/language-service 8.0.2 8.2.1 8.2.1 sanrokumaru
@angular/material 8.0.1 8.1.2 8.1.2 sanrokumaru
@angular/material-moment-adapter 8.0.1 8.1.2 8.1.2 sanrokumaru
@angular/platform-browser 8.0.2 8.2.1 8.2.1 sanrokumaru
@angular/platform-browser-dynamic 8.0.2 8.2.1 8.2.1 sanrokumaru
@angular/router 8.0.2 8.2.1 8.2.1 sanrokumaru
@types/jasmine 2.8.16 2.8.16 3.4.0 sanrokumaru
@types/node 8.9.5 8.9.5 12.7.1 sanrokumaru
angular-in-memory-web-api 0.6.1 0.6.1 0.8.0 sanrokumaru
core-js 2.6.4 2.6.9 3.2.0 sanrokumaru
jasmine-core 2.99.1 2.99.1 3.4.0 sanrokumaru
karma 1.7.1 1.7.1 4.2.0 sanrokumaru
karma-chrome-launcher 2.2.0 2.2.0 3.0.0 sanrokumaru
karma-coverage-istanbul-reporter 2.0.4 2.1.0 2.1.0 sanrokumaru
karma-jasmine 1.1.2 1.1.2 2.0.1 sanrokumaru
karma-jasmine-html-reporter 0.2.2 0.2.2 1.4.2 sanrokumaru
npm 6.9.0 6.10.3 6.10.3 sanrokumaru
ts-node 5.0.1 5.0.1 8.3.0 sanrokumaru
tslib 1.9.3 1.10.0 1.10.0 sanrokumaru
tslint 5.9.1 5.9.1 5.18.0 sanrokumaru
typescript 3.4.5 3.4.5 3.5.3 sanrokumaru
zone.js 0.9.1 0.9.1 0.10.1 sanrokumaru
最新化
わたしのローカルはKarma
やJasmin-core
などが軒並み低いようです。
Angularの4か5の頃にこのプロジェクトを作ったので、その時から更新されずに古いまま残っていたのかもしれません。
Angular CLIやcoreだけのバージョンを先にあげようとしたのですが、
そうこうしているうちにng update --all --force
を流してしまい、一気に全部のバージョンを上げてしまいました。
そもそも起動しなくなるのではと一瞬ドキっとしましたが無事でした、結果オーライというところでしょうか。
BootStrapの名残りを除去
あともう1か所Angular.json
ファイルのtestの所に昔使っていたbootstrap
の項目がまだ残っていました。
これらも邪魔していたようです、きれいに消しました。
これでテストが動くようになりました。
現状のテストをすべて無効化
テストは動くようになりましたが、下記のような状態で17/34が失敗とのこと。
自動で作られたコードでも失敗するんですね。
そこでいったんすべてのテストコードを実行しないように、各テストクラス先頭のdescribe
の部分をxdescribe
に変更しました。
そして一つずつx
を外しながらテストコードを追加/修整していくことにしました、最初はcompanyServiceです。
companyServiceはサーバからhttpCientで会社関連のデータを取得・新規作成・更新をするクラスです。
Componentよりも簡単で、テストコードを書きやすそうに思えたので選びました。
Serviceのテスト(CompanyService)
まずはテスト対象のCompanyServiceですが下記のようなコードになります、1メソッドのみ掲載します。
company.service.tsの内容
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AppConst } from 'src/app/app-const';
import { SearchCompanyListDto } from 'src/app/entity/company/search-company-list-dto';
import { ErrorMessageService } from 'src/app/service/message/error-message.service';
import { environment } from 'src/environments/environment';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CompanyDto } from 'src/app/entity/company/company-dto';
@Injectable({
providedIn: 'root'
})
export class CompanyService {
private server = environment.production ? AppConst.URL_PROD_SERVER : AppConst.URL_DEV_SERVER;
constructor(
private http: HttpClient,
private errorMessageService: ErrorMessageService,
private readonly translateService: TranslateService
) { }
public getCompanyList(httpParams: HttpParams): Observable {
const webApiUrl: String = 'company-list';
return this.http.get(this.server + webApiUrl, { params: httpParams })
.pipe(
catchError(error => {
this.errorMessageService.add(this.translateService.instant('errMessage.http'));
return of(null as any);
})
);
}
~ 中略 ~
}
下記はデフォルトで作成されていたテストコードです。
company.service.spec.tsの内容
import { TestBed } from '@angular/core/testing';
import { CompanyService } from './company.service';
describe('CompanyService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: CompanyService = TestBed.get(CompanyService);
expect(service).toBeTruthy();
});
});
このテストコード、ネットで意味を調べたりしばらく公式リファレンスを見ているとわかってきました。
describe
はお約束で必ず書く。
beforeEach
は毎回実行されるコード、junitでいうところのsetUp
や@Before
のような感じと思いました。
TestBed
はAngularのテスト用モジュールを作成する機能のようです。
テストコードの修正その1
import { TestBed } from '@angular/core/testing';
import { CompanyService } from './company.service';
import { HttpClient } from '@angular/common/http';
import { ErrorMessageService } from 'src/app/service/message/error-message.service';
import { TranslateService } from '@ngx-translate/core';
describe('CompanyService', () => {
// beforeEach(() => TestBed.configureTestingModule({}));
let service: CompanyService;
beforeEach(() => {
service = new CompanyService(
new HttpClient(null),
new ErrorMessageService(),
new TranslateService(null, null, null, null, null));
});
it('should be created', () => {
expect(service).toBeTruthy();
// const service: CompanyService = TestBed.get(CompanyService);
// expect(service).toBeTruthy();
});
});
今回テストを修整するにあたっては、公式リファレンスの「開発ワークフロー」→「テスト」→「サービスのテスト」を参考にしました。
https://angular.jp/guide/testing#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88
ここのページは分量が多いためか重くてなかなか思うようにスクロール操作ができませんお気を付けを、わたしはのんびり見ましたけど。
ここではTestBed
では書いていなく、普通にクラスをnewしていましたので、そちらにわたしも寄せました。
Javaのjunitに近い感覚で書けそうだという理由もあります。
テストコードについては初期化に失敗していますので、呼び出し時の引数が不足していると思います。
そこでCompanyServiceをnewするときにHttpClient、ErrorMessageService、TranslateServiceの3つのサービスを引数にいれます。
これでとりあえずはテストが通るようになりました。
テストコードの修正その2
次はHttpClientとTranslateServiceをモックに差替えて、#getCompanyListメソッドをテストします。
もう一つ独自に作ったErrorMessageServiceはモックにする必要がありませんでした。
import { CompanyService } from './company.service';
import { ErrorMessageService } from 'src/app/service/message/error-message.service';
import { SearchCompanyListDto } from 'src/app/entity/company/search-company-list-dto';
import { SearchCompanyDto } from 'src/app/entity/company/search-company-dto';
import { HttpErrorResponse } from '@angular/common/http';
import { asyncError, asyncData } from 'src/app/testing/async-observable-helpers';
import { CompanyDto } from 'src/app/entity/company/company-dto';
describe('CompanyService', () => {
let companyService: CompanyService;
let httpClientSpy: { get: jasmine.Spy, post: jasmine.Spy, put: jasmine.Spy };
let translateServiceSpy: { instant: jasmine.Spy };
beforeEach(() => {
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put']);
translateServiceSpy = jasmine.createSpyObj('TranslateService', ['instant']);
companyService = new CompanyService(
httpClientSpy,
new ErrorMessageService(),
translateServiceSpy);
});
it('should be created', () => {
expect(companyService).toBeTruthy();
});
it('#getCompanyList:should return expected SearchCompanyListDto (HttpClient called once)', () => {
const expectedSearchCompanyListDto: SearchCompanyListDto = new SearchCompanyListDto();
const searchCompanyDto: SearchCompanyDto[] =
[{
companySeq: BigInt('1'),
companyName: 'companyName',
companyKana: 'companyKana',
companyAddress1: 'companyAddress1',
deleted: '',
createUser: 'createUser',
createTime: new Date,
updateUser: 'updateUser',
updateTime: new Date
}];
expectedSearchCompanyListDto.searchCompanyDtos = searchCompanyDto;
httpClientSpy.get.and.returnValue(asyncData(expectedSearchCompanyListDto));
companyService.getCompanyList(null).subscribe(
searchCompanyList => expect(searchCompanyList).toEqual(expectedSearchCompanyListDto, 'expected searchCompanyDto'),
fail
);
expect(httpClientSpy.get.calls.count()).toBe(1, 'one call');
});
it('#getCompanyList:when the server returns 400', () => {
const errorResponse = new HttpErrorResponse({
error: 'test 400 error',
status: 400, statusText: 'Bad Request'
});
httpClientSpy.get.and.returnValue(asyncError(errorResponse));
companyService.getCompanyList(null).subscribe(
searchCompanyList => expect(searchCompanyList).toBe(null)
);
});
let httpClientSpy: { get: jasmine.Spy, post: jasmine.Spy, put: jasmine.Spy };
は、HttpClientモックの宣言です。
今回はget
のことのみ書きますが、get
、post
、put
の3メソッドをモックに変えます。
そしてbeforeEachの中でCompanyServiceをnewする箇所の引数を<any>httpClientSpy
に変えます。
テストメソッドit('#getCompanyList:should return expected xxxxx
の中ではexpectedSearchCompanyListDtoを検証用に作ります。
そして下記のようにhttpCientモックのgetメソッドが呼ばれたときに返すようにします。
httpClientSpy.get.and.returnValue(asyncData(expectedSearchCompanyListDto));
ちなみにit('#getCompanyList:when the server returns 400
は通信エラー時のテストです。
AsyncObservableHelpersを作成
少し順序が逆転しましたが、今回AsyncObservableHelpersというテスト用のHelperクラスを作成しました。
リファレンスの下記を参照ください。
https://angular.jp/guide/testing.en
ng g class testing/async-observable-helpers
のコマンドで作成します。
import { defer } from 'rxjs';
export class AsyncObservableHelpers {
}
export function asyncData(data: T) {
return defer(() => Promise.resolve(data));
}
export function asyncError(errorObject: any) {
return defer(() => Promise.reject(errorObject));
}
カバレッジレポート
上記と同じ要領でcompanyServiceの残りのメソッドのテストコードも追加します。
すると下記のように今回追加した10件のテストはグリーンになりました。
下記のコマンドでカバレッジもとってみます。
ng test --code-coverage
まだまだカバレッジが足りませんが、下記のようにcompanyServiceだけは100%となりました。
今日の感想
今日はJasmine
にチャレンジしてみました。
こんな感じで書くんですねー!
大変勉強になりました、よし次はcomponentのテストだ。
では、今日もお疲れ様でした。