超シンプルなRuby向けテスティングライブラリ、TestRocketを使ってみた

[今週のRuby Weekly](https://rubyweekly.com/issues/470)で TestRocket というテスティングライブラリが紹介されていたので、試しに使ってみました。

https://github.com/peterc/testrocket

### 対象となるバージョン

- testrocket 1.0.0

## セットアップ

gemをインストールすればOKです。

```console
gem install testrocket
```

Bundlerを使う場合は、Gemfileに追加して`bundle install`してください。

```ruby:Gemfile
gem 'testrocket'
```

## 使い方

testrocket は Refinements を利用するので、次のようにテストを書きたいファイルでrequire＆usingします。

```ruby
# テストを書きたいファイルでrequire＆using
require 'testrocket'
using TestRocket
```

Fizz Buzzプログラムをテストするならこんな感じになります。

```fizz_buzz.rb
def fizz_buzz(n)
  if n % 15 == 0
    'Fizz Buzz'
  elsif n % 3 == 0
    'Fizz'
  elsif n % 5 == 0
    'Buzz'
  else
    n.to_s
  end
end

require 'testrocket'
using TestRocket

+-> { fizz_buzz(1) == '1' }
+-> { fizz_buzz(2) == '2' }
+-> { fizz_buzz(3) == 'Fizz' }
+-> { fizz_buzz(4) == '4' }
+-> { fizz_buzz(5) == 'Buzz' }
+-> { fizz_buzz(6) == 'Fizz' }
+-> { fizz_buzz(15) == 'Fizz Buzz' }
```

実行してみましょう。

```console
$ ruby fizz_buzz.rb 
     OK
     OK
     OK
     OK
     OK
     OK
     OK
```

はい、実行できました。

試しに失敗させてみましょう。

```ruby
+-> { fizz_buzz(1) == 'Hoge' }
```

```console
$ ruby fizz_buzz.rb 
   FAIL @ fizz_buzz.rb:16
     OK
     OK
     OK
     OK
     OK
     OK
     OK
```

こんな感じで失敗します。

なお、テスト結果のサマリ（何件パスして、何件失敗したか、といった情報）は表示されないようです。

## 構文

テストを書くための構文は必要最小限です。
`+-> {}`でブロックが真を返せばパスするテストを、`--> {}`でブロックが偽を返すか、例外が発生したらパスするテストを書きます。

```ruby
# ブロックが真を返せばパス
+-> { 1 + 1 == 2 }

# ブロックが偽を返すか、例外が発生したらパス
--> { 'abc'.nil? }
--> { 1 / 0 }
```

`~-> {}`でpendingするテストが書けます。

```ruby
# このテストはpending
~-> { 1 + 1 == 2 }
```

```console
$ ruby sample.rb
PENDING 'true' @ sample.rb:4
````

`!-> {}`はテストの説明(description)を書くために使います。

```fizz_buzz.rb
# テストの説明
!-> { "test fizz buzz" }
# テスト本体
+-> { fizz_buzz(1) == '1' }
```

```console
$ ruby fizz_buzz.rb
   FIRE 'test fizz buzz'!
     OK
```

用意されている構文はこれだけです。

## 応用
READMEの中ではクラスファイルの中でクラスの読み込みと同時にテストを実行する例が示されています。

```user.rb
require 'testrocket'
using TestRocket

class User
  def initialize(name:, age:)
    @name = name
    @age = age
  end

  def greet
    if @age <= 12
      "ぼくは#{@name}だよ。"
    else
      "僕は#{@name}です。"
    end
  end
  # クラスファイル読み込み時にgreetメソッドのテストを実行する
  +-> { User.new(name: 'たろう', age: 12).greet == 'ぼくはたろうだよ。' }
  +-> { User.new(name: 'たろう', age: 13).greet == '僕はたろうです。' }
end
```

```console
$ ruby user.rb
     OK
     OK
```

ただし、`ENV['RACK_ENV']`か`ENV['RAILS_ENV']`が"production"を返したり、`Rails.env.production?`が`true`になる場合はテストを実行しません。（つまり、本番環境ではテストは実行されません）

また、テストを実行するためには、対象となるメソッドがすでに定義されている必要があるので、`greet`メソッドよりも手前にテストを書くとテストが失敗します。

```user.rb
  # この時点ではgreetメソッドが定義されていないので、テストが失敗する
  +-> { User.new(name: 'たろう', age: 12).greet == 'ぼくはたろうだよ。' }
  +-> { User.new(name: 'たろう', age: 13).greet == '僕はたろうです。' }
  def greet
    if @age <= 12
      "ぼくは#{@name}だよ。"
    else
      "僕は#{@name}です。"
    end
  end
```

```console
$ ruby user.rb
   FAIL @ user.rb:10
   FAIL @ user.rb:11
```

## 技術的な解説
`+->{}`や`-->{}`はProcオブジェクトの単項演算子として定義されています。
つまり、Rubyの文法的には、`+3`や`-2`と同じように、プラスのProcオブジェクト（`+-> {}`）や、マイナスのProcオブジェクト（`--> {}`）を書いていることになります。

### ソースコードも超シンプル！

testrocket 1.0.0のソースコードは1ファイルのみで、わずか37行です。
実装も大変シンプルなので、コードリーディングの題材としてみるのも面白いかもしれません。

https://github.com/peterc/testrocket/blob/v1.0.0/lib/testrocket.rb

## まとめ
というわけで、この記事では超シンプルなテスティングライブラリ、TestRocketを紹介してみました。

これでRailsアプリのテストを書け、と言われるとかなり難しいと思いますが、ごく単純なプログラミング問題をテスト駆動開発したりする場合は便利かもしれません。

興味を持った人はTestRocketで一度遊んでみてください！

https://github.com/peterc/testrocket