シェルスクリプトで自動販売機を作る実況中継

## はじめに

表題の通り、シェルスクリプトで自動販売機を作ってみたいと思います。  
仕様はざっくり以下の通りとします。

- 硬貨を投入できる
- 投入金額がいくらか知ることができる
- 飲み物が何種類か用意されている
- 投入金額が足りていれば飲み物を買える
- 投入金額が不足していれば飲み物を買えない
- 投入されたお金は返却できる

## 環境

- Ubuntu on WSL2 (Windows 11)
- GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
- Docker

## 事前準備

### 作業ディレクトリを作成する

適当な場所にディレクトリを作成して、そこで作業します。

```sh
mkdir -vp vending-machine && cd $_
```

### テストフレームワークを用意する

今回はシェルスクリプトの BDD フレームワークである [ShellSpec](https://github.com/shellspec/shellspec) を Docker で利用したいと思います。

https://hub.docker.com/r/shellspec/shellspec

```sh
docker pull shellspec/shellspec:latest
```

初期化を行います。

```sh
docker run -it --rm -u $(id -u):$(id -g) -v "$PWD:/src" shellspec/shellspec --init
```

```text
$ docker run -it --rm -u $(id -u):$(id -g) -v "$PWD:/src" shellspec/shellspec --init
  create   /src/.shellspec
  create   /src/spec/spec_helper.sh
```

テストを実行します。

```text
$ docker run -it --rm -u $(id -u):$(id -g) -v "$PWD:/src" shellspec/shellspec
Running: /bin/sh [sh]


Finished in 0.07 seconds (user 0.05 seconds, sys 0.00 seconds)
0 examples, 0 failures
```

正常に動作しました :tada:

コマンドが長いので `shellspec` というエイリアスを作って呼び出せるようにしてみましょう。  

```sh:~/.bash_aliases
alias shellspec='docker run -it --rm -u $(id -u):$(id -g) -v "$PWD:/src" shellspec/shellspec'
```

書き終わったらプロファイルをロードしなおして `shellspec` で実行してみます。

```text
$ shellspec
Running: /bin/sh [sh]


Finished in 0.04 seconds (user 0.04 seconds, sys 0.00 seconds)
0 examples, 0 failures
```

正常に動作しました :tada:

## 実装開始

前置きが長くなりましたが、実装していきたいと思います。  
テストケースは `spec/vending_machine_spec.sh` に書いていきます。

### 預り金を照会する

初期状態で自動販売機に預けているお金は 0 円なので、`deposit` を実行したら `0` を返してくるように作ってみたいと思います。

```sh:spec/vending_machine_spec.sh
# shellcheck shell=sh

Describe "vending_machine.sh"
  Include lib/vending_machine.sh

  It "initially deposits 0 yen"
    When call deposit
    The output should eq 0
  End
End
```

テストを実行します。

```text
$ shellspec
/bin/sh: .: line 35: can't open 'lib/vending_machine.sh': No such file or directory

Unexpected output to stderr occurred at line 3-4 in 'spec/vending_machine_spec.sh'
Running: /bin/sh [sh]


Finished in 0.19 seconds (user 0.06 seconds, sys 0.00 seconds)
0 examples, 0 failures, aborted by an unexpected error

Aborted with status code [executor: 2] [reporter: 1] [error handler: 102]
Fatal error occurred, terminated with exit status 102.
```

`lib/vending_machine.sh` を作っていませんでした。作ってみます。

```sh:lib/vending_machine.sh
# shellcheck shell=sh
```

```text
$ shellspec
Running: /bin/sh [sh]
F

Examples:
  1) vending_machine.sh initially deposits 0 yen
     When call deposit

     1.1) The output should eq 0

            expected: 0
                 got: ""

          # spec/vending_machine_spec.sh:7

     1.2) WARNING: It exits with status non-zero but not found expectation

            status: 127

          # spec/vending_machine_spec.sh:5-8

     1.3) WARNING: There was output to stderr but not found expectation

            stderr: /bin/sh: deposit: not found

          # spec/vending_machine_spec.sh:5-8

Finished in 0.07 seconds (user 0.04 seconds, sys 0.01 seconds)
1 example, 1 failure


Failure examples / Errors: (Listed here affect your suite's status)

shellspec spec/vending_machine_spec.sh:5 # 1) vending_machine.sh initially deposits 0 yen FAILED
```

1.3\) で `deposit` が見つからないと言われたので作ります。

