今度は客先でコンテナ勉強会
今日すること
こんにちはふるてつです。
ただいま客先の勉強会にてDockerを勉強中で、まずは入門としてこちらのサイト「入門Docker」を勉強しております。
https://y-ohgi.com/introduction-docker/
今回は少なめですが、「コンポーネント」 メニューの中から「volume」ページをもくもくとしました。
またいつものように勉強した内容を書きます。
ボリュームはデータを永続化するための機能になります。
1. Volume
Volumeには2つの種類が存在するそうです。
1-1. Data Volume
Volume は Docker Container のライフサイクルの外で管理されるファイル/ディレクトリとのことです。
ではまず Volume の方を試してみます、下記のコマンドでボリュームをマウントしてみます。
docker run -v /tmp/text ubuntu touch /tmp/text/hogefugapiyo
ところがこちらを流しても、なにもおこりません🤢
"hogefugapiyo"というファイルができてエクスプローラーで見えるはずですが、どこにもファイルができません。
わたしの場合ホストOSがWindowsなので、どうやら基本的にC:ドライブを含んだ書き方にしないとならない模様です。
というわけでやむなく下記のように-v <ホストOSのディレクトリ>:<コンテナのディレクトリ>
と書き替えました。
こちらはファイルができました。
docker run -v /c/tmp:/tmp ubuntu touch /tmp/text/hogefugapiyo
下図のようにエクスプローラーでファイルが見えました、よしよし ✨
その昔、VirtualBoxでも同じようなことができてましたねぇ、少し思い出しました。
あとはvolume が作成されたか確認のために、docker volume ls
を実行します。
うーむ、しかし volume は1件も表示されません。
volumeのところはなんだか難しいですね、Windows版のDocker
だと表示されないのかも🤔
ここは気を取り直して次にいきます。
1-2. Data Volume Container
次は、Data Volume Container。
他のDocker Container で指定されているVolumeを参照するための機能とのことです。
1-1 の Data Volume は他のコンテナからは見えないんですね多分。
コンテナ同士で値を引き渡すときなどに使うのでしょう。
では下のコマンドを試します。
docker run --name volume-test -v /tmp/test ubuntu touch /tmp/test/{hoge,fuga,piyo}
今度はエラーになりました。
Docker
のubuntu
だからか、複数ファイルのtouch
コマンドを受け付けていないようです。
docker run --name volume-test -v /tmp/test ubuntu touch /tmp/test/{hoge,fuga,piyo}
発生場所 行:1 文字:72
+ ... --name volume-test -v /tmp/test ubuntu touch /tmp/test/{hoge,fuga,piy ...
+ ~
パラメーター一覧に引数が存在しません。
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : MissingArgument
仕方がないので、こちらも1ファイルだけに書き直します。
C:ドライブのところも同じく書き直します。
docker run --name volume-test -v /c/tmp:/tmp ubuntu touch /tmp/tesst/piyo
今度はエラーになりません。
では、別のコンテナを新しく起動して、さきほど作成した volume-test コンテナのファイルへアクセスできるか確認してみます。
docker run ubuntu ls -l /tmp/test
なるほど確かにファイルを認識できていません。
docker run ubuntu ls -l /tmp/test
ls: cannot access '/tmp/test': No such file or directory
次はData Volume Container
の方のコマンドを実行します。
docker run --volumes-from volume-test ubuntu ls -l /tmp/test
docker run --volumes-from volume-test ubuntu ls -l /tmp/test
total 0
-rwxr-xr-x 1 root root 0 Oct 2 11:29 piyo
こちらではファイルを認識しました。
volume には自コンテナ内のみと他コンテナから見れるものと2種類あるんですね。
今日の感想
今回もまた「入門Docker」の内容をそのまま、まとめた内容になりました。
volumeのところは少し難しく途中で詰まることがあり、その分内容も少し短くなりました。
あっさりした内容で申し訳ありませんが、次は頑張ります✨
それではまた
今夜は社内AWSもくもく会2 - Elastic Beanstalkをもういちど
今日すること
こんにちはふるてつです。
今回もElastic Beanstalk
による構築レスなサイト作りをおこないます。
前回は思ったように扱えず、サブネットやセキュリティーグループが新たに数個増えよくわからない状態になりました。
そこで今回は事前にサブネットやセキュリティーグループを準備しておくことにしました。RDS
も今回は作っておきます。
前回の内容と重複するところがありますが最初から書いていきます。
Elastic Beanstalkの作成
AWSにログインし、サービス一覧からElastic Beanstalk
をクリックします。
上の画面にて右上の方にある「新しいアプリケーションの作成」をクリックします。
上記の画面が表示されますのでアプリケーション名を入力して「作成」ボタンをクリックします。
環境の設定画面がでてきます。「今すぐ作成しましょう。」のリンクをクリックします。
上記は"ウェブサーバー環境"を選択します。
下記の画面が表示されます。
上記ウェブサーバー環境の作成は以下のように登録します。
- 環境名:EbWordPress-env
- ドメイン:ebwordpress-env
- プラットフォーム:事前設定済みプラットフォームでPHP
- アプリケーションコード:コードのアップロード
(事前にダウンロードしておいたZIPファイルをアップロードします) ここで「より多くのオプションの設定」をクリックします。
オプションの設定
オプションの設定は下記の順番で設定したほうが良いです。
最初にインスタンスを設定しようとするとセキュリティグループを既存のものから選択できませんでしたので。
- ネットワーク
- セキュリティ
- インスタンス
ネットワークの設定
WordPressに作った既存のVPCを選択します。
インスタンスサブネットは2つのAZのそれぞれPublicの方のサブネットを選びます。
セキュリティの設定
セキュリティの設定ですがサービスロールのところが3種類ほど選べます。
このロールがどう違うのかはよく分かりませんが、いったん「aws-elasticbeanstalk-service-role」にしました。
インスタンス の設定
WordPress用のセキュリティグループを選択します。
この設定をネットワークより前のタイミングでおこなうとインスタンスセキュリティグループのところに「default」しか出てきません。
今回は既存のものが出てきましたのでそちらを選びます(「WP-Web-DMZ」です)
そして最後に環境の作成画面に戻ってきたら「環境の作成」ボタンをクリックします。
今回は一発で無事作成できました。
Elastic Beanstalk
も正常に動いています、WordPress
も動くようになりました。
結果
今回の結果としてはサブネットが増えたりはしなくなりました。
しかし「ebWordpress-env」というセキュリティグループが増えてました。
EC2インスタンスの設定のところで指定したセキュリティグループの内容が複製されて新たにできているようです。
すべてのアプリケーションから「EB-WordPress」の詳細を確認します。
緑色の付箋?みたいなところをクリックします。
下記画面のインスタンスセキュリティグループを見ると、2種類のセキュリティグループが指定されています。
これはおそらくそういう仕様なんでしょうね。
であれば、はじめに指定した「WP-Web-DMZ」はもう不要なので削除したほうが良さそうです。
感想
前回よりわたしElastic Beanstalk
の操作がうまくなりました✨
余談ですが最後に確認した「EB-WordPress」の詳細画面には他のメニューで「モニタリング」や「ログ」があり、他に色々と使える機能がありそうでした。
しかし私の書籍ではElastic Beanstalk
はいったん終わりで次のテーマに移ります。
もう少し勉強したかったのですが、それは別途またの機会に。
それではまた😎
モダンコーディング入門 - HTML5とCSS3 - 最後
今日すること
こんにちは、ふるてつです。
🍉盆休みを使って下の本でHTML5
とCSS3
を勉強中でしたが盆中に終わらず、いまも勉強を継続しています、
今回もその中で新しく知ったことを書きます。
書籍中では3種類のレイアウトのサイトを作るのですが最後3つ目のレイアウトになりました。
今回は個人のポートフォリオサイトや商用のランディングページなどで多く見られる、1枚構成のシングルページサイトを作ります。
作ったサイトですが、下のように「タイトル」、「自己紹介」、「作品紹介」、「スキル紹介」、「問い合わせフォーム」と縦にならんでいきます。
実際に出来上がったサイトはこちらです、せっかくですのでGithub pagesで見れるようにしています(ボタンはクリックしても動きません)
https://tetsujifurukawa.github.io/learning_modernCodingOfHtml5Css3/singlepage-layout/index.html
● display:table
書籍も終盤になりあまりこれ知らなかったということはなくなってきました。
display プロパティの所のみはじめてだったので書きました。
display プロパティには block、inline、inline-block の他に、table、table-cell という値を設定でき、
display:table を使用すると従来のテーブルレイアウトのようなことをCSS
だけで実現できます。
これはわたしはじめて知りました。
Bootstrap
を使ったりCSS Grid Layout
のような手法でないと、似たようなことはできないと思ってました、
知らないというのは怖いですねぇ🤢
書き方は下記で、親要素のCSS
にdisplay: table;
を設定して、子要素にdisplay: table-cell;
を設定します。
HTMLの例
<div class="parent">
<div class="child">hoge1</div>
<div class="child">hoge2</div>
<div class="child">hoge3</div>
</div>
CSSの例
.parent{
display: table;
width: 100%;
}
.child{
display: table-cell;
}
● スマートフォン対応(レスポンシブ)
スマートフォン対応はメディアクエリ
を使用します。
メディアクエリ
は使ったことがありますので、全然知らないようなことはなかったです。
参考までにどんな作業をしたか箇条書きにしました。
- display:tableで横並びにした写真は、display:tableを解除して縦並びにする。
- 文字サイズは少し小さめにする。
- 上下の余白を少し狭くする。
- button:hoverは無効にする。
携帯サイズだと下記のようになります。
今日の感想
盆休みに始めた書籍の勉強ですが最後の3種類目が9月にやっと終わりました。
なかなかに勉強しがいがありました、新しく知ったことも沢山ありました。
しっかり忘れないようにして、役立てていこうと思います。
では、今日もお疲れ様でした。
Angular8 で Web アプリを作ろう - Angular もくもく会 in Fukuoka
こんにちはふるてつです。
昨日は ng_fukuoka さんが開催する「Angular もくもく会 in Fukuoka #9」に参加しました。
19時からもくもく開始で、20時からLTが2つほどありました。
ng-fukuoka.connpass.com
もくもく会の様子
会場は株式会社ベガコーポレーションさんで、このような感じでした。
常連さんの他に、Angularをこれから始められる方が新たに2~3名来られてました。
LTの内容
●まずはわたくしのLTの番。
Angularもくもく会でのLTは2回目ですが、話下手なのでやはり緊張します。
テーマは主にJasmine
での単体テストについてです。
これまでブログで書いたJasmine
についての内容をまとめた感じで話しました。
speakerdeck.com
●次は ng_fukuoka 代表の新福さんのLT。
NgRxを使った状態管理’についてです。
使ってみたいですね、すごく面白そうでした。
もっと勉強せねば、と思いました。
speakerdeck.com
今日の感想
無事LTを終えてホッとしました。
だいぶ緊張もしましたが、質問も少しいただけたし、苦手ながらやはり話をして良かったと思いました。
見直したほうが良い所も見つかりましたし、ありがたく思います。
次回もまたLTできるようレベルアップせねばと思う昨日でした✨
では。
今度は客先でコンテナ勉強会
今日すること
こんにちはふるてつです。
ただいま客先の勉強会にてDockerを勉強中で、まずは入門としてこちらのサイト「入門Docker」を勉強しております。
https://y-ohgi.com/introduction-docker/
今週は「コンポーネント」 メニューの中から「container」、「network」ページをもくもくとしました、では勉強した内容を書いていきます。
1. Container
言わずと知れたDocker Container
です。
Docker Image
がスナップショットだとするとDocker Container
はその「スナップショットから起動したプロセス」だとのこと。
意識する点はDocker Container
は1つのコマンドをフォアグラウンドで動かすように設計されていることだそうです。
1-1. ライフサイクル
Docker Container
の状態は5つあるとのことです。
- Image
- RUNNING
- STOPPED
- PAUSED
- DELETED
4番目のPAUSED
はユーザーがコマンドを発行しないとならない模様なので、あまり覚える必要はないかもしれません。
2. Network
Dockerはネットワークの振る舞いを定義することが可能で、デフォルトでは2種類のNetwork Driver が存在するそうです。
- bridge
- host
ネットワークのところは少し難しそうですね。
昔、VirtualBoxの時にもブリッジとかホストとか出てきて困惑しましたけど、同じものですかねー、いやだなあ🤢
とりあえず1番目のbridge
というのだけを覚えようかとは思います。
2-1. ネットワークを試す
● デフォルトで存在するネットワークの確認
docker network ls
コマンドでDocker
が管理しているNetwork
一覧を出力します。
docker network ls
NETWORK ID NAME DRIVER SCOPE
578fc5351093 bridge bridge local
4b353041c00c host host local
f5018c1f7037 none null local
3件表示されました。bridge
とhost
以外にnoneというのもあります。
これらはデフォルトで存在するものと思います。
次にホスト側のネットワークも確認します。
勉強しているサイトではip a
コマンドですが、わたしはWindows環境なのでipconfig -all
で確認します。
DockerNATというネットワークがありました、多分これですね。
Windows IP Configuration
~ 中略 ~
Ethernet adapter vEthernet (DockerNAT):
Connection-specific DNS Suffix . :
Description . . . . . . . . . . . : Hyper-V Virtual Ethernet Adapter #2
Physical Address. . . . . . . . . : xx-xx-xx-xx-xx-xx
DHCP Enabled. . . . . . . . . . . : No
Autoconfiguration Enabled . . . . : Yes
IPv4 Address. . . . . . . . . . . : xx.xx.xx.xx
Subnet Mask . . . . . . . . . . . : xxx.xxx.xxx.xxx
Default Gateway . . . . . . . . . :
DNS Servers . . . . . . . . . . . : fec0:0:0:ffff::1%1
fec0:0:0:ffff::2%1
fec0:0:0:ffff::3%1
NetBIOS over Tcpip. . . . . . . . : Enabled
● 新しいネットワークの作成
次は新しいbridge
ネットワークを作成してみます。
docker network create myapp
コマンドを実行します。
docker network create myapp
ffb3874673689da21d1e10002140f92d5f40c2b1fd1fe085821eb7429a9b44e5
networkに myapp が増えていることを確認します。
docker network ls
コマンド
docker network ls
NETWORK ID NAME DRIVER SCOPE
578fc5351093 bridge bridge local
4b353041c00c host host local
ffb387467368 myapp bridge local
f5018c1f7037 none null local
myappが増えました。
ホスト側のネットワークにも追加されたことを確認しようとしましたが、違いは見れませんでした。
LinuxとWindows環境の違いだと思います。
● Networkへnginxを参加させる
通信を受けるためのサーバーとしてnginx
を myapp ネットワークに構築します。
docker run --name nginx --network=myapp -d nginx
● AmazonLinux2を起動し、nginx
コンテナへ接続する
bridge
ネットワークの場合、同一ネットワークのコンテナにはコンテナ名で名前解決が可能だそうです。
nginx
と疎通できるか myapp ネットワーク内にAmazonLinux2
イメージでコンテナを起動し、 curl を実行してみます。
docker run --network=myapp -it amazonlinux:2 curl nginx:80
docker run --network=myapp -it amazonlinux:2 curl nginx:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
なるほど確かに疎通できました。
● 新しくネットワークを作成し、疎通できないことを確認する
では myapp2 というネットワークを作成し、nginx2 という命名でnginx
を起動します。
docker network create myapp2
docker run --name nginx2 --network=myapp2 -d nginx
myapp ネットワークに所属しているAmazonLinux2
から curl を実行し、疎通できないことを確認します。
$ docker run --network=myapp -it amazonlinux:2 curl nginx2:80
curl: (6) Could not resolve host: nginx2
なるほどたしかに疎通できませんでした。
今日の感想
今回もあいかわらず「入門Docker」の内容をそのまままとめた内容になりました。
コンテナのページは少ししかなかったので、
主にネットワークについて勉強しましたが、bridge
を使用すれば大体がつながりそうですね。
今回勉強してネットワークはそれほど恐れることはないかもと思いました。
それではまた
今夜は社内AWSもくもく会2 - Elastic Beanstalk
今日すること
こんにちはふるてつです。
夏も終わりですねぇ🍉、もう9月に入りました。
今回はElastic Beanstalk
のお話です。
Elastic Beanstalk
による構築レスなサイト作りをおこないます。
少し前にEC2
を使用してWordPress
サイトを構築しましたが、同じことを、Elastic Beanstalk
で行います。
しかしあまりうまく使えなかったので軽い失敗😢の記事になります。
WordPressのZIP入手
まずWordPress
のZIP版を入手します。
AWSで作業する前に下記のサイトから入手しておきます。
https://ja.wordpress.org
Elastic Beanstalkの作成
AWSにログインし、サービス一覧からElastic Beanstalk
をクリックします。
上の画面にて右上の方にある「新しいアプリケーションの作成」をクリックします。
上記の画面が表示されますのでアプリケーション名を入力して「作成」ボタンをクリックします。
環境の設定らしき画面がでてきました。「今すぐ作成しましょう。」のリンクをクリックします。
上記は"ウェブサーバー環境"を選択します。
下記の画面が表示されます。
上記ウェブサーバー環境の作成は以下のように登録します。
- 環境名:EbWordPress-env
- ドメイン:hogehoge_ebwordpress-env
(ドメイン名は一意なものにしないとおこられます) - プラットフォーム:事前設定済みプラットフォームでPHP
- アプリケーションコード:コードのアップロード
(事前にダウンロードしておいたZIPファイルをアップロードします)
書籍ではここで同時にネットワークの設定や、RDS
の作成もおこなうように書いてあったのですが、
書籍に掲載されている画面と実際の画面が若干違い、良くわからなかったため「環境の作成」ボタンのほうをクリックしました。
書籍通りにするには「より多くのオプションの設定」をクリックすべきでした🤢
しかし一応アプリケーションが一つできました、まだ間もないのでステータスは"Pending"になっていますが数分したらグリーンになりました。
インスタンスもできました。
しかしRDS
をそもそも作っていないので、WordPress
は動きません。
というわけでもう一度最初からやりなおして、今度は「より多くのオプションの設定」を使用するようにします。
数回ほどやり直し
今度はアプリケーション名は"EbWordPress-env2"にします。
最初からやり直して、Webサーバー環境の作成で「より多くのオプションの設定」ボタンをクリックします。
上記の画面が表示されます。
- インスタンス
- セキュリティ
- ネットワーク
- データベース
上記のうち4項目を設定しました、そして「環境の作成」ボタンをクリックします。
すると今度はエラーが出ました、セキュリティグループを作ることができない?だとか。
しかたないのでネットワークあたりの設定を見直して、もう一度"EbWordPress-env3"を作ります。
結局3でも、4でもエラーになり、面倒くさくなってアプリケーション名を"aaaa"にした頃にうまく動きました。
どこが結局悪かったかは申し訳ありませんがよく分かりません。
WordPress
サイトも動きました。
できた環境について
RDS
が自動で作られましたが、識別子がランダムな文字列なのがあまりうれしくないですね。
RDS
の情報とセキュリティを見ると、サブネットが3つ設定されています、なぜでしょうね。
あとVPCセキュリティグループは自動で作られたもの?か、作った覚えのないものが設定されています。
上記の画面ですが、VPCメニューからサブネットの一覧をみると確かに3つ増えています。
セキュリティグループも増えました。
(できれば事前に自前で作ったものを使用したいです、どこかで操作を間違えたかも)
感想
今回はElastic Beanstalk
のお話になりました。
しかしわたし的にうまくいかないことが多く、最後に出来上がった環境は少しぐちゃぐちゃになりました。
環境を一度に作れるのは便利ですが、RDS
などは事前に作っておいたほうが良い気もします。
次回もう一度チャレンジしたいと思います。
それではまた
Angular8 で Web アプリを作ろう - Jasmine - Componentのテスト その2 DOM
今日すること
こんにちは、ふるてつです。
前回、前々回とJasmine
のユニットテストについて書いていきました。
今回はComponent
の DOM 部分のテストになります。
テストをするComponentの概要
今回テストをするのは前回と同様に会社マスターの一覧を検索する画面です。
すごく簡単です、検索条件は「会社名」、「会社名カナ」、「削除フラグ」の3です。
画面は下記になります(レイアウトはざっとしか作っていません今後きれいにします)
DOM のテストを作るにあたり、下記の公式リファレンスを熟読しました。
https://angular.jp/guide/testing
「コンポーネントの DOM のテスト」という章がありますので、 もしこれからテストコードを書かれる方はまず最初にこちらを参考にすると良いと思います。
テストをするComponentのHTMLとコード
前回はソースコードのみでしたが、今回はHTMLも掲載します。
それぞれ長いのでたたんでおきます。
テストをするComponentのHTMLはこちら(company-list.component.html)
<div class="mainColor">
<div class="wrapper mainBody">
<form [formGroup]="mainForm">
<!-- Screen Title Area -->
<div class="titleArea">
{{ 'companyListScreen.title' | translate }}
</div>
<!-- Search Conditions Area -->
<mat-card>
<mat-card-content>
<app-error-messages></app-error-messages>
<div id="searchConditionsArea">
<!-- -------------------- 1 -------------------- -->
<div id="searchCondition1">
<mat-form-field class="form-field">
<input id="companyName" matInput type="text" formControlName="companyName"
placeholder="{{ 'companyListScreen.companyName' | translate }}">
</mat-form-field>
</div>
<div id="searchCondition2">
<mat-form-field class="form-field">
<input id="companyKana" matInput type="text" formControlName="companyKana"
placeholder="{{ 'companyListScreen.companyKana' | translate }}">
</mat-form-field>
</div>
<div id="searchCondition3">
<label class="labelFor">{{ 'companyListScreen.deleted' | translate }}</label>
<mat-checkbox id="deleted" formControlName="deleted"></mat-checkbox>
</div>
</div>
</mat-card-content>
</mat-card>
<!-- Button Area -->
<div id="searchButtonArea">
<div id="paginatorArea">
<mat-paginator [length]="resultsLength" [pageSize]="50" [pageSizeOptions]="[10, 50, 100]"></mat-paginator>
</div>
<div id="newBtnArea">
<button mat-raised-button color="primary" id="newBtn" class="btn" (click)="onNew()"
type="button">{{ "companyListScreen.newBtn" | translate }}
</button>
</div>
<div id="clearBtnArea">
<button mat-raised-button color="primary" id="clearBtn" class="btn" (click)="onClear()"
type="button">{{ "companyListScreen.clearBtn" | translate }}
</button>
</div>
<div id="searchBtnArea">
<button mat-raised-button color="primary" id="searchBtn" class="btn" type="submit"
(click)="onSearch()">{{ "companyListScreen.searchBtn" | translate }}
</button>
</div>
</div>
<!-- evaluationResult Area-->
<div id="evaluationResult">
<div class="loading-shade" *ngIf="isLoadingResults">
<mat-spinner class="loading-spinner" *ngIf="isLoadingResults"></mat-spinner>
</div>
<div class="example-container">
<table mat-table *ngIf="resultsLength>0" [dataSource]="searchCompanyDtos">
<ng-container matColumnDef="companySeq">
<th mat-header-cell *matHeaderCellDef style="width: 10%;">
{{ "companyListScreen.searchResult.companySeq" | translate }}
</th>
<td mat-cell *matCellDef="let element"> {{element.companySeq}} </td>
</ng-container>
<ng-container matColumnDef="companyName">
<th mat-header-cell *matHeaderCellDef>
{{ "companyListScreen.companyName" | translate }}
</th>
<td mat-cell *matCellDef="let element"> {{element.companyName}} </td>
</ng-container>
<ng-container matColumnDef="companyKana">
<th mat-header-cell *matHeaderCellDef>
{{ "companyListScreen.companyKana" | translate }}</th>
<td mat-cell *matCellDef="let element"> {{element.companyKana}} </td>
</ng-container>
<ng-container matColumnDef="companyAddress1">
<th mat-header-cell *matHeaderCellDef>
{{ "companyListScreen.searchResult.companyAddress1" | translate }}</th>
<td mat-cell *matCellDef="let element"> {{element.companyAddress1}} </td>
</ng-container>
<ng-container matColumnDef="numOfEmployee">
<th mat-header-cell *matHeaderCellDef>
{{ "companyListScreen.searchResult.numOfEmployee" | translate }}</th>
<td mat-cell *matCellDef="let element"> {{element.numOfEmployee}} </td>
</ng-container>
<ng-container matColumnDef="deleted">
<th mat-header-cell *matHeaderCellDef>
{{ "companyListScreen.deleted" | translate }}</th>
<td mat-cell *matCellDef="let element"> {{element.deleted}} </td>
</ng-container>
<ng-container matColumnDef="evaluationSetting">
<th mat-header-cell *matHeaderCellDef>
{{ "companyListScreen.searchResult.evaluationSetting" | translate }}</th>
<td mat-cell *matCellDef="let element"> {{element.evaluationSetting}} </td>
</ng-container>
<ng-container matColumnDef="createTime">
<th mat-header-cell *matHeaderCellDef>
{{ "companyListScreen.searchResult.createTime" | translate }}</th>
<td mat-cell *matCellDef="let element"> {{element.createTime|date:'medium':timezone:locale}}
</td>
</ng-container>
<ng-container matColumnDef="createUser">
<th mat-header-cell *matHeaderCellDef>
{{ "companyListScreen.searchResult.createUser" | translate }}</th>
<td mat-cell *matCellDef="let element"> {{element.createUser}} </td>
</ng-container>
<ng-container matColumnDef="updateTime">
<th mat-header-cell *matHeaderCellDef>
{{ "companyListScreen.searchResult.updateTime" | translate }}</th>
<td mat-cell *matCellDef="let element"> {{element.updateTime|date:'medium':timezone:locale}} </td>
</ng-container>
<ng-container matColumnDef="updateUser">
<th mat-header-cell *matHeaderCellDef>
{{ "companyListScreen.searchResult.updateUser" | translate }}</th>
<td mat-cell *matCellDef="let element"> {{element.updateUser}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayCompanyListColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayCompanyListColumns;" (click)="listClicked(row)"></tr>
</table>
</div>
</div>
</form>
</div>
</div>
テストをする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]);
}
}
基本的な DOM の操作方法
レファレンスを見る限り下記のような感じが良いかと思いました。
箇条書きにしてみます。
const debugElement: DebugElement = fixture.debugElement;
まずfixture
のdebugElement
(Debug用のElement)を取得
ちなみにこのfixture
はbeforeEach
の中でTestBed
のcreateComponent()メソッドから作られます。const queriedElement = debugElement.query(By.css(' id or class name' ));
つぎにcssのセレクタを使用して目的の要素を取り出します。const htmlElement: HTMLElement = queriedElement.nativeElement;
実際に DOM を確認・操作したくなった時には、nativeElement
プロパティを通して行います。
DOM のテストケースの追加
では実際にテストケースを追加してみます。
①初期表示のテスト
●検索条件の名前
まずは検索条件の名前をテストしてみます。
検索条件の部分はHTML中では下記のように書いています。
判定できそうなところがplaceholder
しかありませんので、<div></div>
タグ全体を取り出します。
そして<div></div>
のテキストの中に日本語で "企業名" が含まれているかテストします。
<div id="searchCondition1">
<mat-form-field class="form-field">
<input id="companyName" matInput type="text" formControlName="companyName"
placeholder="{{ 'companyListScreen.companyName' | translate }}">
</mat-form-field>
</div>
下記がテストコードです。
<div></div>
の個所はdebugElement.query(By.css('#searchCondition1'))
で取得します。
そして最後にhtmlElement.textContentで中にあるテキスト文字を取得しテストします。
実際に動かすとテキスト文字は日本語にはなっておらず | translate より前の 'companyListScreen.companyName' が入ってました。
これは他言語化の為のtranslate
サービスを spy にしていて、うまく日本語に変換されていない為と思われます。
やりたかったことと若干違いましたが、これでもテストにはなっているので今回はこれで良しとしました✨
it('should set company name with searchCondition1', () => {
const debugElement: DebugElement = fixture.debugElement;
const queriedElement = debugElement.query(By.css('#searchCondition1'));
const htmlElement: HTMLElement = queriedElement.nativeElement;
expect(htmlElement.textContent).toContain('companyListScreen.companyName');
});
●検索条件の初期値
次は検索条件それぞれの初期値です。
「企業名」「企業カナ」は初期値は空で、「削除済み」のチェックボックスはチェックOFFです。
it('verify initial value of searchConditions', () => {
const debugElement: DebugElement = fixture.debugElement;
let queriedElement = debugElement.query(By.css('#companyName'));
let htmlInputElement: HTMLInputElement = queriedElement.nativeElement;
expect(htmlInputElement.textContent).toEqual('');
queriedElement = debugElement.query(By.css('#companyKana'));
htmlInputElement = queriedElement.nativeElement;
expect(htmlInputElement.textContent).toEqual('');
queriedElement = debugElement.query(By.css('.mat-checkbox-inner-container'));
htmlInputElement = queriedElement.nativeElement;
expect(htmlInputElement.checked).toBeUndefined();
});
今回「削除済み」のチェックボックスのcssセレクタだけ、クラス指定(.mat-checkbox-inner-container)にしています。
これはAngular Material
のチェックボックスは、ブラウザで表示されたときに、書いたコードとは別のコードを生成して表示する為でした。
実際にわたしが書いたチェックボックスにはIDを振ったのですが、こちらが使えずしかたなくデバックで調べたクラスを指定しました。
Chromeでデバッグすると上記のように「mat-checkbox-inner-container」というクラスの要素が出来ています。
②初期表示以外のテスト
●検索条件の入力
次は検索条件に実際に入力してみます。
htmlInputElement
の value に期待値をセットして、dispatchEvent(new Event('input'))
で入力イベントを発生させます。
(入力イベントを発生させないと思ったように変数 companyName にバインドしないのでご注意ください)
そしてType Script側の変数 companyName に期待値が等しく入っているかをチェックしました。
it('should entry company name', () => {
const debugElement: DebugElement = fixture.debugElement;
const queriedElement = debugElement.query(By.css('#companyName'));
const htmlInputElement: HTMLInputElement = queriedElement.nativeElement;
const expectedEntry = 'abcd1234日本語';
htmlInputElement.value = expectedEntry;
htmlInputElement.dispatchEvent(new Event('input'));
expect(component.companyName.value).toEqual(expectedEntry);
});
●HttpParamの値
最後は CompanyService の検索時に引数で渡すHttpParam
に、検索条件の DOM の値が正しくセットされるかをテストします。
まず3種類の検索条件にそれぞれの期待値を入力します。
その後、createHttpParamsメソッドを実行してHttpParamが正しいかを検証します。
it('should create http param', () => {
const debugElement: DebugElement = fixture.debugElement;
let queriedElement = debugElement.query(By.css('#companyName'));
const htmlInputElement: HTMLInputElement = queriedElement.nativeElement;
const expectedEntry = 'abcd1234日本語';
htmlInputElement.value = expectedEntry;
htmlInputElement.dispatchEvent(new Event('input'));
queriedElement = debugElement.query(By.css('#companyKana'));
const htmlInputElementKana: HTMLInputElement = queriedElement.nativeElement;
const expectedEntryKana = 'アイウエオカキクケコ';
htmlInputElementKana.value = expectedEntryKana;
htmlInputElementKana.dispatchEvent(new Event('input'));
queriedElement = debugElement.query(By.css('.mat-checkbox-inner-container'));
const htmlInputElementDeleted: HTMLInputElement = queriedElement.nativeElement;
htmlInputElementDeleted.click();
fixture.detectChanges();
const conditions = {
companyName: expectedEntry,
companyKana: expectedEntryKana,
deleted: 'true',
pageSize: '50',
pageIndex: '0',
};
const paramsOptions = { fromObject: conditions };
const expectedHttpParams = new HttpParams(paramsOptions);
expect(component['createHttpParams']()).toEqual(expectedHttpParams);
});
●テストコード全体
これまでのテストコード全体を下に記します、みなさまのご参考になればと思います。
テストコード(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.
*/
it('should set company name with searchCondition1', () => {
const debugElement: DebugElement = fixture.debugElement;
const queriedElement = debugElement.query(By.css('#searchCondition1'));
const htmlElement: HTMLElement = queriedElement.nativeElement;
expect(htmlElement.textContent).toContain('companyListScreen.companyName');
});
it('should set company kana with searchCondition2', () => {
const debugElement: DebugElement = fixture.debugElement;
const queriedElement = debugElement.query(By.css('#searchCondition2'));
const htmlElement: HTMLElement = queriedElement.nativeElement;
expect(htmlElement.textContent).toContain('companyListScreen.companyKana');
});
it('should set deleted with searchCondition3', () => {
const debugElement: DebugElement = fixture.debugElement;
const queriedElement = debugElement.query(By.css('#searchCondition3'));
const htmlElement: HTMLElement = queriedElement.nativeElement;
expect(htmlElement.textContent).toContain('companyListScreen.deleted');
});
it('verify initial value of searchConditions', () => {
const debugElement: DebugElement = fixture.debugElement;
let queriedElement = debugElement.query(By.css('#companyName'));
let htmlInputElement: HTMLInputElement = queriedElement.nativeElement;
expect(htmlInputElement.textContent).toEqual('');
queriedElement = debugElement.query(By.css('#companyKana'));
htmlInputElement = queriedElement.nativeElement;
expect(htmlInputElement.textContent).toEqual('');
queriedElement = debugElement.query(By.css('.mat-checkbox-inner-container'));
htmlInputElement = queriedElement.nativeElement;
expect(htmlInputElement.checked).toBeUndefined();
});
it('should entry company name', () => {
const debugElement: DebugElement = fixture.debugElement;
const queriedElement = debugElement.query(By.css('#companyName'));
const htmlInputElement: HTMLInputElement = queriedElement.nativeElement;
const expectedEntry = 'abcd1234日本語';
htmlInputElement.value = expectedEntry;
htmlInputElement.dispatchEvent(new Event('input'));
expect(component.companyName.value).toEqual(expectedEntry);
});
it('should entry company kana', () => {
const debugElement: DebugElement = fixture.debugElement;
const queriedElement = debugElement.query(By.css('#companyKana'));
const htmlInputElement: HTMLInputElement = queriedElement.nativeElement;
const expectedEntry = 'アイウエオカキクケコ';
htmlInputElement.value = expectedEntry;
htmlInputElement.dispatchEvent(new Event('input'));
expect(component.companyKana.value).toEqual(expectedEntry);
});
/** The material check box is different from normal. */
it('should entry deleted', () => {
const debugElement: DebugElement = fixture.debugElement;
const queriedElement = debugElement.query(By.css('.mat-checkbox-inner-container'));
const htmlInputElement: HTMLInputElement = queriedElement.nativeElement;
htmlInputElement.click();
fixture.detectChanges();
expect(component.mainForm.value.deleted).toBe(true);
});
it('should create http param', () => {
const debugElement: DebugElement = fixture.debugElement;
let queriedElement = debugElement.query(By.css('#companyName'));
const htmlInputElement: HTMLInputElement = queriedElement.nativeElement;
const expectedEntry = 'abcd1234日本語';
htmlInputElement.value = expectedEntry;
htmlInputElement.dispatchEvent(new Event('input'));
queriedElement = debugElement.query(By.css('#companyKana'));
const htmlInputElementKana: HTMLInputElement = queriedElement.nativeElement;
const expectedEntryKana = 'アイウエオカキクケコ';
htmlInputElementKana.value = expectedEntryKana;
htmlInputElementKana.dispatchEvent(new Event('input'));
queriedElement = debugElement.query(By.css('.mat-checkbox-inner-container'));
const htmlInputElementDeleted: HTMLInputElement = queriedElement.nativeElement;
htmlInputElementDeleted.click();
fixture.detectChanges();
const conditions = {
companyName: expectedEntry,
companyKana: expectedEntryKana,
deleted: 'true',
pageSize: '50',
pageIndex: '0',
};
const paramsOptions = { fromObject: conditions };
const expectedHttpParams = new HttpParams(paramsOptions);
expect(component['createHttpParams']()).toEqual(expectedHttpParams);
});
});
function fillSearchCriteria(component: CompanyListComponent) {
component.companyName.setValue('a');
component.companyKana.setValue('a');
component.deleted.setValue(true);
}
今日の感想
今日はComponent
のテストその2ということで、DOM 側のテストについて書きました。
ひととおり完成したと思いブログを書き始めたのですが、書き終わる今になって、
検索結果の一覧のテストを追加したほうが良い気もしてきました。
Jasmine
をはじめて間もないので、もっと良いやり方があればまだまだ工夫していきたいと思います。
少し落ち着いたらそのうちまたJasmine
について記事を書きたいと思います。
では、今日もお疲れ様でした。