LoginSignup
43
35

More than 5 years have passed since last update.

「「テストが書きやすいコードは良いコード」をリファクタリングしてみた」をdoctestしてみた

Last updated at Posted at 2015-06-20

先日 @choripon さんの「テストが書きやすいコードは良いコード」という記事をうけて @jnchito さんが「「テストが書きやすいコードは良いコード」をリファクタリングしてみた」という興味深いQiita記事を投稿されていました。

両氏のテストとリファクタリングに関する考え、作成するテストに関する考えがとても参考になります。

しかし、サンプルコードとして乗せられた小規模なコードの場合、テストのために別途Rspecファイルを作成するのは大げさではないでしょうか。
Rspecの哲学であるBDDとは「振る舞い」を記述するものであり、テストと同時に仕様書でもあります。
小規模なプログラムの場合は、実装プログラム中に振る舞いのテストを記述してしまえば、ソースコードがドキュメントになって良いのではないか、と考えてみました。

元のコード

前提として、「テストが書きやすいコードは良いコード」をリファクタリングしてみた」を元記事、「テストが書きやすいコードは良いコード」を元記事の元記事と呼びます。

元記事のソースは以下のようになっています。

require 'yaml'

class Ore
  def initialize(last_name)
    @last_name = last_name
  end

  def first_name
    return '詐欺かも' if suspicious?

    if File.exists?(file_path)
      name_table[@last_name] || '(Unknown)'
    else
      puts "'#{file_path}' is not found."
    end
  end

  private

  def suspicious?
    @last_name == 'オレオレ'
  end

  def file_path
    File.expand_path("../../config/ore.yml", __FILE__)
  end

  def name_table
    YAML.load_file(file_path).map(&:values).to_h.invert
  end
end

上記コードは、元記事の元記事を元に元記事でリファクタリングされたコードです。
スッキリとしておりかつ、後味の良いコードですが、しゃっきりぽんと舌の上で踊りません。

コメントとリファクタリング

上記コードには、振る舞いについての説明がありません(別途Rspecファイルに書かれているからです)。
しかしこのファイルだけを見て、このクラスやそれぞれのメソッドがどう振る舞うかわかれば嬉しいですね。
つまりコメントが欲しいな、みたいな。

コメントとリファクタリングには以下のような関係があると思っています。
他にもあるかも知れませんが、色々な人がコメントとリファクタリングについて言及しているので、そういうを見てください。

  1. 1つのメソッドは数行のコメントで言い表せるべき
    • 例: 「このメソッドは、引数1と2を合算する」「ログインユーザーの取得」
    • 一言で表現できなかったり、メソッド中にもコメントが頻出するのならば、その部分を分離するほうが良い場合が多い
  2. コメントやメソッド名で、そのメソッドがどんな役割をもっているか明示することで、機能追加の際に関係ない機能が入り込むのを防ぐ
    • 例: ユーザー情報を取得するメソッド中に、今日の祝日判定をするなど
  3. 動的型付けな言語で、誤った型でメソッドが利用されるのを防ぐ
  4. コードからでは読み取れない意図を残す
    • 例: Rubyで s * 100 な処理をするメソッドで、コードだけで文字列を100回繰り返させたいという意図を読み取るのはほぼ不可能
  5. 機能の変更があったとき、コメントがメンテナンスされないと、かえってコードのリーダビリティが下がる

元記事でも言及されていますが、不要なテストや過剰なテストはメンテナンスコストを上げてしまうこともあります。
また、実装コードに書かれたコメントと、Rspecなどのテストコードに書いた振る舞いに関するコメントが、時を経るにつれてちぐはぐになっていく、なんていうことも目にします。

実装のコード・振る舞いに関するコメント・テスト、をできるだけ近い場所に記述することで、他の要素も同時に変更させやすくする…ということがしたい。

Yard / Doctest

そこで Yardminitest-doctest を用いてコードにコメントやテストを書いてみることにしました。

ちなみにYardは、Rubyのコードにいい感じにコメントを記述する記法と、それをドキュメント化するツールを提供するGem。minitest-doctestはテストをコメントとして記述できるGemです。

Yard
# 区切り用の文字列を標準出力する
#
# @param [String] mozi 区切り文字
# @param [Integer] length (100) moziの繰り返し回数
def separator(mozi, length=100)
  puts mozi.to_s * length