```sh:lib/vending_machine.sh
deposit() {
  :
}
```

```text
$ shellspec
Running: /bin/sh [sh]
F

Examples:
  1) vending_machine.sh initially deposits 0 yen
     When call deposit

     1.1) The output should eq 0

            expected: 0
                 got: ""

          # spec/vending_machine_spec.sh:7

Finished in 0.07 seconds (user 0.06 seconds, sys 0.00 seconds)
1 example, 1 failure


Failure examples / Errors: (Listed here affect your suite's status)

shellspec spec/vending_machine_spec.sh:5 # 1) vending_machine.sh initially deposits 0 yen FAILED
```

1.1\) で出力結果が異なると言われたので、`deposit` 関数を以下のように修正します。

```sh:lib/vending_machine.sh
deposit() {
  echo 0
}
```

```text
$ shellspec
Running: /bin/sh [sh]
.

Finished in 0.06 seconds (user 0.05 seconds, sys 0.00 seconds)
1 example, 0 failures
```

おめでとうございます :tada:

:::note info
エンジニア界隈では、テストが一つ通るたびに「おめでとうございます」と言い、拍手やクラッカー等でテストの成功を祝う風習があります。よろしくお願いします。
:::

### 100 円玉を 1 枚投入する

次に 100 円玉を 1 枚投入してみましょう。投入するメソッドは `insert` とします。

テストファイルの `Describe` 句に以下のコードを追加します。

```sh:spec/vending_machine_spec.sh
  It "insert 100 yen, then deposit is 100 yen"
    insert 100
    When call deposit
    The output should eq 100
  End
```

```text
$ shellspec
Running: /bin/sh [sh]
.F

Examples:
  1) vending_machine.sh insert 100 yen, then deposit is 100 yen
     1.1) Example aborted (exit status: 127)

            /bin/sh: insert: not found

          # spec/vending_machine_spec.sh:11-15

Finished in 0.07 seconds (user 0.06 seconds, sys 0.00 seconds)
2 examples, 1 failure


Failure examples / Errors: (Listed here affect your suite's status)

shellspec spec/vending_machine_spec.sh:11 # 1) vending_machine.sh insert 100 yen, then deposit is 100 yen FAILED
```

`insert` メソッドを作成します。

```sh:lib/vending_machine.sh
insert() {
  :
}
```

```text
$ shellspec
Running: /bin/sh [sh]
.F

Examples:
  1) vending_machine.sh insert 100 yen, then deposit is 100 yen
     When call deposit

     1.1) The output should eq 100

            expected: 100
                 got: 0

          # spec/vending_machine_spec.sh:14

Finished in 0.07 seconds (user 0.05 seconds, sys 0.00 seconds)
2 examples, 1 failure


Failure examples / Errors: (Listed here affect your suite's status)

shellspec spec/vending_machine_spec.sh:11 # 1) vending_machine.sh insert 100 yen, then deposit is 100 yen FAILED
```

`insert` したら預り金を増やす処理を書きます。  
コードを以下のように変更します。

```sh:lib/vending_machine.sh
# shellcheck shell=sh

_deposit=0

deposit() {
  echo $_deposit
}

insert() {
  money=$1
  _deposit=$((_deposit + money))
}
```

```text
$ shellspec
Running: /bin/sh [sh]
..

Finished in 0.06 seconds (user 0.06 seconds, sys 0.00 seconds)
2 examples, 0 failures
```

