Scala Conference in Japan 2013 にスポンサーとして参加させていただきました。

個人的に Scala は、認知度や採用事例の面でこれからの言語だと思っていましたが、200名の一般参加枠が一瞬で埋まったことや、各社のScalaの事例を聞き、今後確実に広がっていく言語だと感じました。

技術的な面では、Akka のインパクトが相当強かったです。ちょうど今仕事で取り組んでいるところで使えそうな仕組みでしたので、非常に勉強になりました。あとは Play のライブコーディングが凄かった。

また、リクルーティングセッションにも参加させて頂きました。思ったよりも参加者が少なかったですが、意外と仕事で Scala を使える会社が多いんでしょうか。。マジで切実なので仕事で Scala 使いたい方はお声がけして頂けると嬉しいです。Scala Conference から来たと言えば一次面接が免除になりますよー。ガチで。条件とかはこちらから確認できます。webから応募もできますが、入力フォームがやたら面倒なので面倒臭い人は @shitai246_ までメンション飛ばして頂いても大丈夫です。

あああと、スタッフの方が着ていた格好いいパーカーのプレゼント企画をやってるそうです。超欲しい。

最後に、このような場を用意して頂いた運営の皆様、スピーカーの皆様、本当にありがとうございました。

この記事はTDD Advent Calendar jp: 2012の14日目の記事です。前日はposaunehmさんのJUnit実践入門 MSTest用パッチ #TddAdventJpでした。

プログラマであれば、多かれ少なかれシェルスクリプトを書いた経験はあると思います。でも、シェルスクリプトでユニットテストを書いたことのある人は意外に少ないのではないでしょうか。

シェルスクリプトでユニットテストを書くには?

shunit2 を使います。ダウンロードして解凍してexamplesの下にあるシェルを実行すればサンプルが動きます。

$ sh ./equality_test.sh
testEquality

Ran 1 test.

OK

使い方

test で始まるファンクションがテスト対象になります。また、テストスクリプトの最後でshunit2を読み込む必要があります。

#! /bin/sh

# test で始まるファンクションがテスト対象になる
testSample() {
  # ファイルを吐けるか確認
  echo "test" > ./test.txt
  assertEquals $? 0
}

# test で始まらないファンクションはテスト対象にならない
sampleTest() {
  echo "test2" > ./test2.txt
  assertEquals $? 1
}

# shunit2 を読み込みます。これは最後に書かないといけない
. ./shunit2-2.1.6/src/shunit2

実行結果

$ sh ./sample_test.sh
testSample

Ran 1 test.

OK

setUp や tearDown なども準備されています。

#!/bin/sh

# テスト前処理
oneTimeSetUp() {
  # テストに使うディレクトリを作る
  mkdir -p ./test/
}

# 各テストの前処理
setUp() {
  # テストに使うファイルを作る
  echo "test" > ./test/test.txt
}

