30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

なぜ E2E テストを書くのか

あまりまとまってないし、大したことは書いていない。このへんの話について知見を持っている人は、いろいろ教えて欲しい。
前提として、フロントエンドエンジニアの立場から書いている。

E2Eテストは高コストだと言われる。書くのも大変だし、メンテナンスも大変。
私はSPAを開発することが多いが、SPAをE2Eテストする場合、APIサーバをどうするのかも考えないといけない。

ではなぜ、E2Eテストを書くのだろうか。なぜ、ユニットテストだけではダメなのだろうか。

どこまで投資するかはともかく、E2Eテストは書いたほうがいいと思う。書くに越したことはない。自然にそう思う。
でもその理由を、上手く言語化できない。
しかし高いコストをかけて導入する以上、ちゃんと理由を整理しておきたい。

手間のかかる手動テストをある程度代替できるから、だろうか。
UIのあるソフトウェアを作っている人は、必ず、手動でテストすると思う。
SPAのコードをある程度書き上げたとき、それをブラウザで動作確認することなく「出来た」と見做すことはないだろう。TDDで開発していて全てのテストをパスした状態になったとしても、当然、手動や目視で動作を確認するはず。
最終成果物はあくまでも、ソフトウェアだからだ。関数やコンポーネントに対していくらユニットテストを書いても、それはあくまでも、一つ一つの部品の動作を保証したに過ぎない。ソフトウェアの動作を保証することにはならない。だから、リリースや納品の前に必ず、ユーザーと同じ環境(ブラウザやスマホなど)で、手動テストを行う。
そもそも現実的には、ユニットテストのカバレッジを100%に近づけることは困難であり、抜け漏れがあったりそもそもユニットテストが十分に書かれていなかったりする。であれば尚更、手動テストによる動作確認は必須になる。

しかし、手動テストは高コストである。いちいち人間が、実際に操作して、目視でチェックして、動作確認しなければならない。
その手動テストを、自動化出来る。それがE2Eテストを導入するメリットなのかもしれない。
つまり、最終成果物であるソフトウェアに対するテストを、ユニットテストのように自動化できるというメリット。

とはいえ、E2Eテストは手動テストの完全な代替にはなり得ない。
リリース前にはやっぱり、ちゃんと動作しているか、デグレが発生していないかを確認するために、手動でテストするだろう。
E2Eテストも所詮は人間が書いたものだから、間違っている可能性がある。そこに全幅の信頼を寄せて手動テストをしない、というのは怖い。
そもそも、E2Eテストでカバーできる範囲はたかが知れている。全てのユースケースに対してE2Eテストを書くのは、コストがかかり過ぎる。

ではなぜ、「それでもE2Eテストは書いたほうがいい」と思うのだろうか。
不完全とはいえ、これまで手動テストが担っていた役割を部分的に代替できるからだと思う。それにより、開発や運用を効率化できる。逆に言えば、この効率化以上にメンテナンスのコストがかかるなら、E2Eテストを導入する意味はない。
自分が思いついた役割は、以下の4つ。

  • 手動テストをやるほどではない変更を加えたときの安全ネット
  • ソフトウェアが壊れたときにすぐに検知できる
  • ソフトウェアに変更を加えたときの影響範囲を確認できる
  • E2Eテストを通すことが、ソフトウェアを直す際の最初の目標になる

手動テストをやるほどではない変更を加えたときの安全ネット

ここまで「手動テストをしないと不安だ」と書いてきたが、常にそういう気持ちになるわけではない。
ちょっとした変更、影響範囲を完全に把握できていると思えるレベルの変更、くらいであれば、わざわざ手動テストする必要はない。というか、そんなことで面倒な手動テストはしたくない。
例えば、マイページに表示する文言のタイポを修正した場合、そのタイポが修正されていることさえ確認すればいいわけで、マイページの各種機能が正しく動いているかを確認する必要はない。したくない。
こういうときにE2Eテストがあれば、手動テストの手間を省けるし、それでいて、動かなくなったときにそのことに気付くことが出来る。
変更による影響が心配なときは、手動テストをする。E2Eテストが活きるのは、影響がないと思われるとき。予想通り影響がないことを確認するためにE2Eテストを実行する。