おめでとうございます :tada:

### 180 円分の硬貨を投入する

ペットボトルのコーラを飲みたいんですが、最近自販機のコーラは 180 円 ~~ととても高価~~ なので、 ~~仕方なく~~ 180 円入れてみたいと思います。

```sh:spec/vending_machine_spec.sh
  It "insert a 100 yen coin, a 50 yen coin, and three 10 yen coins -> then deposit is 180 yen"
    insert 100
    insert 50
    insert 10
    insert 10
    insert 10
    When call deposit
    The output should eq 180
  End
```

```text
$ shellspec
Running: /bin/sh [sh]
...

Finished in 0.06 seconds (user 0.06 seconds, sys 0.00 seconds)
3 examples, 0 failures
```

おめでとうございます :tada:

### コーラを買う

お待たせいたしました。180 円を入れてコーラを買ってみたいと思います。  
180 円入れて 180 円のコーラを `buy cola` で買うと `cola` がリターンされ、預り金は 0 円になるはずです。

```sh:spec/vending_machine_spec.sh
  It "insert 180 yen -> buy cola (180 yen) -> returns cola and deposit is 0 yen"
    insert 100
    insert 50
    insert 10
    insert 10
    insert 10
    When call buy cola
    The output should eq "cola"
    When call deposit
    The output should eq 0
  End
```

```text
$ shellspec
Running: /bin/sh [sh]
...F

Examples:
  1) vending_machine.sh insert 180 yen -> buy cola (180 yen) -> returns cola and deposit is 0 yen
     When call buy cola

     1.1) The output should eq cola

            expected: "cola"
                 got: ""

          # spec/vending_machine_spec.sh:34

     1.2) When call deposit

            Evaluation has already been executed. Only one Evaluation allow per Example.
            (Use 'parameterized example' if you want a loop)

          # spec/vending_machine_spec.sh:35

     1.3) The output should eq 0

            expected: 0
                 got: ""

          # spec/vending_machine_spec.sh:36

     1.4) WARNING: It exits with status non-zero but not found expectation

            status: 127

          # spec/vending_machine_spec.sh:27-37

     1.5) WARNING: There was output to stderr but not found expectation

            stderr: /bin/sh: buy: not found

          # spec/vending_machine_spec.sh:27-37

Finished in 0.07 seconds (user 0.06 seconds, sys 0.00 seconds)
4 examples, 1 failure


Failure examples / Errors: (Listed here affect your suite's status)

shellspec spec/vending_machine_spec.sh:27 # 1) vending_machine.sh insert 180 yen -> buy cola (180 yen) -> returns cola and deposit is 0 yen FAILED
```

1.2\) で Evaluation は 1 つまでだと言われました。どうやら When を 2 つ以上書いてはいけないようです。

テストケースを「コーラを買うまで」と「コーラを買った後に預り金を確認するまで」の 2 つに分けます。

```sh:spec/vending_machine_spec.sh
  Context "buy cola"
    insert 100
    insert 50
    insert 10
    insert 10
    insert 10
    It "returns cola"
      When call buy cola
      The output should eq "cola"
    End
    It "deposit is 0 yen"
      buy cola
      When call deposit
      The output should eq 0
    End
  End
```

```text
$ shellspec
Running: /bin/sh [sh]
...FF

Examples:
  1) vending_machine.sh buy cola returns cola
     When call buy cola

     1.1) The output should eq cola

            expected: "cola"
                 got: ""

          # spec/vending_machine_spec.sh:35

     1.2) WARNING: It exits with status non-zero but not found expectation

            status: 127

          # spec/vending_machine_spec.sh:33-36

     1.3) WARNING: There was output to stderr but not found expectation

            stderr: /bin/sh: buy: not found

          # spec/vending_machine_spec.sh:33-36

  2) vending_machine.sh buy cola deposit is 0 yen
     2.1) Example aborted (exit status: 127)

            /bin/sh: buy: not found

          # spec/vending_machine_spec.sh:37-41

Finished in 0.08 seconds (user 0.06 seconds, sys 0.00 seconds)
5 examples, 2 failures


Failure examples / Errors: (Listed here affect your suite's status)

shellspec spec/vending_machine_spec.sh:33 # 1) vending_machine.sh buy cola returns cola FAILED
shellspec spec/vending_machine_spec.sh:37 # 2) vending_machine.sh buy cola deposit is 0 yen FAILED
```

