ふるてつのぶろぐ

福岡在住のエンジニアです。

写真提供:福岡市

Angular8 で Web アプリを作ろう - Jasmine - Componentのテスト その1

今日すること

こんにちは、ふるてつです。

前回はServiceJasmineユニットテストを追加していきましたが、
今回はさらにComponentに対して追加していきたいと思います。

テストをするComponentの概要

今回テストをするのは会社マスターの一覧を検索する画面です。
すごく簡単です、検索条件は「会社名」、「会社名カナ」、「削除フラグ」の3です。

画面は下記のようになります、レイアウトはざっとしか作っていません。
別途修正していくと思いますが、今日はこの感じのまま、ご説明します。
f:id:tetsufuru:20190831161753p:plain

テストしたいアクションとしては「新規」「クリア」「検索」の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')からまず失敗します。
f:id:tetsufuru:20190831175021p:plain

なぜ失敗するかと言いますとcompany-list.componentにはわたしが新たにサービスを追加して、 コンストラクタでインジェクションしているからです。
メッセージをよく読むとたしかにRouterのDIができていない模様です。
ではそのあたりからまず修正してきます、箇条書きにすると下記になります。

  1. テストの先頭でlet routerSpy: { navigate: jasmine.Spy };を追加
  2. beforeEach(async(() => {}の中で、
    routerSpy = jasmine.createSpyObj('Router', ['navigate']);でspyを作成
  3. providers: []の中に{ provide: Router, useValue: routerSpy }を追加

  4. 上記と同様にcompanyServiceSpytranslatePipeSpyも追加
    translatePipeはhtmlの中で他言語のパイプを使っているため登場しました

  5. FormBuilderTitleは今回のテストではそのまま使用できそうなのでspyではなくそのものをprovidersに追加

  6. わたしの環境ではReactiveFormsMaterialを使用しており、imports文が必要でした
    imports: [ReactiveFormsModule, BrowserAnimationsModule, MaterialModule, HttpClientModule,

  7. 最後に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させているもの以外はテストがすべて通るようになりました。

f:id:tetsufuru:20190901014803p:plain

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」で書きたいと思います。

では、今日もお疲れ様でした。

今度は客先でコンテナ勉強会

今日すること

f:id:tetsufuru:20190806104529p:plain:w100
こんにちはふるてつです。

最近客先にて勉強会がスタートしまして、ただいまDockerを勉強中です。
まずは入門ということでこちらのサイト「入門Docker」を勉強しております。
https://y-ohgi.com/introduction-docker/
今週は自社の会議で参加できず、自宅にて「コンポーネント」 メニューの 「Dockerfile」ページを自習しました。

1. Dockerfile

Dockerfileを記述してそのファイルを元にDocker Imageをビルドすることでスナップショットの作成ができます。
試しにDockerfileを記述してDocker Imageを作成してみます。
f:id:tetsufuru:20190824090522p:plain:w500

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 イメージをアップロードします。
使用する前にアカウント登録が必要になります、下記サイトです。
f:id:tetsufuru:20190824110923p:plain 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>という命名になります。
( : は省略可能で、省略すると :latest と命名されます)
まずは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を確認してみます。
登録されていますね✨ f:id:tetsufuru:20190824101838p:plain

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つのコマンドをまずは覚えて、今後試してみようかと思います。
FROMCOPYRUNCMDWORKDIRENVUSER

今日の感想


今回も勉強した「入門Docker」の内容をそのまままとめた内容になりました。
申し訳ありませんがDocker初心者ですのでご勘弁ください。
でもこうやってブログに書くのは良いですね、書くことでいい復習になります。

それではまた

モダンコーディング入門 - HTML5とCSS3 - その3

今日すること

こんにちは、ふるてつです。
🍉盆休みを使って下の本でHTML5CSS3を勉強中です、その中で新しく知ったことを書いています。
f:id:tetsufuru:20190815030218p:plain:w100

書籍中では3種類のレイアウトのサイトを作るのですが、2つ目のレイアウトを作り終えました。
今回はMasonryというJavaScriptライブラリを使用し、ウィンドウサイズに合わせて自動に段組みが変わっていく可変グリッドレイアウトのサイトを作成しました。
下記のようなデザインになります。

f:id:tetsufuru:20190818111609p:plain

出来上がったサイトはこちらです、リンクなどはクリックしても一切動きませんが、せっかくなのでGithub pagesで見れるようにしました。

https://tetsujifurukawa.github.io/learning_modernCodingOfHtml5Css3/grid-layout/index.html

● Masonry

これははじめて聞くライブラリです。
f:id:tetsufuru:20190818121423p:plain

https://masonry.desandro.com/

Masonryを組み込むと下のように縦に並んだボックスのエリアが、良い感じに整列します。
このMasonryは自分で撮った写真をいろいろなサイズに編集して1ページで見せたい時などに便利そうですね、 きっときれいにできそうです。

こちらが組み込み前
f:id:tetsufuru:20190818121911p:plain

組み込み後
f:id:tetsufuru:20190818121951p:plain

組み込み方は簡単。
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>要素をエリアと同じ領域に広げます。
これは常識かもしれませんが、わたしは知りませんでした。
f:id:tetsufuru:20190818115130p:plain:w150

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」のところのように、フォーカスを当たるとアンダーラインがアニメーション付きでゆっくり出てきます。
こういうのがあるとちょっと楽しいですよねぇ。
f:id:tetsufuru:20190818145005p:plain

AngularMaterialをさわっていると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%にします。
その際にtransitionease-in-outを設定しておくと、アニメーション付きでアンダーラインが表示されるようになります。

Githubに絵文字

最後はHTMLとは全然関係のないお話です。
MasonryGithubをみたのですが、コミットのコメントに絵文字が入っていてなんだかかわいいと思いました。
開発系のGithubでは見たことがないですけど、デザイン系だと普通なのでしょうか。
業務では怒られそうなので個人で使っているGithubに今度から絵文字を入れてみようかと思いました。
f:id:tetsufuru:20190818151022p:plain

今日の感想

盆休み最終日にやっと2種類目が終わりました。 まだまだ知らないことはありますね、勉強し放題です。
盆休みが終わったので他のAngularやAWSもくもく会もまた再開するので、 残り1種類は少し期間が空きそうですが、9月末くらいには全部終えたいと思います。

では、今日もお疲れ様でした。

モダンコーディング入門 - HTML5とCSS3 - その2

今日すること

こんにちは、ふるてつです。
盆休みを使ってHTML5CSS3を勉強中です、その中で新しく知ったことを書いています。

本の中では3種類のレイアウトのサイトを作るのですが、1つ目のレイアウトを作り終えました。
普通のレイアウトというところでしょうか、下記のようなデザインです。

f:id:tetsufuru:20190816105904p:plain

出来上がったサイトはこちらです、工夫なく本の通りに作った内容なので、若干恥ずかしいところもありますが、せっかくなので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

f:id:tetsufuru:20190816115437p:plain

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;

  ~ 中略 ~
}
● フッターの書き方

フッターについてです。
f:id:tetsufuru:20190816153326p:plain

フッターで良く見かける「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

今日すること

こんにちは、ふるてつです。
今日はCSS3HTML5です。

f:id:tetsufuru:20190815031209p:plain:w200
盆休みということもあり少しまとまった時間が取れそうなので、
普段やらないことをしてみようと思い、HTML5CSS3を勉強することにしました。
わたしはもともとの育ちがWindowsアプリだったり、Web系開発ではサーバ側が主でしたので、 Angularのようなフロントエンドの仕組みを使うと、HTMLやCSS3でつまずいてしまうことが多いです。
そこですこし勉強してみようかと思いました。

基本的にはこの本を使います。
この本を進めていった中で新たに知ったことをまとめようかと思います。
f:id:tetsufuru:20190815030218p:plain:w100

● そもそもの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

  1. メリット
    ブラウザ間の表示の違いを効率的に最小限に抑えることができます。
    ブラウザ間の差異を気にせずに各要素の必要な部分だけスタイルできます。
  2. デメリット
    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;
}

今日の感想

今日は盆休みということもありHTML5CSS3にチャレンジしてみました。
もともと不勉強だったせいもありますが、新たに知ったことが多すぎて困りました。
本では全体で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-1aap-northeast-1cにサブネットをそれぞれ作成します。
そしてDBサブネットグループを作成しサブネット2つを追加します。
わたしの場合、これまでに作ったものがありそのまま使用します。

f:id:tetsufuru:20190813163613p:plain
上のwp-dbsubnetがサブネットグループです。

f:id:tetsufuru:20190813163951p:plain
サブネットグループには2つのサブネットが追加されています。

RDSの作成

以前作ったRDSは削除したので、再度当時と同じ設定で作り直します。
前回と画面レイアウトが若干変わってますね。
f:id:tetsufuru:20190813170108p:plain

f:id:tetsufuru:20190813230316p:plain DBはMySQLを選び、バージョンも前回と同じにします。
テンプレートも前回同様に"無料利用枠"を選びます。
これ以降はほぼ前回と同様で進みます。
インスタンスのサイズやストレージはデフォルトのままで。
f:id:tetsufuru:20190813170612p:plain f:id:tetsufuru:20190813170856p:plain f:id:tetsufuru:20190813170954p:plain 上記のように作成時はマルチAZは選べません。
これも前回と同じです。

f:id:tetsufuru:20190813171213p:plain 上記の概算月間コストを見ると、db.t2.microインスタンスシングルAZにおける750時間が無料枠の目安だそうです。
マルチAZにして2台構成にすると半分の375時間/月になるんでしょうね。
f:id:tetsufuru:20190813171956p:plain いったん作成し終わりました。

RDSの変更・マルチAZ化

作成したRDSを選択して「アクション」から変更を行います。
下記のような変更画面が表示されます。
f:id:tetsufuru:20190813173058p:plain f:id:tetsufuru:20190813173343p:plain マルチAZに変更して、「変更のスケジュール」を"すぐに適用"にします。
あとは「DBインスタンスの変更」ボタンを押して待つだけです。
10分ほどかかります。
f:id:tetsufuru:20190813225630p:plain 変更が完了すると「マルチ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.

原因をネットで調べるとKarmaJasmineのバージョンが低いのではないか?
と思い、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
最新化

わたしのローカルはKarmaJasmin-coreなどが軒並み低いようです。
Angularの4か5の頃にこのプロジェクトを作ったので、その時から更新されずに古いまま残っていたのかもしれません。
Angular CLIやcoreだけのバージョンを先にあげようとしたのですが、
そうこうしているうちにng update --all --forceを流してしまい、一気に全部のバージョンを上げてしまいました。
そもそも起動しなくなるのではと一瞬ドキっとしましたが無事でした、結果オーライというところでしょうか。
f:id:tetsufuru:20190813042832p:plain

BootStrapの名残りを除去

あともう1か所Angular.jsonファイルのtestの所に昔使っていたbootstrapの項目がまだ残っていました。
これらも邪魔していたようです、きれいに消しました。
f:id:tetsufuru:20190813043903p:plain これでテストが動くようになりました。

現状のテストをすべて無効化

テストは動くようになりましたが、下記のような状態で17/34が失敗とのこと。
自動で作られたコードでも失敗するんですね。
f:id:tetsufuru:20190813094831p:plain

そこでいったんすべてのテストコードを実行しないように、各テストクラス先頭の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していましたので、そちらにわたしも寄せました。 Javajunitに近い感覚で書けそうだという理由もあります。
テストコードについては初期化に失敗していますので、呼び出し時の引数が不足していると思います。
そこで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のことのみ書きますが、getpostputの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件のテストはグリーンになりました。
f:id:tetsufuru:20190813120807p:plain 下記のコマンドでカバレッジもとってみます。
ng test --code-coverage f:id:tetsufuru:20190813121357p:plain まだまだカバレッジが足りませんが、下記のようにcompanyServiceだけは100%となりました。
f:id:tetsufuru:20190813121344p:plain

今日の感想

今日はJasmineにチャレンジしてみました。
こんな感じで書くんですねー!
大変勉強になりました、よし次はcomponentのテストだ。

では、今日もお疲れ様でした。