ソフトウェアが壊れたときにすぐに検知できる

CIでE2Eテストを実行することで、ソフトウェアが壊れたことをすぐに検知できるという利点がある。
時間が経ってから一部のUIが動いていないことに気付くと、原因を追うのが難しくなる。だがE2Eテストなら実行のためのコストは低いので、高頻度で行うことが可能になり、破壊にすぐに気付ける可能性が高まる。

ソフトウェアに変更を加えたときの影響範囲を確認できる

E2Eテストが壊れるだろうなと思いながら破壊的変更を加えたとき、予想通りに壊れることで、変更の影響範囲に対する自分の理解が正しいことを確認できる。
予想に反して壊れなかったり、予想とは違う壊れ方をした場合は、自分の理解が間違っているか、テストが間違っている可能性がある。それにより、作業前は気付けていなかった要素に気付くことができ、開発の手戻りが少なくなる。

E2Eテストを通すことが、ソフトウェアを直す際の最初の目標になる

一度壊したソフトウェアは再び動くようにしなければならないが、E2Eテストを通すことで、少なくとも以前と同じように動くようになったことは保証できる。
E2Eテストがない場合、直ったと思う度に手動で確認し、直っていなかった場合はまたそれを繰り返すことになるが、E2Eテストを書いておけば、この確認作業を自動化できる。

***

整理すると、フロントエンドのコードに変更を加えた時の安全ネット、という感じだろうか。
E2Eテストは、意図せず壊したときにアラートを出したり、意図的に壊した時にそれを確認したりするために、書く。

そしてこの「壊した」というのは、フロントエンドのコードのことだ。バックエンドは関係ない。
そう考えると、APIはモックサーバーで十分に思える。
フロントエンドのコードが壊れてしまったことに気付けるようにするのがE2Eテストの目的なのだから、バックエンドはモックで構わないはず。

E2Eテストがコストの元を取れるかは、対象のソフトウェアによる。
シンプルなソフトウェアで手動テストが簡単に行えるなら、高いコストを支払ってE2Eテストをメンテナンスする価値はない。
ソフトウェアの運用期間が長ければ長いほど、E2Eテストがあることによる効率化が活きていくる。

『Software Design 2019年6月号』の「思わず実践したくなるシェル&シェルスクリプト」を読んだ

初心者向けに書かれているので、読みやすかった。

コマンドがファイルとして定義されていることすら知らないレベルだったので、勉強になった。
分かっているようで分かっていなかった「パスを通す」の意味も分かったし。分かってしまえば、なんで分からなかったんだろうという感じではあるが。

シェルスクリプトで何が出来るのか分かっていないと自動化の発想すら出てこないので、基礎が分かってよかった。

gihyo.jp

以下、各章のメモ。

第1章 楽しく学ぶシェルの基本

例外もあるが、コマンドというのは、基本的にはファイル。
whichコマンドを使うと、コマンドのファイルがどこにあるか調べることが出来る。

$ which date
/bin/date
$ which git
/usr/local/bin/git
$ which node
/Users/$USER/.nodebrew/current/bin/node

標準出力、標準入力、標準エラー出力の3つは「標準ストリーム」と呼ばれる。

パイプ|は、左側のコマンドの標準出力を、右側のコマンドの標準入力にする。

リダイレクト>は、左側に書いたコマンドの標準出力先を、右側に書いたファイルにする。
>&2は、標準出力ではなく標準エラー出力を使うようにする命令。

;でコマンドをつなげることで、複数のコマンドをまとめて実行できる。これを「リスト」という。

$ echo a; echo b
a
b

「リスト」をまとめてパイプに渡すには、括弧で囲む。この記法を「サブシェル」という。