# 各テストの後処理
tearDown() {
  # テストに使ったファイルを消す
  rm ./test/*
}

# テスト後処理
oneTimeTearDown() {
  # 最後にディレクトリ毎消しておく
  rm -rf ./test
}

testSample1() {
  cnt=`cat ./test/test.txt | wc -l`
  assertEquals ${cnt} 1

  echo "abc" >> ./test/test.txt
  cnt=`cat ./test/test.txt | wc -l`
  assertEquals ${cnt} 2
}

testSample2() {
  cnt=`cat ./test/test.txt | wc -l`
  assertEquals ${cnt} 1

  echo "abc" > ./test/test.txt
  cnt=`cat ./test/test.txt | wc -l`
  assertEquals ${cnt} 1
}

. ./shunit2-2.1.6/src/shunit2

実行結果

$ ls
sample_test.sh  sample_test2.sh  shunit2-2.1.6
$ sh ./sample_test2.sh
testSample1
testSample2

Ran 2 tests.

OK
$ ls
sample_test.sh  sample_test2.sh  shunit2-2.1.6

assertEquals 以外にも、assertTrue とか assertNotNull とか、そこそこ揃っています。

#! /bin/sh

testSample() {
  assertEquals $? 0
  assertTrue "true"
  assertTrue 0
  assertFalse "false"
  assertFalse 1
  assertNotNull "aaa"
}

. ./shunit2-2.1.6/src/shunit2

実行結果

$ sh ./sample_test3.sh
testSample

Ran 1 test.

OK

shunit2の欠点を克服する

実際にshunit2を試してみた方ならすぐに気づいたと思いますが、shunit2にはTDDerにとって非常に重大な欠点があります。それは、テストに失敗したとき赤くならないこと。これは気分的にも盛り上がりませんし、シェルであれば色つきで標準出力することも簡単にできるので、shunit2 に少し手を入れて改善してしまいましょう。

shunit2-2.1.6/src/shunit2 の 808行目から始まる _shunit_generateReport() でテスト結果を表示しているので、修正します。

# Generates the user friendly report with appropriate OK/FAILED message.
#
# Args:
#   None
# Output:
#   string: the report of successful and failed tests, as well as totals.
_shunit_generateReport()
{
  _shunit_ok_=${SHUNIT_TRUE}

  # if no exit code was provided one, determine an appropriate one
  [ ${__shunit_testsFailed} -gt 0 \
      -o ${__shunit_testSuccess} -eq ${SHUNIT_FALSE} ] \
          && _shunit_ok_=${SHUNIT_FALSE}

  echo
  if [ ${__shunit_testsTotal} -eq 1 ]; then
    echo "Ran ${__shunit_testsTotal} test."
  else
    echo "Ran ${__shunit_testsTotal} tests."
  fi

  _shunit_failures_=''
  _shunit_skipped_=''
  [ ${__shunit_assertsFailed} -gt 0 ] \
      && _shunit_failures_="failures=${__shunit_assertsFailed}"
  [ ${__shunit_assertsSkipped} -gt 0 ] \
      && _shunit_skipped_="skipped=${__shunit_assertsSkipped}"

  if [ ${_shunit_ok_} -eq ${SHUNIT_TRUE} ]; then
## OK のときは緑色にする
#    _shunit_msg_='OK'
    _shunit_msg_='\e[32mOK\e[m'
    [ -n "${_shunit_skipped_}" ] \
        && _shunit_msg_="${_shunit_msg_} (${_shunit_skipped_})"
  else
## NG のときは赤色にする
#    _shunit_msg_="FAILED (${_shunit_failures_}"
#    [ -n "${_shunit_skipped_}" ] \
#        && _shunit_msg_="${_shunit_msg_},${_shunit_skipped_}"
#    _shunit_msg_="${_shunit_msg_})"

    _shunit_msg_="\e[31mFAILED (${_shunit_failures_}"
    [ -n "${_shunit_skipped_}" ] \
        && _shunit_msg_="${_shunit_msg_},${_shunit_skipped_}"
    _shunit_msg_="${_shunit_msg_})\e[m"
  fi

  echo
## -e オプションをつけてエスケープを有効にする
#  echo ${_shunit_msg_}
  echo -e ${_shunit_msg_}
  __shunit_reportGenerated=${SHUNIT_TRUE}

  unset _shunit_failures_ _shunit_msg_ _shunit_ok_ _shunit_skipped_
}

これでOKやFAILEDがちゃんと緑や赤で表示されるようになりました。

さいごに

実際にTDDでシェルスクリプトを書いてみたら、シェルのことを何も知らなったことに気づきました。TDDをやるとなんとなくでコードが書けなくなるので、理解の浅い言語でTDDをすると非常に勉強になります。TDDでシェルスクリプトを書いたことがない人は、ぜひ一度書いてみてはどうでしょうか。

この記事はPlay or Scala Advent Calendar 2012の13日目の記事です。Advent Calendar も折り返し地点になります。

ScalaはJavaに比べて非常に簡潔なコードを書けることが魅力のひとつですが、簡潔に書こうとして想定しない動作になってしまったことのひとつやふたつ誰にでもあるはず。

Exsample

さて、以下のREPLの結果を見てください。

scala> val list1 = List(1, 2, 3)
list1: List[Int] = List(1, 2, 3)

scala> val list2 = List(4, 5, 6)
list2: List[Int] = List(4, 5, 6)

scala> val list3 = list1:::list2.filter(_ % 2 == 1)
list3: List[Int] = List(1, 2, 3, 5)

俺はList(1, 2, 3) と List(4, 5, 6) を結合して奇数のみ抽出しようとしたのに、なぜか2がフィルターできていなかった…。なんてことになることがあります。

何が起こっているのか?

Scalaist の皆さんならご存知の通り、Scalaだと ’1 + 2′ の ‘+’ のように、演算子に見えるものも全て関数です。また、全ての関数が演算子と同じような書き方で呼び出せます。平たく言うと、ピリオドを省略できます。上の例では:::関数はピリオドを省略していますが、filter関数の呼び出しはピリオドをつけています。

何が起こっているのかよりわかりやすくするため、Stringで同じようなことをやってみましょう。

scala> val str1 = "abc"
str1: java.lang.String = abc

scala> val str2 = "def"
str2: java.lang.String = def

scala> val str3 = str1 + str2.substring(1, 2)
str3: java.lang.String = abce

どうでしょうか。こちらの例だと直感的にわかると思いますが、str3 は str1 に str2.substring(1, 2) を足したものになっています。これと同じことが上のListの例でも起こっているわけです。
つまり、list2 から奇数だけを抽出したあとにlist1と結合しているわけですね。

どうすればいいか

なんでこんなことになってしまったのか理解できてしまえば簡単です。

scala> val list4 = (list1:::list2).filter(_ % 2 == 1)
list4: List[Int] = List(1, 3, 5)

括弧をつければ、括弧内の処理結果に対して括弧外の処理を行わせることができます。他にも、list1 と list2 を結合したListを変数に格納してからfilterしてもいいですね。
ただ、最初に書いたコードと比べると括弧をつけた分だけ冗長な感じもします。それがどーしても許せない人はこんな書き方もできます。

scala> val list5 = list1:::list2 filter(_ % 2 == 1)
list5: List[Int] = List(1, 3, 5)

filter関数の呼び出しのピリオドを省略してしまう。この書き方でもlist1とlist2を結合してからfilterが走ります。まぁ後からコードを読んだときのことを考えると、括弧をつけた方が無難な気がしますが…。

まとめ

Scalaでは1行の中にピリオド有と無が混在している場合、ピリオド有の関数が優先して呼ばれます。演算子系の関数はわかりやすいですが、Listの:::などだと一瞬ポルナレフ状態に陥ることもあるので気を付けましょう。