Angular8 で Web アプリを作ろう - Jasmine - Componentのテスト その1
今日すること
こんにちは、ふるてつです。
前回はService
にJasmine
のユニットテストを追加していきましたが、
今回はさらにComponent
に対して追加していきたいと思います。
テストをするComponentの概要
今回テストをするのは会社マスターの一覧を検索する画面です。
すごく簡単です、検索条件は「会社名」、「会社名カナ」、「削除フラグ」の3です。
画面は下記のようになります、レイアウトはざっとしか作っていません。
別途修正していくと思いますが、今日はこの感じのまま、ご説明します。
テストしたいアクションとしては「新規」「クリア」「検索」の3種類のボタンのクリック。
それと一覧の上をクリックして明細にジャンプするアクションです。
テストをするComponentのソースコード
ソースコードは下記のようになります。
長いのでたたんでおきます。
テストをするComponent(company-list.component.ts)
のソースコードはこちら
import { merge, of } from 'rxjs';
import { catchError, map, startWith, switchMap } from 'rxjs/operators';
import { AppConst } from 'src/app/app-const';
import { SearchCompanyDto } from 'src/app/entity/company/search-company-dto';
import { CompanyService } from 'src/app/service/company/company.service';
import { HttpParams } from '@angular/common/http';
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { MatPaginator } from '@angular/material/paginator';
import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router';
@Component({
selector: 'app-company-list',
templateUrl: './company-list.component.html',
styleUrls: ['./company-list.component.css']
})
export class CompanyListComponent implements OnInit {
// Timezone and Locale
locale: string;
timezone: string;
// Search criteria controls
companyName = new FormControl('', []);
companyKana = new FormControl('', []);
deleted = new FormControl(false);
// Form builder
mainForm = this.formBuilder.group({
companyName: this.companyName,
companyKana: this.companyKana,
deleted: this.deleted
});
// Search result dto
searchCompanyDtos: SearchCompanyDto[];
// Material tables header
displayCompanyListColumns: string[] = [
'companySeq',
'companyName',
'companyKana',
'companyAddress1',
'deleted',
'createUser',
'createTime',
'updateUser',
'updateTime',
];
// Loading and pagenation
isLoadingResults = false;
resultsLength = 0;
@ViewChild(MatPaginator, { static: true }) public paginator: MatPaginator;
constructor(
private formBuilder: FormBuilder,
private companyService: CompanyService,
private title: Title,
private router: Router
) { }
ngOnInit() {
this.setUpLocale();
this.setUpBrowserTitle();
}
/**
* Sets the locale from appConst.
*/
private setUpLocale() {
this.locale = AppConst.LOCALE;
this.timezone = AppConst.TIMEZONE;
}
/**
* Sets screen title.
*/
private setUpBrowserTitle() {
this.title.setTitle(AppConst.APP_TITLE + AppConst.APP_SUB_TITLE_COMPANY_LIST);
}
/**
* Clicks the new registration button.
*/
private onNew() {
this.router.navigate(['/company-detail/new']);
}
/**
* Click the clear button.
*/
private onClear() {
this.clearSearchCondition();
this.clearSearchResultList();
}
/**
* Searches for customer informations.
*/
private onSearch() {
merge(this.paginator.page)
.pipe(
startWith({}),
switchMap(() => {
this.isLoadingResults = true;
return this.companyService.getCompanyList(this.createHttpParams());
}),
map(data => {
// Flip flag to show that loading has finished.
this.isLoadingResults = false;
this.resultsLength = data.resultsLength;
this.paginator.pageIndex = data.pageIndex;
return data.searchCompanyDtos;
}),
catchError(() => {
this.isLoadingResults = false;
return of(null as SearchCompanyDto[]);
})
).subscribe(data => this.searchCompanyDtos = data);
}
/**
* Creates search criterias.
*/
private createHttpParams(): HttpParams {
const conditions = {
companyName: this.companyName.value,
companyKana: this.companyKana.value,
deleted: this.deleted.value.toString(),
pageSize: this.paginator.pageSize.toString(),
pageIndex: this.paginator.pageIndex.toString()
};
const paramsOptions = { fromObject: conditions };
const params = new HttpParams(paramsOptions);
return params;
}
/**
* Clears search criteria controls.
*/
private clearSearchCondition() {
this.companyName.setValue('');
this.companyKana.setValue('');
this.deleted.setValue(false);
}
/**
* Clears search result list.
*/
private clearSearchResultList() {
this.searchCompanyDtos = null;
this.resultsLength = 0;
}
/**
* Clicks search result.
* @param searchCompanyDto cliked company entity.
*/
private listClicked(searchCompanyDto: SearchCompanyDto) {
this.router.navigate(['/company-detail', searchCompanyDto.companySeq]);
}
}
デフォルトのテストコードを確認
デフォルトでできているテストコードは下記のようになります。
import { TestBed } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CompanyListComponent } from './company-list.component';
describe('CompanyListComponent', () => {
let component: CompanyListComponent;
let fixture: ComponentFixture;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [CompanyListComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CompanyListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
component初期化の修正
ng test
コマンドでテストを実行するとcomponentの初期化('should create')からまず失敗します。
なぜ失敗するかと言いますとcompany-list.componentにはわたしが新たにサービスを追加して、
コンストラクタでインジェクションしているからです。
メッセージをよく読むとたしかにRouterのDI
ができていない模様です。
ではそのあたりからまず修正してきます、箇条書きにすると下記になります。
- テストの先頭で
let routerSpy: { navigate: jasmine.Spy };
を追加 beforeEach(async(() => {}
の中で、
routerSpy = jasmine.createSpyObj('Router', ['navigate']);
でspyを作成providers: []
の中に{ provide: Router, useValue: routerSpy }
を追加上記と同様に
companyServiceSpy
とtranslatePipeSpy
も追加
translatePipe
はhtmlの中で他言語のパイプを使っているため登場しましたFormBuilder
とTitle
は今回のテストではそのまま使用できそうなのでspyではなくそのものをprovidersに追加わたしの環境では
ReactiveForms
やMaterial
を使用しており、imports文が必要でした
imports: [ReactiveFormsModule, BrowserAnimationsModule, MaterialModule, HttpClientModule,
最後に
schemas
プロパティを指定します
下記のように設定すると認識できない要素と属性を無視するようになります
schemas: [NO_ERRORS_SCHEMA],
まとめると下記のようにいったん変わりました。
これでテストが通るようになりました。
translatePipeSpyなど無駄にspyを作っているような気が若干しますが良しとします…
let component: CompanyListComponent;
let fixture: ComponentFixture;
let routerSpy: { navigate: jasmine.Spy };
let companyServiceSpy: { getCompanyList: jasmine.Spy };
let translatePipeSpy: { translate: jasmine.Spy };
beforeEach(async(() => {
routerSpy = jasmine.createSpyObj('Router', ['navigate']);
companyServiceSpy = jasmine.createSpyObj('CompanyService', ['getCompanyList']);
translatePipeSpy = jasmine.createSpyObj('TranslatePipe', ['translate']);
TestBed.configureTestingModule({
declarations: [CompanyListComponent],
providers: [
FormBuilder,
Title,
{ provide: Router, useValue: routerSpy },
{ provide: CompanyService, useValue: companyServiceSpy },
{ provide: TranslatePipe, useValue: translatePipeSpy },
],
})
.compileComponents();
}));
テストケースの追加
次に新たにテストケースを追加します。
●「新規」ボタンのアクションテスト
この画面では「新規」ボタンをクリックするとonNew()が呼ばれます。
onNew()はprivateのメソッドなのでcomponent['onNew']();
で呼び出します。
そしてonNew()を実行した後にrouterSpyの#navigateメソッドが1回呼ばれたのを確認します。
it('should navigate when called onNew', () => {
component['onNew']();
expect(routerSpy.navigate.calls.count()).toBe(1, 'one call');
});
●「クリア」ボタンのアクションテスト
次はonClear()メソッドです、「クリア」ボタンをクリックした時に呼ばれます。
あらかじめ検索条件のコントロールに何か値を設定しておき、onClearメソッドを呼びだします。
そしてそれぞれの検索条件のコントロールが初期値に戻っているかを確認します。
it('should navigate when called onClear', () => {
fillSearchCriteria(component);
component['onClear']();
expect(component.companyName.value).toEqual('');
expect(component.companyKana.value).toEqual('');
expect(component.deleted.value).toEqual(false);
});
~ 中略 ~
function fillSearchCriteria(component: CompanyListComponent) {
component.companyName.setValue('a');
component.companyKana.setValue('a');
component.deleted.setValue(true);
}
●一覧クリックアクションのテスト
一覧の上をクリックすると詳細画面にジャンプします。
上記のonNewの時とほぼ同じ感じになりました。
it('should navigate when called listClicked', () => {
const searchCompanyDto: SearchCompanyDto = new SearchCompanyDto();
searchCompanyDto.companySeq = BigInt('1');
component['listClicked'](searchCompanyDto);
expect(routerSpy.navigate.calls.count()).toBe(1, 'one call');
});
●「検索」ボタンのアクションテスト
最後にonSearch()のテストです、こちらはすこし長くなりました。
このonSearch()メソッドは非同期await component['onSearch']();
で呼び出すようにします。
メソッドの中でsubscribeしているからか非同期でないと思ったような結果が得られません。
まずはcompanyServiceSpyのgetCompanyList()がエラーを返すようにして、元ソースの中にあるcatchError(() => {}
の部分をテストします。
it('should catch error when called onSearch', async () => {
await component['onSearch']();
companyServiceSpy.getCompanyList.and.returnValue(throwError(''));
expect(component.isLoadingResults).toEqual(false);
});
次は正常終了の場合です。
companyServiceSpyのgetCompanyList()がダミーのdtoを返すようにしています。
最初はダミーのdtoは返さずにonSearch()メソッドを呼び出していたのですが、そのやり方だと元ソース中にあるmap(data => {}
の個所がテストできませんでした。
そこも網羅するためにダミーのdtoを返すようにしました。
it('should call map operator when called onSearch', async () => {
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;
companyServiceSpy.getCompanyList.and.returnValue(asyncData(expectedSearchCompanyListDto));
await component['onSearch']();
expect(component.searchCompanyDtos).toEqual(expectedSearchCompanyListDto.searchCompanyDtos);
expect(component.isLoadingResults).toEqual(false);
expect(companyServiceSpy.getCompanyList.calls.count()).toBe(1, 'one call');
});
これで下記のようにSkipさせているもの以外はテストがすべて通るようになりました。
DOM
側のテストコードをまだ説明していませんが、
これまでのテストコードを下に記します、みなさまのご参考になればと思います。
テストコード(company-list.component.spec.ts)はこちら
import { throwError } from 'rxjs';
import { AppConst } from 'src/app/app-const';
import { HttpLoaderFactory } from 'src/app/app.module';
import { SearchCompanyDto } from 'src/app/entity/company/search-company-dto';
import { SearchCompanyListDto } from 'src/app/entity/company/search-company-list-dto';
import { CompanyService } from 'src/app/service/company/company.service';
import { asyncData } from 'src/app/testing/async-observable-helpers';
import { MaterialModule } from 'src/app/utils/material/material.module';
import { HttpClient, HttpClientModule, HttpParams } from '@angular/common/http';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { By, Title } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Router } from '@angular/router';
import { TranslateLoader, TranslateModule, TranslatePipe } from '@ngx-translate/core';
import { CompanyListComponent } from './company-list.component';
describe('CompanyListComponent', () => {
let component: CompanyListComponent;
let fixture: ComponentFixture;
let routerSpy: { navigate: jasmine.Spy };
let companyServiceSpy: { getCompanyList: jasmine.Spy };
let translatePipeSpy: { translate: jasmine.Spy };
beforeEach(async(() => {
routerSpy = jasmine.createSpyObj('Router', ['navigate']);
companyServiceSpy = jasmine.createSpyObj('CompanyService', ['getCompanyList']);
translatePipeSpy = jasmine.createSpyObj('TranslatePipe', ['translate']);
TestBed.configureTestingModule({
declarations: [CompanyListComponent],
schemas: [NO_ERRORS_SCHEMA],
imports: [ReactiveFormsModule, BrowserAnimationsModule, MaterialModule, HttpClientModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
],
providers: [
FormBuilder,
Title,
{ provide: Router, useValue: routerSpy },
{ provide: CompanyService, useValue: companyServiceSpy },
{ provide: TranslatePipe, useValue: translatePipeSpy },
],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CompanyListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
/**
* Type Script test cases.
*/
it('locale and timezone should be set when called ngOnInit', () => {
component.ngOnInit();
expect(component.locale).toEqual(AppConst.LOCALE);
expect(component.timezone).toEqual(AppConst.TIMEZONE);
});
it('locale and timezone should be set when called setUpLocale', () => {
component['setUpLocale']();
expect(component.locale).toEqual(AppConst.LOCALE);
expect(component.timezone).toEqual(AppConst.TIMEZONE);
});
// TBD
// it('browser title should be set when called setUpBrowserTitle', () => {
// component['setUpBrowserTitle']();
// expect(component.locale).toEqual(AppConst.LOCALE);
// });
it('should navigate when called onNew', () => {
component['onNew']();
expect(routerSpy.navigate.calls.count()).toBe(1, 'one call');
});
it('should navigate when called onClear', () => {
fillSearchCriteria(component);
component['onClear']();
expect(component.companyName.value).toEqual('');
expect(component.companyKana.value).toEqual('');
expect(component.deleted.value).toEqual(false);
});
it('should call map operator when called onSearch', async () => {
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;
companyServiceSpy.getCompanyList.and.returnValue(asyncData(expectedSearchCompanyListDto));
await component['onSearch']();
expect(component.searchCompanyDtos).toEqual(expectedSearchCompanyListDto.searchCompanyDtos);
expect(component.isLoadingResults).toEqual(false);
expect(companyServiceSpy.getCompanyList.calls.count()).toBe(1, 'one call');
});
it('should catch error when called onSearch', async () => {
await component['onSearch']();
companyServiceSpy.getCompanyList.and.returnValue(throwError(''));
expect(component.isLoadingResults).toEqual(false);
});
it('should navigate when called listClicked', () => {
const searchCompanyDto: SearchCompanyDto = new SearchCompanyDto();
searchCompanyDto.companySeq = BigInt('1');
component['listClicked'](searchCompanyDto);
expect(routerSpy.navigate.calls.count()).toBe(1, 'one call');
});
/**
* DOM test cases.
*/
~ 中略 ~
});
function fillSearchCriteria(component: CompanyListComponent) {
component.companyName.setValue('a');
component.companyKana.setValue('a');
component.deleted.setValue(true);
}
今日の感想
今日はComponentのテストについて書きました。
Componentの中でもどちらかと言えばType Script
側のコードをテストしたことになります。
あと別途DOM
側のテストが残っていますが、そちらについては書ききれなかったので「その2」で書きたいと思います。
では、今日もお疲れ様でした。
今度は客先でコンテナ勉強会
今日すること
こんにちはふるてつです。
最近客先にて勉強会がスタートしまして、ただいまDockerを勉強中です。
まずは入門ということでこちらのサイト「入門Docker」を勉強しております。
https://y-ohgi.com/introduction-docker/
今週は自社の会議で参加できず、自宅にて「コンポーネント」 メニューの 「Dockerfile」ページを自習しました。
1. Dockerfile
Dockerfile
を記述してそのファイルを元にDocker Image
をビルドすることでスナップショットの作成ができます。
試しにDockerfile
を記述してDocker Image
を作成してみます。
1-1. 環境の用意
ローカルに任意のディレクトリを作成しテキストファイルを1つ作ります(hello.txt)
中身はhello docker !
と書いておきます。
1-2. Dockerfile の編集
次に同一のディレクトリに下記内容のDockerfile
を作成します。
FROM ubuntu
COPY hello.txt /tmp/hello.txt
CMD ["cat", "/tmp/hello.txt"]
上記のDockerfileは上から順番に
1. ubuntu というDocker Imageをもとに、
2. ホストの hello.txt をコンテナの /tmp/hello.txt へコピーして、
3. 「cat /tmp/hello.txt コマンドを実行」という意味になります。
1-3. Docker Image のビルド&実行
docker build
コマンドでDockerfile
から Docker Image を作成します。
docker build -t hello .
-t hello オプションは「Docker Imageを hello というタグ名にする」という意味です。
"."はdocker build 実行時のコンテキストの指定です。
COPY コマンドを実行する際にどのディレクトリを起点とするかを指定します。
実行すると下記のメッセージが表示されました。
You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.
わたしはWindows環境を使用していますのでその関係で出てくるもようです。
「すべてのディレクトリのpermission
を755にとりあえずしますよ」という旨のようです。
今回はテストなのでそのままにしておきます。
docker images
コマンドで確認します、下のようにhello
イメージができています。
補足ですがFromで指定したubuntu
のイメージもダウンロードされてきています。
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello latest b789b8db2e5f 3 seconds ago 64.2MB
ubuntu latest a2a15febcdf3 5 days ago 64.2MB
ではdocker run hello
で、このイメージを実際に動かしてみます。
下記のように CMD で指定した cat /tmp/hello.txt が実行され、"hello docker !"と表示されました。
docker run hello
hello docker !
なるほどDockerfile
はこんな感じで作るわけです。
このコマンドを色々組合わせて自分独自のDocker Image
を作っていくんでしょうね。
2. Docker Hub へアップロード
ローカルで開発したイメージをステージングや本番環境で動かすにはDockerレジストリにアップロードする必要があるようです。
DockerレジストリはDocker Image を保存するための場所で、Docker版のGitHubのようなものだそうです。
Docker公式が提供しているDockerHub
へ先ほど作成した hello イメージをアップロードします。
使用する前にアカウント登録が必要になります、下記サイトです。
https://hub.docker.com/
2-1. Docker Hubへログイン
下記コマンドでDocker Hub
へログインします。
docker login
ユーザ名やパスワードを聞かれるのでコンソールに従って入力していきます。
2-2. Docker Image を命名
Docker Hub
にアップロードするためにはDocker Image
の命名規則に従う必要があるそうです。
ユーザーのオリジナルイメージは<USER NAME>/<IMAGE NAME>:<TAG>
という命名になります。
( :
まずはdocker tag
コマンドで命名します。
<USER NAME> は自分のユーザー名を入力します。
docker tag hello <USER NAME>/hello
命名できたかdocker images
コマンドで確認します。
REPOSITORYが <USER NAME>/hello になっているイメージが新たにできています。
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello latest b789b8db2e5f 14 minutes ago 64.2MB
<USER NAME>/hello latest b789b8db2e5f 14 minutes ago 64.2MB
ubuntu latest a2a15febcdf3 5 days ago 64.2MB
2-3. Docker Imageのアップロード
下記のコマンドでDocker Imageをアップロードします。
docker push <USER NAME>/hello
うまくいったようです。下のようなメッセージが表示されました。
bf69b9f7a066: Pushed
122be11ab4a2: Mounted from library/ubuntu
7beb13bce073: Mounted from library/ubuntu
f7eae43028b3: Mounted from library/ubuntu
6cebf3abed5f: Mounted from library/ubuntu
latest: digest: sha256:f4c69205069a08247f3e5403d982b8635b7c74620a9966a4b1105e62c5974a83 size: 1359
Docker Hub
を確認してみます。
登録されていますね✨
2-4. Docker Hub にアップロードしたDocker Imageの実行
現在存在するローカルのイメージを削除します。
同名のイメージが存在するとDockerHub
から取得せず、ローカルに存在するイメージを参照してしまうからだそうです、なるほど。
docker container prune
コマンドを実行します。
container prune
コマンドは停止しているコンテナをすべて消すコマンドです。
参考サイト:https://docs.docker.com/engine/reference/commandline/container_prune/
docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
ca5ffc8299ad46f659ee21aa8b3793d651b3be8f1124d1cd7469cb99ab2dba2f
8f2895514831065050183538213b4f28bb829c823363466801bb63b48ef7fc16
4348253120ae6d8fdb1ad1c479091b446c15b3a56ac42ef9cf012dc8b7cbbbb8
d9dddcfe7a042bf7813ea5308a4a880a9c0d5742c649229c69272a54f8f4dfdd
af14ba183197d11fc16365470d989dad9d28e101ffeba56c8e3c377929235101
次にdocker image prune -a
コマンドを実行します。
image prune
コマンドは停止しているイメージを消すコマンドです。
-a
オプションをつけるとすべてになります(-all
)
参考サイト:https://docs.docker.com/engine/reference/commandline/image_prune/
docker image prune -a
WARNING! This will remove all images without at least one container associated to them.
Are you sure you want to continue? [y/N] y
Deleted Images:
untagged: hello:latest
untagged: tetsufuru1968/hello:latest
untagged: tetsufuru1968/hello@sha256:f4c69205069a08247f3e5403d982b8635b7c74620a9966a4b1105e62c5974a83
deleted: sha256:b789b8db2e5f37ea6eda822a1fcf82334d60bb36f1dc8940518e1238cbb9335f
deleted: sha256:5ebfb5bb22e017b5f43655e7388072f559c9b547148e906101b8be88d4e97a25
deleted: sha256:01af779985d92c769233e7a4f07321dac0e25a5ec198660c9faed0c3590195e7
untagged: ubuntu:latest
untagged: ubuntu@sha256:d1d454df0f579c6be4d8161d227462d69e163a8ff9d20a847533989cf0c94d90
deleted: sha256:a2a15febcdf362f6115e801d37b5e60d6faaeedcb9896155e5fe9d754025be12
deleted: sha256:fdc47e80ad3dbe1767a5c2141442c0c7aa93a14a817357a0d4353cd8ae48ee58
deleted: sha256:2b4ed599a73ad82b15d4e3488d95af4f387037b90401d63ad6cb433374b3e3d3
deleted: sha256:8f06e3a624319de717230f0d766dd8922d048441adb9e0387860a8047d000409
deleted: sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f
それでは改めてdocker pull <USER NAME>/hello
コマンドでDocker Hub
から自作したイメージを取得します。
docker pull <USER NAME>/hello
Using default tag: latest
latest: Pulling from <USER NAME>/hello
35c102085707: Pull complete
251f5509d51d: Pull complete
8e829fe70a46: Pull complete
6001e1789921: Pull complete
2cf3a5dd5be8: Pull complete
Digest: sha256:f4c69205069a08247f3e5403d982b8635b7c74620a9966a4b1105e62c5974a83
Status: Downloaded newer image for <USER NAME>/hello:latest
docker.io/<USER NAME>/hello:latest
docker run <USER NAME>/hello
コマンドで上記で取得したDocker image
を実行します。
docker run <USER NAME>/hello
hello docker !
3. DSL(Domain Specific Language:ドメイン固有言語)
Dockerfileには17のコマンドが用意されているそうですが、わたしは初心者ですのでいきなり全部は大変そうです。
「入門Docker」のサイトで言われている基本的な7つのコマンドをまずは覚えて、今後試してみようかと思います。
FROM
、COPY
、RUN
、CMD
、WORKDIR
、ENV
、USER
今日の感想
今回も勉強した「入門Docker」の内容をそのまままとめた内容になりました。
申し訳ありませんがDocker初心者ですのでご勘弁ください。
でもこうやってブログに書くのは良いですね、書くことでいい復習になります。
それではまた
モダンコーディング入門 - HTML5とCSS3 - その3
今日すること
こんにちは、ふるてつです。
🍉盆休みを使って下の本でHTML5
とCSS3
を勉強中です、その中で新しく知ったことを書いています。
書籍中では3種類のレイアウトのサイトを作るのですが、2つ目のレイアウトを作り終えました。
今回はMasonry
というJavaScriptライブラリを使用し、ウィンドウサイズに合わせて自動に段組みが変わっていく可変グリッドレイアウトのサイトを作成しました。
下記のようなデザインになります。
出来上がったサイトはこちらです、リンクなどはクリックしても一切動きませんが、せっかくなのでGithub pagesで見れるようにしました。
https://tetsujifurukawa.github.io/learning_modernCodingOfHtml5Css3/grid-layout/index.html
● Masonry
これははじめて聞くライブラリです。
Masonry
を組み込むと下のように縦に並んだボックスのエリアが、良い感じに整列します。
このMasonry
は自分で撮った写真をいろいろなサイズに編集して1ページで見せたい時などに便利そうですね、
きっときれいにできそうです。
こちらが組み込み前
組み込み後
組み込み方は簡単。
HTMLのbody
の最後尾で下記のコードを書きます。
columnWidth
はグリッド1列分の幅
gutter
はグリッド同士の水平方向の間隔
<body>
~ 中略 ~
<script src="lib/masonry.pkgd.min.js"></script>
<script>
window.onload = function () {
new Masonry('body', {
itemSelector: '.item',
columnWidth: 180,
gutter: 4
});
};
</script>
</body>
詳しくはこちらを参照ください:https://masonry.desandro.com/options.html
● script要素はhead内?それともbody内?
HTMLの描画が終わる前に実行する必要があるJavaScript以外は、パフォーマンス上の理由からなるべくbody要素の最後に記述したほうが良いそうです。
これも知りませんでした、body
にも書けますが、基本はhead
の中に全部書くものだと思ってました。
jQuery
などもですかね、これまでずっと呪文のようにhead
に書いてきた気がします。
headに書いた場合、HTML要素の描画の順番が最後になります。
<head>
<script src=""~""></script>・・・ 1. ファイルのロード、実行
<script>~</script>・・・ 2. コードの実行
</head>
<body>
<!- HTML -->・・・ 3. HTML要素の描画
</body>
bodyの最後尾に書いた場合、HTML要素の描画の順番が最初になります。
<head>
</head>
<body>
<!- HTML -->・・・ 1. HTML要素の描画
<script src=""~""></script>・・・ 2. ファイルのロード、実行
<script>~</script>・・・ 3. コードの実行
</body>
参考にしたサイト:https://www.1-firststep.com/archives/2086
● マージンのネガティブ指定
下記は一つのアイテムのブロックで、8px~11pxのpadding
を設定しています。
このようなエリアだとクリック領域がpadding
の内側までしか広がりません。
そこでネガティブマージンを使って<a>
要素をエリアと同じ領域に広げます。
これは常識かもしれませんが、わたしは知りませんでした。
HTMLは下記のようになっています。
<section class="item item-m item-breaktime">
<a href="#">
<img class="image" src="images/image_M_3.jpg" alt="おやつの時間">
<div class="category">BREAK TIME</div>
<p class="description">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt
</p>
</a>
</section>
そして下記CSSのようにitemクラスのa
にマージンのマイナス値を入れます。
.item>a {
display: block;
margin: -8px -8px -11px;
padding: 8px 8px 11px;
border-radius: inherit;
~ 中略 ~
}
● フォーカスが当たるとアニメーション付きでアンダーラインが出てくるTips
下記の「MAIN DISH」のところのように、フォーカスを当たるとアンダーラインがアニメーション付きでゆっくり出てきます。
こういうのがあるとちょっと楽しいですよねぇ。
Angular
でMaterial
をさわっているとInput
になにか入力しようとすると、
下線にアニメーションがかかって良い感じでフォーカスが当たった感じになります。
Material
だから簡単にできるのかと思っていましたが、わりと似たようなことはCSSすぐにできるんですね。
これも知りませんでした。
https://material.angular.io/components/input/overview
ちなみにHTMLは下記です。
<nav class="nav">
<ul>
<li class="nav-item"><a href="#">HOME</a></li>
<li class="nav-item"><a href="#">ABOUT</a></li>
<li class="nav-item"><a href="#">MAIN DISH</a></li>
<li class="nav-item"><a href="#">APPETIZER</a></li>
<li class="nav-item"><a href="#">BREAK TIME</a></li>
<li class="nav-item"><a href="#">COLUMN</a></li>
<li class="nav-item"><a href="#">OTHER</a></li>
</ul>
</nav>
CSSは下記。
.nav-item a {
display: inline-block;
}
.nav-item a::after {
content: '';
display: block;
width: 0;
margin: 6px auto 0;
border-bottom: 1px solid #7C5119;
transition: width 0.3s ease-in-out;
}
.nav-item a:hover::after {
width: 100%;
}
nav-itemクラスのa
の後にafter
で疑似要素を作り、そこにあらかじめborder-bottom
で下線を引いておきます。
width
はゼロにしておき、hover
したときに100%にします。
その際にtransition
のease-in-out
を設定しておくと、アニメーション付きでアンダーラインが表示されるようになります。
● Githubに絵文字
最後はHTMLとは全然関係のないお話です。
Masonry
のGithub
をみたのですが、コミットのコメントに絵文字が入っていてなんだかかわいいと思いました。
開発系のGithub
では見たことがないですけど、デザイン系だと普通なのでしょうか。
業務では怒られそうなので個人で使っているGithub
に今度から絵文字を入れてみようかと思いました。
今日の感想
盆休み最終日にやっと2種類目が終わりました。
まだまだ知らないことはありますね、勉強し放題です。
盆休みが終わったので他のAngularやAWSもくもく会もまた再開するので、
残り1種類は少し期間が空きそうですが、9月末くらいには全部終えたいと思います。
では、今日もお疲れ様でした。
モダンコーディング入門 - HTML5とCSS3 - その2
今日すること
こんにちは、ふるてつです。
盆休みを使ってHTML5
とCSS3
を勉強中です、その中で新しく知ったことを書いています。
本の中では3種類のレイアウトのサイトを作るのですが、1つ目のレイアウトを作り終えました。
普通のレイアウトというところでしょうか、下記のようなデザインです。
出来上がったサイトはこちらです、工夫なく本の通りに作った内容なので、若干恥ずかしいところもありますが、せっかくなのでGithub pagesで見れるようにしました。
ただしリンクなどはクリックしても一切動きません。
https://tetsujifurukawa.github.io/learning_modernCodingOfHtml5Css3/standard-layout/index.html
● ベンダープレフィックス
ベンダープレフィックスとは、仕様が未確定な機能をブラウザが先行実装したり、独自の拡張機能を実装する場合に、そのことを明示するためにつける識別子です。
じつはわたしは自分で書いたことはないです。
これまでにわたしが担当したWeb系のシステムはほぼ企業内部で使用するもので、IE限定で動作すれば良くあまり考える必要がなかったからです。
複数ブラウザに対応するサイトを担当すると必要なんでしょうね、覚えておきます。
参考にしたサイト:CSS入門:ベンダープレフィックスとは? | サービス | プロエンジニア
ちなみ書き方は下記のような感じ、これは回転の例です。
.ranking .order {
display: inline-block;
~ 中略 ~
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
ベンダープレフィックスの要不要を確認する際には以下のサイトが便利です。
Can I use... Support tables for HTML5, CSS3, etc
● CSSカウンタ
こちらも使ったことがないです。
CSSの中でカウンタを定義して値を増やしたり表示したりすることができます。
カウントを使用するのに必要な手順は4つです。
- カウンタの名前を決める
- カウンタの値をゼロに初期化する ・・・ counter-rest : カウンタ名 ;
- カウンタの値を表示する ・・・ content: counter(カウンタ名) ;
- カウンタの値を増加させる ・・・ counter-increment : カウンタ名 ;
.ranking {
margin-bottom: 30px;
~ 中略 ~
counter-reset: ranking;
}
.ranking .order::before {
content: counter(ranking);
counter-increment: ranking;
~ 中略 ~
}
● フッターの書き方
フッターについてです。
フッターで良く見かける「ABOUT XX」や「CONTACT」などはひとつひとつ<li></li>
で書くんですね。
隣同士は縦罫線で区切るのも知りませんでした。
みたまま左から右に<a></a>
で文字を並べていくと思っていましたし、縦罫線ではなくパイプ|
で区切るとも勘違いしてました。
知らないというのは怖いですね!
ちなみに本では下記のようなhtmlになっていました。
<footer class="footer">
<ul class="horizontal-list">
<li class="horizontal-item"><a href="#">ABOUT ME</a></li>
<li class="horizontal-item"><a href="#">SITE MAP</a></li>
<li class="horizontal-item"><a href="#">SNS</a></li>
<li class="horizontal-item"><a href="#">CONTACT</a></li>
</ul>
<p class="copyright">Copyright © 2015 SAMPLE SITE</p>
</footer>
今日の感想
今回は新しくわかったことなどは前回ほどは多くなかったです。
今回はサクッと進みまして、1種類目のレイアウトが終わりました。
あと2種類ちゃんと勉強して終わらせねば!
では、今日もお疲れ様でした。
モダンコーディング入門 - HTML5とCSS3
今日すること
こんにちは、ふるてつです。
今日はCSS3
とHTML5
です。
盆休みということもあり少しまとまった時間が取れそうなので、
普段やらないことをしてみようと思い、HTML5
とCSS3
を勉強することにしました。
わたしはもともとの育ちがWindowsアプリだったり、Web系開発ではサーバ側が主でしたので、
Angularのようなフロントエンドの仕組みを使うと、HTMLやCSS3でつまずいてしまうことが多いです。
そこですこし勉強してみようかと思いました。
基本的にはこの本を使います。
この本を進めていった中で新たに知ったことをまとめようかと思います。
● そもそものHTML5の書き方
HTML5からDOCTYPE宣言やmeta要素、外部ファイルの読込が簡潔に書けるようになりました、そういえば昔より短いですね。
HTML4 | HTML5 |
---|---|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> | <!DOCTYPE html> |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | <meta charset="UTF-8"> |
<script type="text/javascript" src="main.js"></script> | <script src="main.js"></script> |
<link rel="stylesheet" type="text/css "href="main.css"> | <link rel="stylesheet" href="main.css"> |
● 新たな要素が追加
header、footer、main要素が追加になっていました。
例)<header></header>
、<main></main>
、<footer></footer>
ヘッダーやフッター、メインのコンテンツなど、以前はすべて<div>
で定義していたと思いますが最近は違うんですね。
そのほかにも<nav>
や<time>
なども追加になりました。
HTML5で追加された要素については下記を参照ください。
https://www.tagindex.com/html5/basic/added.html
● clearfixというcssのクラス
これは全然知らなかった内容です。
クラスというよりtips的なテクニックと言ったところでしょうか。
横並びのレイアウトを実現するためにfloatを使用した時に、要素の高さを親要素が認識できなくなり表示が崩れる場合があります。
それを修正するために使います。
参考にしたサイト:https://qiita.com/mariofujisaki/items/2ad1de8432d7249afadc
● ::after、::before疑似要素
HTMLには書かれていない要素もどきをCSSで作ることができます。
タグ名やクラス名、id名などの後に::beforeや::afterをつけます。
afterやbeforeが便利なのは「HTMLコードを汚すことなく様々な表現ができる」点です。
以下のように書きます、上述のclearfixクラスを作る際に出てきました。
.clearfix::after {
content: '';
display: block;
clear: both;
}
参考にしたサイト:https://saruwakakun.com/html-css/basic/before-after
● reset.css
職場ではよく聞きましたが、個人的に自分で使ったことはありません。
ブラウザ間の表示を統一しやすくするために使用します。
参考にしたサイト:https://parashuto.com/rriver/development/to-reset-or-no-to-reset-css
- メリット
ブラウザ間の表示の違いを効率的に最小限に抑えることができます。
ブラウザ間の差異を気にせずに各要素の必要な部分だけスタイルできます。 - デメリット
CSSの量が多くなります。
リセットしたスタイルの再設定を忘れる可能性があります。
リセットしても結局再定義している場合が多いので意味がない?
● font-size:62.5%
こちらはわたしが不勉強なだけかもしれません。
html要素に対して、ルートのフォントサイズを10pxに設定するためにCSSに記述します。
ほとんどのブラウザで、ルート要素のフォントサイズが16pxに設定されています。
16pxに0.625を掛けると10pxとなるので、フォントサイズを「62.5%」と指定していることになります。
わざわざルートを10pxに設定する理由は、rem指定のときの計算が楽になるからです。
html {
font-size: 62.5%;
}
参考にしたサイト:https://bsj-k.com/font-size-rem-bestpractice/
● ブロックレベル要素とインライン要素
CSSの display オプションの話になります。
以前は「要素」ごとにブロックレベルかインラインか決まっていましたが、現在はdisplay
でどちらにも指定できます。
display: block;
この様に書けば、ブロックレベル扱いになりますし、
display: inline;
この様に書けばインライン扱いになります。
そこに加えてインラインブロック扱いが存在しますdisplay: inline-block;
インラインのように扱って横に並べたり出来るのですが、ブロックレベルのように幅や高さなどを指定できます。
参考にしたサイト:https://kent-and-co.com/127/
要素ごとに決まっていた時のブロックレベル要素とインライン要素については、下記を参照ください。
http://www.htmq.com/htmlkihon/005.shtml
● transitionプロパティ
CSSトランジション機能を使うと、CSSプロパティの値が変化する際にかかる時間や変化の方法を制御することができます。
下記のように書くとリンクをクリックした時などに使用できます。
.global-nav .nav-item a {
~ 中略 ~
transition: 0.15s;
}
参考にしたサイト:https://developer.mozilla.org/ja/docs/Web/CSS/transition
●hoverとopacity
これはテクニック的なものです。
画像などの上で、ホバーしたときにopacity
を少し下げ透過させると少し光ったように見せることができます。
下記のような書き方になります。
.hot-topic:hover {
opacity: 0.85;
}
今日の感想
今日は盆休みということもありHTML5
とCSS3
にチャレンジしてみました。
もともと不勉強だったせいもありますが、新たに知ったことが多すぎて困りました。
本では全体で3種類のサイトを作るのですが、まだ1つ目のサイトの半分程度です。
https://tetsujifurukawa.github.io/learning_modernCodingOfHtml5Css3/standard-layout/index.html
盆休みに全部終わるつもりがとても終わりません…
小学生の時いつも「夏休みの友」が終わらず大騒ぎしていたのを思い出します。
では、今日もお疲れ様でした。
今夜は社内AWSもくもく会2 - RDSを無料枠でマルチAZ化
今日すること
こんにちはふるてつです。
今週は夏休みで社内AWSもくもく会もお休みです。
今回は自習した内容になります。
以前、RDSを無料枠内で作成しようとしたときに、マルチAZの構成を設定できませんでした。
わたしは無料枠内ではこの構成はできないと思っていましたが、後日可能なことが分かりました。
一度シングルAZでRDSを作成した後に、変更するとマルチAZにできるんです。
わたし気づきませんでした。
ただしマルチAZの設定はできますが、マルチにした分、無料枠の時間がマルチに減っていくことになりますので、 試したら結局すぐ消さないとなりません。
サブネットの作成・DBサブネットグループの作成
ap-northeast-1a
とap-northeast-1c
にサブネットをそれぞれ作成します。
そしてDBサブネットグループを作成しサブネット2つを追加します。
わたしの場合、これまでに作ったものがありそのまま使用します。
上のwp-dbsubnetがサブネットグループです。
サブネットグループには2つのサブネットが追加されています。
RDSの作成
以前作ったRDSは削除したので、再度当時と同じ設定で作り直します。
前回と画面レイアウトが若干変わってますね。
DBはMySQLを選び、バージョンも前回と同じにします。
テンプレートも前回同様に"無料利用枠"を選びます。
これ以降はほぼ前回と同様で進みます。
インスタンスのサイズやストレージはデフォルトのままで。
上記のように作成時はマルチAZは選べません。
これも前回と同じです。
上記の概算月間コストを見ると、db.t2.microインスタンスシングルAZにおける750時間が無料枠の目安だそうです。
マルチAZにして2台構成にすると半分の375時間/月になるんでしょうね。
いったん作成し終わりました。
RDSの変更・マルチAZ化
作成したRDSを選択して「アクション」から変更を行います。
下記のような変更画面が表示されます。
マルチAZに変更して、「変更のスケジュール」を"すぐに適用"にします。
あとは「DBインスタンスの変更」ボタンを押して待つだけです。
10分ほどかかります。
変更が完了すると「マルチAZ」欄が"あり"に変わります。
感想
夏休み中ということもあり今回はかなり軽めの内容になりました。
ボタン一つでDB構成を変えられるのは面白いですね。
次の冬休みか春休みに、DBについてもっと深く試してみようかなと思いました。
それではまた
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のテストだ。
では、今日もお疲れ様でした。