$ echo a; echo b | echo-sd -s
a
_人人_
> b <
 ̄Y^Y^ ̄
$ (echo a; echo b) | echo-sd -s
_人人_
> a <
> b <
 ̄Y^Y^ ̄

ちなみにecho-sdコマンドは以下の手順で利用できるようになる。

$ curl -OL https://git.io/echo-sd
$ sudo install -m 0755 echo-sd /usr/local/bin/echo-sd

第2章 テキスト処理で役立つシェル・テクニック

筆者の実際の原稿を題材に、テキスト処理を行う。
原稿は以下のリポジトリで公開されている。

github.com

grepコマンドで検索できる。wcコマンドは、入力された文字列の情報を返す。
以下は、カレントディレクトリ以下にある全ての.rstファイルを対象にできるを検索し、該当した行数、単語数、バイト数、を表示している。

$ grep できる *.rst | wc
87     150    8572

wc-lオプションを渡すと、行数のみが表示される。

$ grep できる *.rst | wc -l
87

awkFNRは、各ファイルの行数を持っている。

$ awk 'FNR==2' *.rst
開眼 シェルスクリプト 第1回
開眼シェルスクリプト 第2回
開眼シェルスクリプト 第3回
開眼シェルスクリプト 第4回
...

第3章 基本から押さえるシェルスクリプト

シェルスクリプトとは、シェルで実行するコマンドを順番に記述したテキストファイルのこと。
シェルスクリプトを用意することで、複雑なコマンドを簡単に実行できるようになる。

$ bash hoge.bashのようにシェル(この例ではbash)の引数にシェルスクリプトを渡すと、そのシェルスクリプトが実行される。

$ chmod +x hoge.bashでシェルスクリプトに実行権限を付与できる。これで、$ ./hoge.bashのようにシェルスクリプトを実行できるようになる。

./のようなディレクトリの指定をせずにシェルスクリプトを実行するには、そのシェルスクリプトをコマンド検索パスに含める必要がある。
コマンド検索パスはPATHという環境変数に入っているので$ echo $PATHで見れる。
ディレクトリを指定せずにシェルスクリプトを実行すると、このコマンド検索パスの中から探す。そのため、hoge.bashがここに含まれていなければ、$ hoge.bashとしたときにcommand not foundになってしまう。

$HOME/bin/PATHに含めたい場合は、$ PATH="${PATH}:${HOME}/bin/とする。
この状態でhoge.bash$HOME/bin/に入れておくと、$ hoge.bashを実行できる。

だがコマンド検索パスはログインする度に設定しないといけないので、.bash_profileに書くなどしてログイン時に自動的に反映されるようにしておくとよい。

位置変数とは、シェルスクリプトの実行時に渡した引数が格納されている変数。
$1$2のように、$と引数の順番の組み合わせになっている。
hoge.bashの内容が以下のときに$ hoge.bash abc xyzとすると、abcxyzが表示される。

echo $1
echo $2

コマンドは、実行結果を示す「終了ステータス」を返す。
コマンドが成功した場合は0を、エラーが発生した場合は0以外を返す。

シェルスクリプトのなかでexitコマンドを使うと、その引数を終了ステータスとして返し、コマンドが終了する。
以下のように書くと、abを表示したあと、終了ステータスとして0を返す。
exitでコマンドを終了するためcは表示されない。

echo a
echo b
exit 0
echo c

直前に実行したコマンドの終了ステータスは$?という変数に入っている。

$ foo.bash
-bash: foo.bash: コマンドが見つかりません
$ echo $?
127

ifforなどの制御構文も扱える。条件式にはtestコマンドを使うことが多い。

unix時間は$ date "+%s"で取得できる。

この特集では特に触れられていなかった(サンプルには記述されていた)が、シェルスクリプトの1行目には#!/bin/bashなどのシバンを書いたほうがいいはず。

#!/bin/sh は ただのコメントじゃないよ! Shebangだよ! - Qiita

第4章 効率的で安全なファイル操作の秘訣