先ほどのエラーが消えました :tada:

実装してみます。とりあえず `buy` メソッドが呼ばれたら預り金を 180 円減らして cola を返してみます。

```sh:lib/vending_machine.sh
buy() {
  kind=$1
  _deposit=$((_deposit - 180))
  echo "cola"
}
```

```text
$ shellspec
Running: /bin/sh [sh]
...cola
..

Finished in 0.10 seconds (user 0.08 seconds, sys 0.01 seconds)
5 examples, 0 failures
```

### 水を買う

別の飲み物も買ってみましょう。  
120 円入れて 120 円の水を買います。

```sh:spec/vending_machine_spec.sh
  Context "buy water"
    insert 100
    insert 10
    insert 10
    It "returns water"
      When call buy water
      The output should eq "water"
    End
    It "deposit is 0 yen"
      buy water
      When call deposit
      The output should eq 0
    End
  End
```

```text
$ shellspec
Running: /bin/sh [sh]
...cola
..cola
FF

Examples:
  1) vending_machine.sh buy water returns water
     When call buy water

     1.1) The output should eq water

            expected: "water"
                 got: "cola"

          # spec/vending_machine_spec.sh:50

  2) vending_machine.sh buy water deposit is 0 yen
     When call deposit

     2.1) The output should eq 0

            expected: 0
                 got: "-60"

          # spec/vending_machine_spec.sh:55

Finished in 0.09 seconds (user 0.07 seconds, sys 0.01 seconds)
7 examples, 2 failures


Failure examples / Errors: (Listed here affect your suite's status)

shellspec spec/vending_machine_spec.sh:48 # 1) vending_machine.sh buy water returns water FAILED
shellspec spec/vending_machine_spec.sh:52 # 2) vending_machine.sh buy water deposit is 0 yen FAILED
```

コーラが出てきたうえに預り金が -60 円となってしまいました。  
`buy` メソッドの実装を修正しましょう。

```sh:lib/vending_machine.sh
buy() {
  kind=$1
  if [ "$kind" = "cola" ]; then
    price=180
  elif [ "$kind" = "water" ]; then
    price=120
  fi
  _deposit=$((_deposit - price))
  echo "$kind"
}
```

```text
$ shellspec
Running: /bin/sh [sh]
...cola
..water
..

Finished in 0.14 seconds (user 0.12 seconds, sys 0.00 seconds)
7 examples, 0 failures
```

おめでとうございます :tada:

### 飲み物を買うテストケースをパラメータ化する

コーラ・水を買うテストケースを別々に作りましたので、パラメタライズしてみましょう。

```sh:spec/vending_machine_spec.sh
  Describe "buy something with enough money"
    Parameters
      # description coins kind expected_deposit
      "buy cola" 100,50,10,10,10 cola 0
      "buy water" 100,10,10 water 0
    End

    insert_coins() {
      _coins=$1
      for coin in $(echo "$_coins" | sed -e 's/,/ /g'); do
        insert "$coin"
      done
    }

    It "$1: returns $3"
      insert_coins "$2"
      When call buy "$3"
      The output should eq "$3"
    End
    It "$1: deposit is $4 yen"
      insert_coins "$2"
      buy "$3"
      When call deposit
      The output should eq "$4"
    End
  End
```

```text
$ shellspec
Running: /bin/sh [sh]
....cola
.water
..

Finished in 0.11 seconds (user 0.06 seconds, sys 0.01 seconds)
7 examples, 0 failures
```

