bats-assertの使用例

<!-- omit in toc -->
# bats-assertの使用例


<!-- omit in toc -->
## Table Of Contents
- [1. はじめに](#1-はじめに)
	- [1-1. Batsコンポーネントのインストール先](#1-1-batsコンポーネントのインストール先)
- [2. 基礎編](#2-基礎編)
	- [2-1. 戻り値のテスト: assert\_success, assert\_failure](#2-1-戻り値のテスト-assert_success-assert_failure)
	- [2-2. 否定する戻り値のテスト: refute](#2-2-否定する戻り値のテスト-refute)
	- [2-3. 標準出力のテスト(1): assert\_output, refute\_output](#2-3-標準出力のテスト1-assert_output-refute_output)
	- [2-4. 標準出力のテスト(2): assert\_line, refute\_line](#2-4-標準出力のテスト2-assert_line-refute_line)
- [3. 発展編](#3-発展編)
	- [3-1. 特定のテストだけを動かす](#3-1-特定のテストだけを動かす)
	- [3-2. テストをスキップする](#3-2-テストをスキップする)
	- [3-3. 標準エラー出力のテストと一時ファイルの利用](#3-3-標準エラー出力のテストと一時ファイルの利用)
	- [3-4. 長いデータ量の出力テスト](#3-4-長いデータ量の出力テスト)
	- [3-5. ファイル更新するテスト](#3-5-ファイル更新するテスト)
- [4. 補足](#4-補足)
	- [4-1. はじめの補足](#4-1-はじめの補足)
	- [4-2. あとがき](#4-2-あとがき)




## 1. はじめに

Batsのヘルパーライブラリー [bats-assert](https://github.com/ztombol/bats-assert) の実用的な使い方を紹介します。

テストコード例示していますので、実際に値を変えながら試してみてください。

:::note info
補足情報はかなり長くなってしまったので[4-1. はじめの補足](#4-1-はじめの補足)に纏めました。興味を持った方はそちらをご覧下さい。
:::


### 1-1. Batsコンポーネントのインストール先
ここでは Batsのコンポーネントはそれぞれ以下にインストールした環境を使用しています。

| Name           | Destination        |
|:---------------|:-------------------|
| bats-core      | `/usr/local`       |
| bats-assert    | `/usr/local/lib`   |
| bats-support   | `/usr/local/lib`   |

適宜、ご自身の環境に合わせて読み替え/書き換えをしてください。

## 2. 基礎編

テストスクリプトコードを以下に示します。以降、このコードを一部分づつ、説明する形を取ります。

```bash:example_assertions02_basic.bats
#!/usr/bin/env bats

BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load

# TARGET=$(target_path $BATS_TEST_FILENAME)
# load $TARGET

@test "Exit code" {
	run true  ; assert_success
	run false ; assert_failure
	run false ; assert_failure 1

	function ret33(){
		return 33
	}
	run ret33 ; assert_failure 33
}

@test "Exit code(refute)" {
	run true  ; refute [ $status -eq 1 ]
	run false ; refute [ $status -eq 0 ]

	function ret33(){
		return 33
	}
	run ret33 ; refute [ $status -eq 22 ]
}

@test "STDOUT: Single line" {
	run echo        ; assert_output ''
	run echo AAAAAA ; assert_output 'AAAAAA'
	run echo A B  C ; assert_output 'A B C'

	run echo 'ERROR: no such file or directory'
	assert_output --partial 'no such'

	run echo 'Foobar v0.1.2'
	assert_output --regexp '^Foobar v[0-9]+\.[0-9]+\.[0-9]$'
	
	
	# refute(not equal)
	run echo        ; refute_output '_'
	run echo AAAAAA ; refute_output 'AAAAAb'
	run echo A B  C ; refute_output 'A B c'

	run echo 'ERROR: no such file or directory'
	refute_output --partial 'SUCCESS'
}

@test "STDOUT: Multi line" {
	run cat <<-STDIN
		1
		22
		333
		44 4
	STDIN

	assert_line 333
	refute_line foo
	
	assert_line --index 0  '1'
	assert_line --index 1  '22'
	assert_line --index 2  '333'
	assert_line --index 3  '44 4'

	assert_equal "${lines[0]}"  '1'
	assert_equal "${lines[1]}"  '22'
	assert_equal "${lines[2]}"  '333'
	assert_equal "${lines[3]}"  '44 4'
}
```


### 2-1. 戻り値のテスト: assert_success, assert_failure

戻り値のアサーションです。

```bash
@test "Exit code" {
	run true  ; assert_success
	run false ; assert_failure
	run false ; assert_failure 1

	function ret33(){
		return 33
	}
	run ret33 ; assert_failure 33
}
```

assert_failureは引数を指定しなければ0以外かをチェックします。

ここでは成功(`$?==0`)に`true`コマンド、失敗(`$?==1`)に`false`コマンドを使っています。また、任意の戻り値を示すために33で返すret33関数を定義して使っています。  

:::note info
尚、`run command; `で繋いで同一行にアサーションを書くのは好みや賛否両論あるかと思いますが、単純なテストで横に長くなりすぎず、縦にバリエーションが続く場合は、実行（入力）とアサーションが並んでいる方が分かりやすくなるのでこのように書いています。
後ろの方では分けて書いたテストも出てきます。
読み易さ、書き易さに応じて使い分けるのが良いかと思います。
:::

これらは直接`assert_equal $status -eq 0`の様にもかけますが、`assert_success`と`assert_failure`を使った書き方の方がExit Codeに関するテストだと明確になるので良いでしょう。


### 2-2. 否定する戻り値のテスト: refute
```bash
@test "Exit code(refute)" {
	run true  ; refute [ $status -eq 1 ]
	run false ; refute [ $status -eq 0 ]

	function ret33(){
		return 33
	}
	run ret33 ; refute [ $status -eq 22 ]
}
```


ここでは、[1-1. Batsコンポーネントのインストール先](#1-1-batsコンポーネントのインストール先)に対して、否定する書き方を示しました。
但し、網羅性の為に記載しましたが、実用する機会は殆どありません。また、分かりにくいので使うのを避けるべきです。


### 2-3. 標準出力のテスト(1): assert_output, refute_output
ここでは、標準出力の結果が一行の場合のアサーションについて書きます。

```bash
@test "STDOUT: Single line" {
	run echo        ; assert_output ''
	run echo AAAAAA ; assert_output 'AAAAAA'
	run echo A B  C ; assert_output 'A B C'

	run echo 'ERROR: no such file or directory'
	assert_output --partial 'no such'

	run echo 'Foobar v0.1.2'
	assert_output --regexp '^Foobar v[0-9]+\.[0-9]+\.[0-9]$'
	
	
	# refute(not equal)
	run echo        ; refute_output '_'
	run echo AAAAAA ; refute_output 'AAAAAb'
	run echo A B  C ; refute_output 'A B c'

	run echo 'ERROR: no such file or directory'
	refute_output --partial 'SUCCESS'
}
```

`assert_output`は標準出力/標準エラー出力の結果のアサーションで、オプションが無ければ完全一致するかをチェックします。
`--partial`は出力結果に特定の文字列が含まれるか、`--regexp`は正規表現でのチェックです。正規表現は拡張正規表現(ERE)が使用でき、部分一致です。(全体としてマッチをテストしたい場合は例示した様にアンカー`^`, `$`を含めた正規表現を指定します。)  

`refute_output`は`assert_output`の否定版です。



### 2-4. 標準出力のテスト(2): assert_line, refute_line
標準出力の結果が複数行の場合のアサーションについて紹介します。

```bash
@test "STDOUT: Multi line" {
	run cat <<-STDIN
		1
		22
		333
		44 4
	STDIN

	assert_line 333
	refute_line foo
	
	assert_line --index 0  '1'
	assert_line --index 1  '22'
	assert_line --index 2  '333'
	assert_line --index 3  '44 4'

	assert_equal "${lines[0]}"  '1'
	assert_equal "${lines[1]}"  '22'
	assert_equal "${lines[2]}"  '333'
	assert_equal "${lines[3]}"  '44 4'
}
```
ここではcatとヒアドキュメントで4行の文字列を出力し、各パターンのアサーションを例示しています。  
`assert_line`は引数を付けない場合、標準出力の行のいずれかに該当の文字列があるかをチェックします。（その否定版が`refute_line`です。）  
`--index n`の様に特定の行の値もチェックできます。これは別の書き方で、`${lines[i]}`をチェックする書き方もできます。  



## 3. 発展編

### 3-1. 特定のテストだけを動かす
Batsへ`-f(--filter) <regex>`を指定することで、特定のテストだけを実行することが出来ます。
前述のテストスクリプト`example_assertions01_basic.bats`を使って、実際に幾つかのパターンを例示します。

<!-- omit in toc -->
#### 3-1-1. 指定なし（全テストの実行）
```bash
$ ./example_assertions01_basic.bats
example_assertions01_basic.bats
 ✓ Exit code
 ✓ Exit code(refute)
 ✓ STDOUT: Single line
 ✓ STDOUT: Multi line

4 tests, 0 failures
```

<!-- omit in toc -->
#### 3-1-2. 「Exit code」を含むテストの実行
```
$ ./example_assertions01_basic.bats -f 'Exit code'
example_assertions01_basic.bats
 ✓ Exit code
 ✓ Exit code(refute)

2 tests, 0 failures
```

<!-- omit in toc -->
#### 3-1-3. 「Exit code」で終わるテストの実行
```bash
$ ./example_assertions01_basic.bats -f 'Exit code$'
example_assertions01_basic.bats
 ✓ Exit code

1 test, 0 failures
```

テストの名前は実行結果の表示だけでなく、この様に指定をする場合もありますので、ある程度分かりやすく、特定しやすい名前にするのが良いでしょう。


### 3-2. テストをスキップする
Batsは`skip`コマンドによってテストを実行しない様に出来ます。  
例えばLinuxだったら実行したい（Linux以外ならテストしないで欲しい）とか、特定のホスト名の場合ならテストを実行しない様にしたい場合などで役立ちます。

まず、`skip`の使い方を見ます。


```bash:example_assertions03_2_skip01.bats
#!/usr/bin/env bats

BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load

@test "my_test" {
	skip 'foo bar'
	run true  ; assert_success
	run false ; assert_failure 1
}
```

<!-- omit in toc -->
#### 実行例
```bash
$ chmod +x example_assertions0b_skip01.bats
$ ./example_assertions0b_skip01.bats
example_assertions0b_skip01.bats
 - my_test (skipped: foo bar)

1 test, 0 failures, 1 skipped
```

`skip`コマンドに引数を与えると、実行時にその値がレポートされます。

実際の使い方ですが、ある条件によってskipコマンドを実行する様に、条件式とskipコマンドを組み合わせて使います。以下、幾つかの想定される用途を見ていきます。

- Linuxなら実行する（Linuxでない場合はスキップ）
- 特定のLinuxディストリビューションならテストを実行する
- 特定のホスト名ならテストを実行する
- コマンドが利用可能ならテストを実行する
- AWS EC2ならばテストしない

<!-- omit in toc -->
#### 3-2-1. Linuxなら実行する（Linuxでない場合はスキップ）

これは`uname`コマンドで実現できます。ユースケースとして、例えば私はWindows 10のgit-bashとLinux(RHEL)を使用して、どちらでもBashとBatsテストが実行できる様にしています。

Windowsで動かすgit-bashは昔のCygwinなど昔WindowsでUnix/LinuxのCLI環境を動かしたい頃と比べ、簡単で安定して動作します。（昔は日本語の文字化けなど動いたものの動作が期待通りならない場合があったりしたものですが、今はGitクライアントを入れるだけで基本的なLinuxのCLIが動きます。）

とは言え、実際のLinuxではないので幾つかのコマンドは使えなかったり、テストが出来ない（実行すると失敗する）場合があります。

この場合は、Linuxではない場合ならskipコマンドを実行する様にテストを書きます。
以下はLinuxかどうかの判定を他のテストでも使う想定で関数化しています。

```bash:example_assertions03_2_skip02.bats
#!/usr/bin/env bats

BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load

function is_linux(){
	if [ $(uname) == Linux ] ; then
		return 0
	fi
	return 1
}

@test "my_test" {
	is_linux || skip 'Not Linux'
	run true  ; assert_success
	run false ; assert_failure 1
}
```


<!-- omit in toc -->
#### 3-2-2. 特定のLinuxディストリビューションならテストを実行する
例えばRHELであれば`/etc/redhat-release`ファイルがあります。また、基本的には`/etc/os-release`ファイルにそれぞれの、Linuxディストリビュートごとの情報が書かれていますので、これらを判断してテストの実行とスキップをコントロール出来ます。
ただし、イレギュラーな環境の場合`/etc/os-release`ファイルがないケースもあるので、そのチェックもした方が良いかと思います。（例えば、git-bashの環境にはこのファイルはありません）

<!-- omit in toc -->
#### 3-2-3. 特定のホスト名ならテストを実行する
ホスト名は`uname -n`, `hostname`, `hostnamectl`, $HOSTNAME環境変数から判断できます。私は経験上、`uname -n`を好んで使用します。

例えばwebサーバだったらテストをしたい場合を例示してみます。ここでホスト名は`web[0-9][0-9].example.com`というルールだったとします。

```bash:example_assertions03_2_skip03.bats
#!/usr/bin/env bats

BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load

function is_websrv(){
	local name=$(uname -n)
	if [[ $name =~ ^web[0-9][0-9]\.example\.com$ ]] ; then
		return 0
	fi
	return 1
}

@test "my_test" {
	is_websrv || skip "Not a Web Server: $(uname -n)"
	run true  ; assert_success
	run false ; assert_failure 1
}
```

ここでは、`skip`コマンドのところを一工夫して、スキップ時のメッセージにホスト名を含めるようにしました。

<!-- omit in toc -->
##### 実行結果
```bash
$ uname -n
rhel94.example.com

$ ./example_assertions03_2_skip04.bats
example_assertions03_2_skip04.bats
 - my_test (skipped: Not a Web Server: rhel94.example.com)

1 test, 0 failures, 1 skipped
```

<!-- omit in toc -->
#### 3-2-4. コマンドが利用可能ならテストを実行する
コマンドが使えれば（インストールされていれば）テストを実行し、それ以外の場合はskipする場合です。コマンドの利用可否はBashの`type`コマンドの戻り値から判別するのが簡便です。（$PATHは通っている前提としています）

```bash:example_assertions03_2_skip04.bats
#!/usr/bin/env bats

BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load

function has_command(){
	type "$1" >/dev/null 2>&1
	return $?
}

@test "my_test" {
	has_command nc || skip 'The nc command is not installed.'
	run true  ; assert_success
	run false ; assert_failure 1
}
```

`has_command`関数で`type`コマンドの標準出力/標準エラー出力を捨てて、戻り値をそのままリターンします。(return文も端折れますが、ここでは明示しています。)

呼び出し側は`nc`と言うコマンドがあるかどうかを例示しています。このコマンドがあるならばテストを実行し、なければスキップします。


<!-- omit in toc -->
#### 3-2-5. AWS EC2ならばテストする

AWS EC2の環境かどうかを判断する例です。AWSの環境とオンプレミス環境扱う方なら、この様な判断をしたいこともあるのではないでしょうか。


```bash:example_assertions03_2_skip05.bats
#!/usr/bin/env bats

BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load

function is_aws_ec2(){
	local manufacturer=${1-$(dmidecode -s system-manufacturer)}
	if [ "$manufacturer" == 'Amazon EC2' ] ; then
		return 0
	fi
	
	return 1
}

@test "my_test" {
	is_aws_ec2 || skip 'Not an AWS EC2 instance.'
	run true  ; assert_success
	run false ; assert_failure 1
}
```

:::note info  
`local manufacturer=${1-$(dmidecode -s system-manufacturer)}`と引数受けしている理由はこの関数自体をテストできる様にしたギミックです。詳細は[3-5. ファイル更新するテスト](#3-5-ファイル更新するテスト)で説明します。
これは、その前までの関数`is_linux`, `has_command`, `is_websrv`では割愛しましたがこれらにも考え方は利用できます。  
:::

`dmidecode -s system-manufacturer`はAWS EC2で動くLinuxならば`Amazon EC2`の結果が返りますのでこれを利用します。

この様に、特定の環境（サーバ、マシン）の場合はテストを実行するかどうかは条件とskipコマンドの組み合わせ方次第です。
テストスクリプトを環境に合わせてそれぞれ準備すると、管理が煩わしいので1つのスクリプトだけになるのが好ましいかと思います。これを実現する手法としてskipコマンドの使用例を紹介しました。



### 3-3. 標準エラー出力のテストと一時ファイルの利用

[2. 基礎編](#2-基礎編)では、便宜上、`assert_output`、`assert_line`のアサーションは標準出力を扱うと記載しましたが、正確には標準出力と標準エラー出力を扱います。
その為、作りたい機能が標準出力と標準エラー出力エラーを使い分け、明確にどちらのストリームの文字列かをテストしたい場合に問題になります。

ですが、Batsでテストできなくはありません。ひと工夫することでテストできますので、例示するコードで紹介します。


```bash:example_assertions03_3_stderr.bats
#!/usr/bin/env bats

BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load


@test "STDERR" {
	function write_stdout_stderr(){
		echo $1
		echo $2 1>&2
	}

	run write_stdout_stderr 1 22
	assert_line --index 0  '1'
	assert_line --index 1  '22'

	tempfile=$( mktemp --tmpdir test_stderr.tmp.XXX )
	write_stdout_stderr 1 22 1>/dev/null 2>$tempfile
	assert_equal $? 0
	run cat $tempfile
	assert_output '22'

	rm -f $tempfile
}
```

このテストでは関数`write_stdout_stderr`で引数1を標準出力へ、引数2を標準エラー出力へ書き出すスタブを使います。

この関数を`run write_stdout_stderr 1 22`の様に実行すると、`assert_line --index n`で標準出力も標準エラー出力もパスすることが分かります。
（標準エラー出力も`assert_line`での対象になっています）

ここで標準エラー出力の結果だけを判断したいとします。
`run write_stdout_stderr 1>/dev/null`の様に標準出力の結果を捨てればよさそうに思えるかもしれませんがこれはうまくいきません。
`run`で実行したコマンドはその時点で標準出力と標準エラー出力を評価するための変数（`${lines[n]}`）へ格納されるためです。

では、どうしたらよいか？ですが、runを使用せず、一時ファイルを利用して標準出力の結果を書き出します。

ここで、一時ファイルの扱いに関して少し触れたいと思います。
一時ファイルは`mktemp`コマンドを利用して、安全に（衝突しない）ファイルが作れます。また長期間アクセスがない場合に自動的に削除されるディレクトリへ作成できます。
更に、テストの最後にはファイルを消すようにしますが、テストが失敗するとそのロジックが通らないため残存します。この時、ファイル名からどのテストで作られたのかわかるように文字列をファイル名に付与できます。

ここでは、`test_stderr.tmp.XXX`の様にSTDERRのテストと分かるような名前を付与し、これをシェル変数`$tempfile`で扱います。

テストに戻ります。`write_stdout_stderr 1 22 1>/dev/null 2>$tempfile`で、標準出力は破棄し、標準エラー出力を一時ファイルに書いています。
次に、念のため、`assert_equal $? 0`で戻り値のチェックをしています。ここでは、runで関数を呼び出していないため、`$staus`にExit Codeがセットされないため、直接`$?`をチェックしています。


次に`run cat $tempfile`で一時ファイルを読んで標準出力へ書き出します。その結果を`assert_output '22'`でチェックしています。

最後に、`rm -f $tempfile`で一時ファイルを削除します。

このように、runを使わずに関数を実行して、一時ファイルに結果を記録してその結果を評価することでストリームごとの結果をチェックできます。


### 3-4. 長いデータ量の出力テスト
出力データ量が長い機能の場合、結果を全てチェックするのは困難になります。
（出来なくはないですが、全量のデータをアサーションでチェックするのはテストコードに期待値のデータを全て書くことになりテストスクリプトの見通しが悪くなります。）

この場合、ハッシュ値でチェックすれば短いコードで確認できます。


```bash:example_assertions03_4_longdata.bats
#!/usr/bin/env bats

BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load


@test "create_data" {
	function create_data(){
		seq 1 1024
		return 0
	}

	tempfile=$( mktemp --tmpdir test_create_data.tmp.XXX )

	create_data > $tempfile
	assert_equal $? 0
	run md5sum -c <<<"45070a9eae28b02bec752e334781359c  $tempfile"
	assert_output "$tempfile: OK"

	rm -f $tempfile
}
```

ここではスタブとして`create_data`関数で1024行のデータを生成し、その結果をチェックしたいとします。

これも[3-3. 標準エラー出力のテストと一時ファイルの利用](#3-3-標準エラー出力のテストと一時ファイルの利用)の様に一時ファイルを利用します。  
`create_data > $tempfile`で、一時ファイルに関数の結果を書き出し、念のため戻り値0を確認します。

その次に、`run md5sum -c <<<"45070a9eae28b02bec752e334781359c  $tempfile"`で`md5sum`コマンドを標準入力で読み込んで期待値かチェックします。
`md5sum`は期待値と異なる場合、戻り値0以外を返すので、そのようなチェックでも構いませんが、今回は標準出力の結果をアサーションに掛けました。


:::note info  
ハッシュ値チェックとしてMD5を扱う`md5sum`を利用しましたが、Linuxではよりビット長の長い`sha224sum`, `sha384sum`, `sha512sum`などハッシュ値計算するコマンドもあります。  
好みのものを使用して下さい。おそらくデータ量次第ですが、基本的にこのような利用であればMD5アルゴリズムで充分かと思います。
:::

期待する値自体は今回の場合は以下の様に取得できます。
```bash
$ seq 1 1024 | md5sum
45070a9eae28b02bec752e334781359c  -
```

実際の関数の場合は一度、一時ファイルに出力するなり単実行した結果をmd5sumにかけてハッシュ値を得ます。
その際、出力結果のデータの妥当性は注意するようにしてください。


このテストだと、「要するに期待通りの結果か？」というニュアンスのテストになります。
違った場合の検出はできますが、その場合、なにがどうおかしいかは分からないので、おそらく、正常なデータと、今回の出力データを比較しながらおかしいところを特定していくことになります。
そういった意味では、ユニットテストより後のテストや回帰テスト（リグレッションテスト）のような毛色のチェックの仕方です。

できるならば、もっと細かい単位でチェック/テストが書かれていてそこでエラーが検出できるようにテストを書く方が好ましいです。
ですが、最終的なテストとして実行する意味合いは意外と多くあるように思います。




### 3-5. ファイル更新するテスト

ファイルを更新する機能と対応するテストについて記載します。

:::note info
ITインフラでは良くBashスクリプトを書きますが、スクリプトで**あるファイルを更新するロジック**を書きたいことがあります。
この時、テストを書こうと思うとどのように書いていいかわからず思考停止することが、私は良くありました。

直接、対象のファイルをテストで更新するわけにはいきません。大問題になります。

ならば、テストが書けないか？というと、一工夫すると機能面でのテストはできます。
:::


```bash:example_assertions03_5_update_file.bats
#!/usr/bin/env bats

BATSLIB=/usr/local/lib
load ${BATSLIB}/bats-support/load
load ${BATSLIB}/bats-assert/load


@test "update_etc-hosts" {
	function update_etc-hosts(){
		local before="$1"
		local after="$2"
		local file="${3-/etc/hosts}"
		sed -i "s/$before/$after/g" $file
		return $?
	}
	tempfile=$( mktemp --tmpdir test_create_data.tmp.XXX )
	seq 1 10 > $tempfile
	run update_etc-hosts 1 2 $tempfile
	assert_success
	run md5sum -c <<<"d50a1cc9633515c3cac4f94381efdb67  $tempfile"
	assert_output "$tempfile: OK"

	rm -f $tempfile
}
```

このテストで例示するのは/etc/hostsを更新する機能を作りたいケースです。
`update_etc-hosts`関数は引数1の文字列を引数2の文字列に書き換えたい、という機能を持たせています。


テストで実際のOSの`/etc/hosts`を書き換えてしまうのは問題です。他の誰も使っていないマシンが確保できるならやりようがありますが、それでも機能面を確認するために実際のOSや他のプロダクションのファイルは更新させたくありません。
「テストでバックアップを取って書き戻す？」、いやそれでも誤ったテストを実行したら良く分からない結果になりますし、書き戻すのも煩わしいですし、何よりテストは安全・気軽に実行できるべきです。


これを一工夫して（ある程度）安全な機能とテストにします。

引数3でファイル名を取り、指定されない場合は、実際に更新したいファイルを定義するよう、シェル変数の初期値を指定する機能を利用します。
`local file="${3-/etc/hosts}"`が該当行です。

なお、`local `は今回の場合はなくてもよいです。関数内で名前空間を閉じるために指定しています。
ポイントは`${foo-DEFAULTVALUE}`です。$fooがない場合、-で指定された以降の値がデフォルト値になります。

このシェル変数のデフォルト値を利用する関数を使うと前述の[3-2. テストをスキップする](#3-2-テストをスキップする)で説明したそれぞれの関数もテストを書くことが出来ます。


## 4. 補足

### 4-1. はじめの補足
`ztombol/bats-assert`はBats(bats-core)でアサーションを簡潔に書ける機能です。bats-coreをベースに、bats-supportと併せて使用することで、簡潔なBashスクリプトのテストコードを書くことができます。

ここでは、前半と後半の２段構成として、最初にやりたいことを軸にどの様にアサーションを書けるか、よく使うものを纏めて示したいと思います。後半ではより実際の利用に際して、Tips的なテクニックを紹介します。

ここでは（少し仰々しいですが）、前半を基礎編、後半を発展編と銘打っています。

前半と後半で例示するために示すBatsスクリプトはテストコードのスクリプトファイルだけの構成<sup>1</sup>で一通りのバリエーションを記載しました。なるべくこれを見れば一通りのやりたいことがリファレンス的に使えるように心掛けています。

前半では戻り値と標準出力(加えて標準エラー出力)をどのようにテストするかを紹介します。
後半は、幾つかの実際の利用に際してのTipsを紹介しています。具体的には標準エラー出力の扱いや、直接ファイルを書き換える関数に対してテストをどのように書けるかを紹介します。  


それぞれ、なるべく実用的な説明になるように心がけました。テストは前提環境が整っていれば全てパスするはずです。
適宜、テストコードの期待値を変えてみて、実際に結果がどうレポートされるか確認してみてください。



<sup>1</sup> 実際の利用に際しては、メインのBashスクリプトとテスト用のBatsスクリプトが分離していることが好ましいかと思います。これは[Batsを利用したBashスクリプトのfizzbuzz](https://qiita.com/hisakug/items/94fdc8ef506a2d321008)で紹介していますのでそちらをご参照ください。


### 4-2. あとがき
この記事では、Batsでのテストで、実用性のある書き方を紹介しました。

他にもTipsとしてテクニックは多くありますが、使用頻度が特に多いと思われるものから順に、一旦、一通りのものが書けたと思っています。  
（なるべく不必要な情報を削るよう心掛けたつもりましたが、長い記事になってしまったことは申し訳なく思います。）

今迄Bashスクリプトと対応するテストを書いたことが無い方が、テストに対する興味を持つようになり、テストを書き始める切っ掛けになったならば、と思います。

また、BatsでのテストはBashスクリプトが表現豊かに記述できますので、実現が難しそうなことであっても工夫次第で実現できることが多くあります。この投稿からそのヒントや気付きを持ってもらえたならば、嬉しく思います。

ありがとうございました。