end

コンソールでyardocすることで、コメントからドキュメントを作成できます。
さらにyard serverとすることでブラウザで開けるようになります。

minitest-doctest
# 階乗をもとめる
#
# >> kai(5)
# => 120
# >> kai(6)
# => 6*120
# >> kai(0)
# => 1
def kai(n)
  return 1 if n <= 0
  kai(n-1) * n
end

コンソールでminidoctest filepathを実行すると、kai(5) kai(6) kai(0)のテストが走ります。

これで我々は、左手に「コメントをドキュメント化する」、右手に「コメントをテストする」の武器を得ました!

require 'yaml'

class Ore
  # @param [String] last_name 設定する苗字
  def initialize(last_name)
    @last_name = last_name
  end

  # 苗字に対応する名前を返す
  #
  # @example 名前を返す
  #   >> Ore.new("Matsuyama").first_name
  #   => "Hideyuki"
  #   >> Ore.new("松山").first_name
  #   => "秀行"
  #   >> Ore.new("Foo").first_name
  #   => "(Unkown)"
  #
  # @example オレオレ詐欺もしっかり検知
  #   >> Ore.new("オレオレ").first_name
  #   => "詐欺かも"
  #
  # @return [String] 苗字に対応する名前
  # @return [nil] 該当する名前がない場合(標準出力にアラート)
  def first_name
    return '詐欺かも' if suspicious?

    if File.exists?(file_path)
      name_table[@last_name] || '(Unknown)'
    else
      puts "'#{file_path}' is not found."
    end
  end

  private

  # 怪しい苗字であるか判定
  # @return [Boolean]
  def suspicious?
    @last_name == 'オレオレ'
  end

  def file_path
    File.expand_path("../config/ore.yml", __FILE__)
  end

  def name_table
    YAML.load_file(file_path).map(&:values).to_h.invert
  end
end

$ yardoc ore.rb 
Files:           1
Modules:         0 (    0 undocumented)
Classes:         1 (    1 undocumented)
Constants:       0 (    0 undocumented)
Methods:         2 (    0 undocumented)
 66.67% documented

$ yard server server                                                                              
[2015-06-20 23:05:46] INFO  WEBrick 1.3.1
[2015-06-20 23:05:46] INFO  ruby 2.1.5 (2014-11-13) [x86_64-linux]
[2015-06-20 23:05:46] INFO  WEBrick::HTTPServer#start: pid=13901 port=8808

ブラウザでlocalhost:8808にアクセスしましょう。
Screenshot_from_2015-06-20 23:14:20.png

いいですね。
テストも動かしてみましょう。

$ minidoctest ore.rb
Run options: --seed 8621

# Running:

F.

Finished in 0.005392s, 370.9124 runs/s, 741.8248 assertions/s.

  1) Failure:
#<Class:0x007f0c9f7523a0>#test_0 [(eval):8]:
Expected: "(Unkown)"
  Actual: "(Unknown)"

2 runs, 4 assertions, 1 failures, 0 errors, 0 skips

Oops! テストケースで誤字をしていたようです。
コメントを以下のように訂正しましょう。

  #   >> Ore.new("Foo").first_name
-  #   => "(Unkown)"
+  #   => "(Unknown)"
$ be minidoctest ore.rb 
Run options: --seed 58089

# Running:

..

Finished in 0.002592s, 771.4651 runs/s, 1542.9301 assertions/s.

2 runs, 4 assertions, 0 failures, 0 errors, 0 skips

無事、実装・ドキュメント・テストを1つのソースコードにまとめられました。
ある程度の規模があるGemやアプリケーションでは、Rspecのような「振る舞いのみを記述したカタログ」がある方が良いのですが、
小さいスクリプトやバッチなどでは、このように1ファイルにコードと振る舞いを記述するのもありだと思います。

ところで、元記事や元記事の元記事ではもっと細かい粒度のテストを実施しています。
しかしdoctestを使う場合、粒度を細かくするとコメントが膨れ上がりかえって読みづらくなってしまいます。
メソッドの本質的な振る舞いから離れたテスト(例外や細かい境界値テスト)などは、RspecやMinitestとして別ファイルにテストを用意するのがいいかもしれません。

適所で使い分けていきたいですね。

参考

43
35
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
35