成功しました。おめでとうございます :tada:

### 自販機に入れた金額が飲み物の値段より多い場合

次も水を 120 円で買うのですが、200 円投入してみます。購入後の預り金は 80 円となるはずです。

パラメータに以下の行を追加します。

```sh:spec/vending_machine_spec.sh
      "insert 200 yen and buy water" 100,100 water 80
```

```text
$ shellspec
Running: /bin/sh [sh]
.....cola
.water
.water
..

Finished in 0.12 seconds (user 0.11 seconds, sys 0.00 seconds)
9 examples, 0 failures
```

おめでとうございます :tada:

### 自販機に入れた金額が飲み物の値段より少ない場合

次は投入金額が足りない状態で飲み物を買います。
この場合は自販機が `NOT ENOUGH MONEY!!` と叫ぶことにします。このときの預り金は変化しません。

テストコードに以下の行を追加します。なお、`insert_coins` メソッドの定義はファイル冒頭に移動しました。

```sh:spec/vending_machine_spec.sh
  Describe "buy something with not-enough money"
    Parameters
      # description coins kind expected_deposit
      "insert 100 yen and buy cola" 100 cola 100
      "insert 110 yen and buy water" 100,10 water 110
    End

    It "$1: returns $3"
      insert_coins "$2"
      When call buy "$3"
      The output should eq "NOT ENOUGH MONEY!!"
    End
    It "$1: deposit is $4 yen"
      insert_coins "$2"
      buy "$3"
      When call deposit
      The output should eq "$4"
    End
  End
```

```text
$ shellspec
Running: /bin/sh [sh]
.....cola
.water
.water
..Fcola
Fwater
FF

Examples:
  1) vending_machine.sh buy something with not-enough money insert 100 yen and buy cola: returns cola
     When call buy cola

     1.1) The output should eq NOT ENOUGH MONEY!!

            expected: "NOT ENOUGH MONEY!!"
                 got: "cola"

          # spec/vending_machine_spec.sh:65

  2) vending_machine.sh buy something with not-enough money insert 110 yen and buy water: returns water
     When call buy water

     2.1) The output should eq NOT ENOUGH MONEY!!

            expected: "NOT ENOUGH MONEY!!"
                 got: "water"

          # spec/vending_machine_spec.sh:65

  3) vending_machine.sh buy something with not-enough money insert 100 yen and buy cola: deposit is 100 yen
     When call deposit

     3.1) The output should eq 100

            expected: 100
                 got: "-80"

          # spec/vending_machine_spec.sh:71

  4) vending_machine.sh buy something with not-enough money insert 110 yen and buy water: deposit is 110 yen
     When call deposit

     4.1) The output should eq 110

            expected: 110
                 got: "-10"

          # spec/vending_machine_spec.sh:71

Finished in 0.15 seconds (user 0.12 seconds, sys 0.02 seconds)
13 examples, 4 failures


Failure examples / Errors: (Listed here affect your suite's status)

shellspec spec/vending_machine_spec.sh:62 # 1) vending_machine.sh buy something with not-enough money insert 100 yen and buy cola: returns cola FAILED
shellspec spec/vending_machine_spec.sh:62 # 2) vending_machine.sh buy something with not-enough money insert 110 yen and buy water: returns water FAILED
shellspec spec/vending_machine_spec.sh:67 # 3) vending_machine.sh buy something with not-enough money insert 100 yen and buy cola: deposit is 100 yen FAILED
shellspec spec/vending_machine_spec.sh:67 # 4) vending_machine.sh buy something with not-enough money insert 110 yen and buy water: deposit is 110 yen FAILED
```

飲み物が普通に買えてしまいました。`buy` メソッドを以下のように修正します。