set -eは、エラーが発生したらすぐにシェルスクリプトを終了させる。
set -uは、未定義の変数を使った場合はエラーにする。
この2つを組み合わせたset -euをシェルスクリプトの冒頭に書いておくことで、記述ミスした場合はすぐにエラーになり、シェルスクリプトが意図せぬ操作を行ってしまうことを防げる可能性が高まる。

以下のコマンドで、パス直下のディレクトリやファイルをファイルサイズの順に並べて表示できる。

$ du -s パス | sort -nr | awk '{print $NF}' | xargs du -sh

ファイル操作は安全が第一なので、削除や上書きは自動化しない。

Appendix 1 シェルの「展開」をマスターしよう

bashには「展開」という機能がある。*も展開の一種で、「パス名展開」という。

ブレース展開

{}でくくられた文字を展開する。区切り文字として,..を用いる。

$ echo a{x,y}b
axb ayb
$ echo a{1..3}b
a1b a2b a3b

x..y..zとすると、xから始まりyに到達するまで、xずつインクリメントしていく。

$ echo a{1..8..3}b
a1b a4b a7b

チルダ展開

~の後ろにつけた文字列をユーザー名と解釈し、そのユーザーのホームディレクトリパスを展開する。
~だけの場合は、実行したユーザーのホームディレクトリパスを展開する。

$ cd ~Guest
$ pwd
/Users/Guest

パラメータと変数展開

先頭に$をつけると、変数を展開できる。

$ hoge=HOGE
$ echo $hoge
HOGE

コマンド置換

$(コマンド)とすると、コマンドの実行結果を展開する。

$ echo 今日の日付は $(date '+%Y%m%d') です
今日の日付は 20190715 です

算術式展開

$((計算式))とすると、計算式の結果を展開する。

$ echo 10 / 2 の結果は $((10/2)) です
10 / 2 の結果は 5 です

単語の分割

展開された文字列を、改行や空白を区切り文字にして分割する。

パス名展開

*?[]をパターンとして認識しマッチしたファイルを展開する。

  • *
    • 任意の文字列にマッチ
  • ?
    • 任意の1文字にマッチ
  • []
    • 間に挟まれた任意の1文字にマッチ
$ ls *.txt
work_1.txt  work_10.txt work_2.txt  work_3.txt  work_4.txt  work_5.txt
$ ls work_?.txt
work_1.txt  work_2.txt  work_3.txt  work_4.txt  work_5.txt
$ ls work_[13].txt
work_1.txt  work_3.txt

プロセス置換

コマンドの出力結果を、別のコマンドの引数として扱える。
<(コマンド)を入力として、>(コマンド)を出力として、扱う。

$ diff <(echo -e 'a\nb\nc') <(echo -e 'a\nb\nz') -u
--- /dev/fd/63  2019-07-15 20:47:14.000000000 +0900
+++ /dev/fd/62  2019-07-15 20:47:14.000000000 +0900
@@ -1,3 +1,3 @@
 a
 b
-c
+z

Appendix 2 シェルのカスタマイズ方法

プロンプトは環境変数PS1で設定されている。

$ echo $PS1
\h:\W \u\$

noclobber

noclobberオプションを有効にすると、>によるファイルの上書きを禁止できる。
有効になっているかは、$ set -o | grep noclobberで確認できる。
$ set -Cで有効にできる。

$ set -o | grep noclobber
noclobber       off
$ set -C
$ set -o | grep noclobber
noclobber       on
$ echo hoge > foo.txt
$ echo fuga > foo.txt
-bash: foo.txt: 存在するファイルを上書きできません

nounset

未定義の変数を参照すると、空文字が返ってくる。

$ echo a${FOO}b
ab

nounsetオプションを有効にすると、未定義の変数を参照した際にエラーが発生するようになる。
$ set -uで有効になる。

$ set -o | grep nounset
nounset         off
$ set -u
$ set -o | grep nounset
nounset         on
$ echo a${FOO}b
-bash: FOO: 未割り当ての変数です