```sh:lib/vending_machine.sh
buy() {
  kind=$1
  if [ "$kind" = "cola" ]; then
    price=180
  elif [ "$kind" = "water" ]; then
    price=120
  fi
  if [ $_deposit -lt "$price" ]; then
    echo "NOT ENOUGH MONEY!!"
    return
  fi
  _deposit=$((_deposit - price))
  echo "$kind"
}
```

```text
$ shellspec
Running: /bin/sh [sh]
.....cola
.water
.water
...NOT ENOUGH MONEY!!
.NOT ENOUGH MONEY!!
..

Finished in 0.18 seconds (user 0.13 seconds, sys 0.01 seconds)
13 examples, 0 failures
```

おめでとうございます :tada:

### 預り金を返却する

最後に、自販機に残った預り金を返却する機能を実装します。  
今回は 0 円、20 円、880 円の 3 ケース用意します。

テストコードに以下の行を追加します。  
私の環境ではデバイスを取り外す `eject` コマンドがすでに存在しますので、自販機から返却するメソッドは `eject_` メソッドとします。

```sh:spec/vending_machine_spec.sh
  Describe "eject money"
    Parameters
      # description coins deposit
      "0 yen" "" 0
      "20 yen" 10,10 20
      "110 yen" 100,10 110
    End

    It "$1: eject, then gets $3 yen"
      insert_coins "$2"
      When call eject_
      The output should eq "$3"
    End
  End
```

出力が長いので抜粋版です。

```text
$ shellspec
Running: /bin/sh [sh]
.....cola
.water
.water
...NOT ENOUGH MONEY!!
.NOT ENOUGH MONEY!!
..FFF

Examples:
  1) vending_machine.sh eject money 0 yen: eject, then gets 0 yen
     When call eject_

     1.1) The output should eq 0

            expected: 0
                 got: ""

          # spec/vending_machine_spec.sh:86

     1.2) WARNING: It exits with status non-zero but not found expectation

            status: 127

          # spec/vending_machine_spec.sh:83-87

     1.3) WARNING: There was output to stderr but not found expectation

            stderr: /bin/sh: eject_: not found

          # spec/vending_machine_spec.sh:83-87
```

`eject_` メソッドを実装します。

```sh:lib/vending_machine.sh
eject_() {
  return_money=$_deposit
  _deposit=0
  echo $return_money
}
```

```text
$ shellspec
Running: /bin/sh [sh]
.....cola
.water
.water
...NOT ENOUGH MONEY!!
.NOT ENOUGH MONEY!!
.....

Finished in 0.15 seconds (user 0.12 seconds, sys 0.01 seconds)
16 examples, 0 failures
```

おめでとうございます :tada:

### 飲み物を買ったのち、余ったお金を返却する

ここまでの総まとめとして、お金を入れて飲み物を買い、余ったお金を返却するテストケースを書きます。  
ケースは以下の通りとします。

- 120 円を入れて 120 円の水を買い、お金を返却する
- 200 円を入れて 180 円のコーラを買い、お金を返却する
- 500 円を入れて 180 円のコーラと 120 円の水を買い、お金を返却する

テストコードに以下の行を追加します。

- 飲み物を複数買うメソッド

```sh:spec/vending_machine_spec.sh
  buy_something() {
    _order=$1
    for kind in $(echo "$_order" | sed -e 's/,/ /g'); do
      buy "$kind"
    done
  }
```

- テストケース

```sh:spec/vending_machine_spec.sh
  Describe "insert coins, buy something, then eject money"
    Parameters
      # description coins kinds change
      "insert 120 yen, buy a water" 100,10,10 water "water 0"
      "insert 200 yen, buy a cola" 100,100 cola "cola 20"
      "insert 500 yen, buy a cola and a water" 500 cola,water "cola water 200"
    End

    It "$1"
      insert_coins "$2"
      When call echo "$( (
        buy_something "$3"
        eject_
      ) | tr '\n' ' ' | sed -e 's/ $//')"
      The output should eq "$4"
    End
  End
```

```text
$ shellspec
Running: /bin/sh [sh]
.....cola
.water
.water
...NOT ENOUGH MONEY!!
.NOT ENOUGH MONEY!!
........

Finished in 0.19 seconds (user 0.15 seconds, sys 0.00 seconds)
19 examples, 0 failures
```

おめでとうございます :tada: :tada: :tada:

## 所感

シェルで TDD ってできるんですね。  
TDD 楽しいのでみなさんもやりましょう。

## おまけ

### 完成品

```sh:spec/vending_machine_spec.sh
# shellcheck shell=sh

Describe "vending_machine.sh"
  Include lib/vending_machine.sh

  insert_coins() {
    _coins=$1
    for coin in $(echo "$_coins" | sed -e 's/,/ /g'); do
      insert "$coin"
    done
  }

  buy_something() {
    _order=$1
    for kind in $(echo "$_order" | sed -e 's/,/ /g'); do
      buy "$kind"
    done
  }

  It "initially deposits 0 yen"
    When call deposit
    The output should eq 0
  End

  It "insert 100 yen, then deposit is 100 yen"
    insert 100
    When call deposit
    The output should eq 100
  End

  It "insert a 100 yen coin, a 50 yen coin, and three 10 yen coins -> then deposit is 180 yen"
    insert 100
    insert 50
    insert 10
    insert 10
    insert 10
    When call deposit
    The output should eq 180
  End

  Describe "buy something with enough money"
    Parameters
      # description coins kind expected_deposit
      "buy cola" 100,50,10,10,10 cola 0
      "buy water" 100,10,10 water 0
      "insert 200 yen and buy water" 100,100 water 80
    End

    It "$1: returns $3"
      insert_coins "$2"
      When call buy "$3"
      The output should eq "$3"
    End
    It "$1: deposit is $4 yen"
      insert_coins "$2"
      buy "$3"
      When call deposit
      The output should eq "$4"
    End
  End

  Describe "buy something with not-enough money"
    Parameters
      # description coins kind expected_deposit
      "insert 100 yen and buy cola" 100 cola 100
      "insert 110 yen and buy water" 100,10 water 110
    End

    It "$1: returns $3"
      insert_coins "$2"
      When call buy "$3"
      The output should eq "NOT ENOUGH MONEY!!"
    End
    It "$1: deposit is $4 yen"
      insert_coins "$2"
      buy "$3"
      When call deposit
      The output should eq "$4"
    End
  End

  Describe "eject money"
    Parameters
      # description coins deposit
      "0 yen" "" 0
      "20 yen" 10,10 20
      "110 yen" 100,10 110
    End

    It "$1: eject, then gets $3 yen"
      insert_coins "$2"
      When call eject_
      The output should eq "$3"
    End
  End

  Describe "insert coins, buy something, then eject money"
    Parameters
      # description coins kinds change
      "insert 120 yen, buy a water" 100,10,10 water "water 0"
      "insert 200 yen, buy a cola" 100,100 cola "cola 20"
      "insert 500 yen, buy a cola and a water" 500 cola,water "cola water 200"
    End

    It "$1"
      insert_coins "$2"
      When call echo "$( (
        buy_something "$3"
        eject_
      ) | tr '\n' ' ' | sed -e 's/ $//')"
      The output should eq "$4"
    End
  End
End
```

```sh:lib/vending_machine.sh
# shellcheck shell=sh

_deposit=0

deposit() {
  echo $_deposit
}

insert() {
  money=$1
  _deposit=$((_deposit + money))
}

buy() {
  kind=$1
  if [ "$kind" = "cola" ]; then
    price=180
  elif [ "$kind" = "water" ]; then
    price=120
  fi
  if [ $_deposit -lt "$price" ]; then
    echo "NOT ENOUGH MONEY!!"
    return
  fi
  _deposit=$((_deposit - price))
  echo "$kind"
}

eject_() {
  return_money=$_deposit
  _deposit=0
  echo $return_money
}
```