【Ruby版】CIツール導入ガイド 第3回 さまざまなジョブ(1)(RuboCop、Flay、test/unit)

By 2018-11-30 DevOps

Ruby版 CIツール導入ガイド

前回「第2回 さまざまなタスク:CIの実行中に処理されるジョブについての概要」で継続的インテグレーション(CI)で実行するツールの概要を紹介した。3回目では、継続的インテグレーションからRuby言語に適したツールのインストールから実行結果までを照会する。

CIのジョブとして利用可能なRuby用のツールとして、以下のような物が有名だ。

名称 役割
RuboCop

循環的複雑度解析, コーディングスタイルチェック
Flay 重複コード検出
Reek コードスメルチェック
RDoc ドキュメント自動生成
test/unit ユニットテスト
RSpec ユニットテスト
SimpleCov カバレッジ解析
Selenium UIテスト
Jenkins 統合ツール
GitLab 統合ツール

今回これらのうちから RuboCop, Flay, Reek そして test/unit を紹介したい。個々のツールの紹介は駆け足になってしまうが、具体的にどのようにインストールし、どういう動作をするのかについての大まかな雰囲気を掴んでもらいたい。

インストール環境について

各ツールは、通常コマンドラインからの手動操作で実行可能であるが、前述した通り CIツールから自動的に実行するのが望ましい。自動実行の設定については第5回で紹介しよう。今回はそれぞれのツールを個別にインストールし、実行した結果を紹介していきたい。

本稿における実行環境としては CentOS 7 を利用する事とした。まずは minimal インストールした CentOS7で以下を実行し、ruby をインストールして頂きたい。もっとも簡単なインストールは CentOS7の yum を利用した方法だ。しかし、古いバージョンの ruby しかインストールできないので利用される事は少ないようだ。ruby をインストールする場合、rbenv を利用し、複数バージヨンの ruby を無理なく共存できる環境を構築するのが一般的だ。rbenv のインストール方法の詳細はここでは触れられないが、各ブログ記事等を参考にインストールして欲しい。

サンプルプログラム

以下は各ツールの動作を検証するためのサンプルプログラムだ。以下2つのファイルを

 ホームディレクトリ/work/class 

に保存して欲しい。

work/class/Sample1.rb

class Sample1
    def go(a, b, c, d, e, f, g, h)
        check_num = 1
        out = 0
        tmp1 = 10000
        tmp2 = 10000
        tmp3 = 10000
        tmp4 = 10000
        tmp5 = 10000
        tmp6 = 10000
        tmp7 = 10000
        tmp8 = 10000
        tmp9 = 10000

        if a == check_num then
            out = out + 1
        end
        if b == check_num then
            out = out + 1
        end
        if c == check_num then
            out = out + 1
        end
        if d == check_num then
            out = out + 1
        end
        if e == check_num then
            out = out + 1
        end
        if f == check_num then
            out = out + 1
        end
        if g == check_num then
            out = out + 1
        end
        if h == check_num then
            out = out + 1
        end
        out += 1000
    end
end

work/class/Sample2.rb

class Sample2
    def go(a, b, c, d, e, f, g, h)
        check_num = 2
        out = 0
        tmp1 = 10000
        tmp2 = 10000
        tmp3 = 10000
        tmp4 = 10000
        tmp5 = 10000
        tmp6 = 10000
        tmp7 = 10000
        tmp8 = 10000
        tmp9 = 10000

        if a == check_num then
            out = out + 1
        end
        if b == check_num then
            out = out + 1
        end
        if c == check_num then
            out = out + 1
        end
        if d == check_num then
            out = out + 1
        end
        if e == check_num then
            out = out + 1
        end
        if f == check_num then
            out = out + 1
        end
        if g == check_num then
            out = out + 1
        end
        if h == check_num then
            out = out + 1
        end
        out += 1000
    end
end

Sample1 は与えられた8個の変数のうち、値が1だった引数の数に1000を足した物を、Sample2 は値が2だった引数の数に1000を足した物を求めるためのクラスだ。
見ての通り色々と問題だらけのコードだが、これらが各ツールによってどのように評価されるかを確認していきたい。

RuboCop

公式サイト::
https://github.com/rubocop-hq/rubocop

静的コード解析を行うツールである。不要変数、循環的複雑度解析等、コーディング規約チェック等を行ってくれる。インストールは以下の通りだ。

gem install rubocop

コマンドラインの書式は以下の通りである。

rubocop [オプション] <ファイル名>

<ファイル名>には対象とするファイルを指定する。ここにディレクトリ名を指定すれば、サブディレクトリも含め配下のファイルをすべて検査してくれる。ファイル名に何も指定しなければ、カレントディレクトリ以下の全ファイルをチェックする。

以下は work/class/Sample1.rb に対して実行した様子である。

$ rubocop work/class/Sample1.rb
Inspecting 1 file
W

Offenses:

work/class/Sample1.rb:1:1: C: Naming/FileName: The name of this source file (Sample1.rb) should use snake_case.
class Sample1
^
work/class/Sample1.rb:1:1: C: Style/Documentation: Missing top-level class documentation comment.
class Sample1
^^^^^
work/class/Sample1.rb:2:1: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
    def go(a, b, c, d, e, f, g, h)
^^^^
work/class/Sample1.rb:2:5: C: Metrics/AbcSize: Assignment Branch Condition size for go is too high. [27.59/15]
    def go(a, b, c, d, e, f, g, h) ...
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
work/class/Sample1.rb:2:5: C: Metrics/CyclomaticComplexity: Cyclomatic complexity for go is too high. [9/6]
    def go(a, b, c, d, e, f, g, h) ...
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
work/class/Sample1.rb:2:5: C: Metrics/MethodLength: Method has too many lines. [36/10]
    def go(a, b, c, d, e, f, g, h) ...
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
work/class/Sample1.rb:2:5: C: Metrics/PerceivedComplexity: Perceived complexity for go is too high. [9/7]
    def go(a, b, c, d, e, f, g, h) ...
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
work/class/Sample1.rb:2:11: C: Metrics/ParameterLists: Avoid parameter lists longer than 5 parameters. [8/5]
    def go(a, b, c, d, e, f, g, h)
          ^^^^^^^^^^^^^^^^^^^^^^^^
work/class/Sample1.rb:2:12: C: Naming/UncommunicativeMethodParamName: Method parameter must be at least 3 characters long.
    def go(a, b, c, d, e, f, g, h)
           ^
work/class/Sample1.rb:2:15: C: Naming/UncommunicativeMethodParamName: Method parameter must be at least 3 characters long.
    def go(a, b, c, d, e, f, g, h)
              ^
work/class/Sample1.rb:2:18: C: Naming/UncommunicativeMethodParamName: Method parameter must be at least 3 characters long.
    def go(a, b, c, d, e, f, g, h)
                 ^
work/class/Sample1.rb:2:21: C: Naming/UncommunicativeMethodParamName: Method parameter must be at least 3 characters long.
    def go(a, b, c, d, e, f, g, h)
                    ^
work/class/Sample1.rb:2:24: C: Naming/UncommunicativeMethodParamName: Method parameter must be at least 3 characters long.
    def go(a, b, c, d, e, f, g, h)
                       ^
work/class/Sample1.rb:2:27: C: Naming/UncommunicativeMethodParamName: Method parameter must be at least 3 characters long.
    def go(a, b, c, d, e, f, g, h)
                          ^
work/class/Sample1.rb:2:30: C: Naming/UncommunicativeMethodParamName: Method parameter must be at least 3 characters long.
    def go(a, b, c, d, e, f, g, h)
                             ^
work/class/Sample1.rb:2:33: C: Naming/UncommunicativeMethodParamName: Method parameter must be at least 3 characters long.
    def go(a, b, c, d, e, f, g, h)
                                ^
work/class/Sample1.rb:3:5: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
        check_num = 1
    ^^^^
work/class/Sample1.rb:5:9: W: Lint/UselessAssignment: Useless assignment to variable - tmp1.
        tmp1 = 10000
        ^^^^
work/class/Sample1.rb:5:16: C: Style/NumericLiterals: Use underscores(_) as decimal mark and separate every 3 digits with them.
        tmp1 = 10000
               ^^^^^
work/class/Sample1.rb:6:9: W: Lint/UselessAssignment: Useless assignment to variable - tmp2.
        tmp2 = 10000
        ^^^^
work/class/Sample1.rb:6:16: C: Style/NumericLiterals: Use underscores(_) as decimal mark and separate every 3 digits with them.
        tmp2 = 10000
               ^^^^^
work/class/Sample1.rb:7:9: W: Lint/UselessAssignment: Useless assignment to variable - tmp3.
        tmp3 = 10000
        ^^^^
work/class/Sample1.rb:7:16: C: Style/NumericLiterals: Use underscores(_) as decimal mark and separate every 3 digits with them.
        tmp3 = 10000
               ^^^^^
work/class/Sample1.rb:8:9: W: Lint/UselessAssignment: Useless assignment to variable - tmp4.
        tmp4 = 10000
        ^^^^
work/class/Sample1.rb:8:16: C: Style/NumericLiterals: Use underscores(_) as decimal mark and separate every 3 digits with them.
        tmp4 = 10000
               ^^^^^
work/class/Sample1.rb:9:9: W: Lint/UselessAssignment: Useless assignment to variable - tmp5.
        tmp5 = 10000
        ^^^^
work/class/Sample1.rb:9:16: C: Style/NumericLiterals: Use underscores(_) as decimal mark and separate every 3 digits with them.
        tmp5 = 10000
               ^^^^^
work/class/Sample1.rb:10:9: W: Lint/UselessAssignment: Useless assignment to variable - tmp6.
        tmp6 = 10000
        ^^^^
work/class/Sample1.rb:10:16: C: Style/NumericLiterals: Use underscores(_) as decimal mark and separate every 3 digits with them.
        tmp6 = 10000
               ^^^^^
work/class/Sample1.rb:11:9: W: Lint/UselessAssignment: Useless assignment to variable - tmp7.
        tmp7 = 10000
        ^^^^
work/class/Sample1.rb:11:16: C: Style/NumericLiterals: Use underscores(_) as decimal mark and separate every 3 digits with them.
        tmp7 = 10000
               ^^^^^
work/class/Sample1.rb:12:9: W: Lint/UselessAssignment: Useless assignment to variable - tmp8.
        tmp8 = 10000
        ^^^^
work/class/Sample1.rb:12:16: C: Style/NumericLiterals: Use underscores(_) as decimal mark and separate every 3 digits with them.
        tmp8 = 10000
               ^^^^^
work/class/Sample1.rb:13:9: W: Lint/UselessAssignment: Useless assignment to variable - tmp9.
        tmp9 = 10000
        ^^^^
work/class/Sample1.rb:13:16: C: Style/NumericLiterals: Use underscores(_) as decimal mark and separate every 3 digits with them.
        tmp9 = 10000
               ^^^^^
work/class/Sample1.rb:15:9: C: Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
        if a == check_num then
        ^^
work/class/Sample1.rb:15:27: C: Style/MultilineIfThen: Do not use then for multi-line if.
        if a == check_num then
                          ^^^^
work/class/Sample1.rb:16:9: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
            out = out + 1
        ^^^^
work/class/Sample1.rb:16:13: C: Style/SelfAssignment: Use self-assignment shorthand +=.
            out = out + 1
            ^^^^^^^^^^^^^
work/class/Sample1.rb:18:9: C: Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
        if b == check_num then
        ^^
work/class/Sample1.rb:18:27: C: Style/MultilineIfThen: Do not use then for multi-line if.
        if b == check_num then
                          ^^^^
work/class/Sample1.rb:19:9: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
            out = out + 1
        ^^^^
work/class/Sample1.rb:19:13: C: Style/SelfAssignment: Use self-assignment shorthand +=.
            out = out + 1
            ^^^^^^^^^^^^^
work/class/Sample1.rb:21:9: C: Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
        if c == check_num then
        ^^
work/class/Sample1.rb:21:27: C: Style/MultilineIfThen: Do not use then for multi-line if.
        if c == check_num then
                          ^^^^
work/class/Sample1.rb:22:9: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
            out = out + 1
        ^^^^
work/class/Sample1.rb:22:13: C: Style/SelfAssignment: Use self-assignment shorthand +=.
            out = out + 1
            ^^^^^^^^^^^^^
work/class/Sample1.rb:24:9: C: Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
        if d == check_num then
        ^^
work/class/Sample1.rb:24:27: C: Style/MultilineIfThen: Do not use then for multi-line if.
        if d == check_num then
                          ^^^^
work/class/Sample1.rb:25:9: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
            out = out + 1
        ^^^^
work/class/Sample1.rb:25:13: C: Style/SelfAssignment: Use self-assignment shorthand +=.
            out = out + 1
            ^^^^^^^^^^^^^
work/class/Sample1.rb:27:9: C: Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
        if e == check_num then
        ^^
work/class/Sample1.rb:27:27: C: Style/MultilineIfThen: Do not use then for multi-line if.
        if e == check_num then
                          ^^^^
work/class/Sample1.rb:28:9: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
            out = out + 1
        ^^^^
work/class/Sample1.rb:28:13: C: Style/SelfAssignment: Use self-assignment shorthand +=.
            out = out + 1
            ^^^^^^^^^^^^^
work/class/Sample1.rb:30:9: C: Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
        if f == check_num then
        ^^
work/class/Sample1.rb:30:27: C: Style/MultilineIfThen: Do not use then for multi-line if.
        if f == check_num then
                          ^^^^
work/class/Sample1.rb:31:9: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
            out = out + 1
        ^^^^
work/class/Sample1.rb:31:13: C: Style/SelfAssignment: Use self-assignment shorthand +=.
            out = out + 1
            ^^^^^^^^^^^^^
work/class/Sample1.rb:33:9: C: Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
        if g == check_num then
        ^^
work/class/Sample1.rb:33:27: C: Style/MultilineIfThen: Do not use then for multi-line if.
        if g == check_num then
                          ^^^^
work/class/Sample1.rb:34:9: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
            out = out + 1
        ^^^^
work/class/Sample1.rb:34:13: C: Style/SelfAssignment: Use self-assignment shorthand +=.
            out = out + 1
            ^^^^^^^^^^^^^
work/class/Sample1.rb:36:9: C: Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
        if h == check_num then
        ^^
work/class/Sample1.rb:36:27: C: Style/MultilineIfThen: Do not use then for multi-line if.
        if h == check_num then
                          ^^^^
work/class/Sample1.rb:37:9: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
            out = out + 1
        ^^^^
work/class/Sample1.rb:37:13: C: Style/SelfAssignment: Use self-assignment shorthand +=.
            out = out + 1
            ^^^^^^^^^^^^^
work/class/Sample1.rb:39:9: W: Lint/UselessAssignment: Useless assignment to variable - out. Use + instead of +=.
        out += 1000
        ^^^

1 file inspected, 68 offenses detected

警告内容をひとつずつ見てみよう。なお、同じ内容の物は省略する。

work/class/Sample1.rb:1:1: C: Naming/FileName: The name of this source file (Sample1.rb) should use snake_case.
class Sample1
^

ruby ではファイル名にスケークケースが利用される事が推奨されている。ファイル名を sample1.rb に変更する事でこの警告は消えるはずだ。

work/class/Sample1.rb:1:1: C: Style/Documentation: Missing top-level class documentation comment.
class Sample1
^^^^^

クラスを説明するコメントが無い事が警告されている。クラス宣言の直前にコメントを記述する事で対応できる。

work/class/Sample1.rb:2:1: C: Layout/IndentationWidth: Use 2 (not 4) spaces for indentation.
    def go(a, b, c, d, e, f, g, h)
^^^^

インデントの深さはスペース2文字が推奨されているが、スペース4文字でインドントされている。

work/class/Sample1.rb:2:5: C: Metrics/AbcSize: Assignment Branch Condition size for go is too high. [27.59/15]
    def go(a, b, c, d, e, f, g, h) ...
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

これはひとことで言ってしまうと、goメソッドが複雑すぎる事を警告されている。Abc Metric という指標に基づき警告が行われるのだが、代入や条件分岐、メソツド呼び出し等の回数が一定数を超えると警告される。上記の場合、Abc Metric が15以下である事が望ましいのに 27.59 という数字が出ていると警告されているわけだ。

Abc Metric の詳細は以下のページを参照して欲しい。

http://wiki.c2.com/?AbcMetric

なかなか複雑な指標だが、要するに各メソッドはもっと完結に記述し、処理が複雑になるなら別のメソッドに分割するべきであるという事だ。

work/class/Sample1.rb:2:5: C: Metrics/CyclomaticComplexity: Cyclomatic complexity for go is too high. [9/6]
    def go(a, b, c, d, e, f, g, h) ...
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

循環的複雑度が高すぎると警告されている。循環的複雑度は Abc Metric と似たような指標であるが、より分岐の複雑さに起因する複雑さを指摘してくれる指標だ。if 文等を減らす事で値を減らす事ができる。

work/class/Sample1.rb:2:5: C: Metrics/MethodLength: Method has too many lines. [36/10]
    def go(a, b, c, d, e, f, g, h) ...
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

メソッド宣言の1行が長すぎる事が警告されている。10文字以下に抑えるのが理想だが、36文字が利用されている。見ての通り、本質的には行が長いというより引数が多いために発生した警告だ。

work/class/Sample1.rb:2:11: C: Metrics/ParameterLists: Avoid parameter lists longer than 5 parameters. [8/5]
    def go(a, b, c, d, e, f, g, h)
          ^^^^^^^^^^^^^^^^^^^^^^^^

引数が多すぎる事が警告されている。8個の引数があるが5個以下に抑えよう。

work/class/Sample1.rb:2:12: C: Naming/UncommunicativeMethodParamName: Method parameter must be at least 3 characters long.
    def go(a, b, c, d, e, f, g, h)
           ^

引数名が短すぎる事が警告されている。最低でも引数名は3文字を利用しよう。

work/class/Sample1.rb:5:9: W: Lint/UselessAssignment: Useless assignment to variable - tmp1.
        tmp1 = 10000
        ^^^^

変数が利用されていない事が警告されている。 ruby のように変数の宣言が不要な言語では、変数をスペルミスしてしまっても表面上はエラーが発生しない場合が多い。そのような場合は最終的に利用しない変数が発生する事になりがちだが、当然バグの原因になるだろう。この警告を参照する事で、そのような状況を未然に防ぐ事ができる。

work/class/Sample1.rb:5:16: C: Style/NumericLiterals: Use underscores(_) as decimal mark and separate every 3 digits with them.
        tmp1 = 10000
               ^^^^^

ruby では数値の中にアンダースコアを入れる事が可能で、実行時に無視される。人が目てすぐ分かるように大きな数値には3桁ごとにアンダースコアを入れると良い。ここでは 10_000 と書く事が要求されている。

work/class/Sample1.rb:15:9: C: Style/IfUnlessModifier: Favor modifier if usage when having a single-line body. Another good alternative is the usage of control flow &&/||.
        if a == check_num then
        ^^

短い if 文は1行で書く事が要求されている。

work/class/Sample1.rb:15:27: C: Style/MultilineIfThen: Do not use then for multi-line if.
        if a == check_num then
                          ^^^^

複数行で if 文を書くのならば、then は不要だと言っている。

work/class/Sample1.rb:16:13: C: Style/SelfAssignment: Use self-assignment shorthand +=.
            out = out + 1
            ^^^^^^^^^^^^^

+= 演算子を利用して簡潔に記載しよう。以上3つの警告(Style/IfUnlessModifier, Style/MultilineIfThen, Style/SelfAssignment)は、以下のように書けば解消できるだろう。

out += 1 if a == check_num
work/class/Sample1.rb:39:9: W: Lint/UselessAssignment: Useless assignment to variable - out. Use + instead of +=.
        out += 1000
        ^^^

out が利用されていない。tmp1 等で警告されていたのと同じ Lint/UselessAssignment という警告だが、このような形で警告された場合、スペルミスが発生原因にはなっていない事が多いだろう。この行はメソッド内の最終行である。ご存知のように ruby ではメソッド内で最後に評価された値がメソッドの戻り値になるが、当然ここでは out に 1000 を足した値が返される事を期待しており、実際そのように動作する。しかし自己代入演算子である += 演算子で out に値を代入している点が無駄であり、この事を指摘する警告だ。代入された値が利用されない事に対する警告なので、最終行が以下のように書かれていても、もちろん警告される。

out = out + 1000

以下のように書けば警告されない。

out + 1000

以上のように、さまざまな警告が表示された。それぞれの警告は Lint/UselessAssignment のように警告のグループ名と名前が記載されているが、–except オプションで特定のグループや単体の警告を除外したり、逆に –only オプションで特定のグループや単体の警告だけを確認したりする事もできる。グループをまとめて指定したい時はグループ名だけを、単体の警告を指定したい時はグループ名と警告名の両方をスラッシュ区切りで指定する。

例えばプログラムの複雑度だけを確認したい場合、以下のようにする。

rubocop --only Metrics work/class/sample1.rb

インデントの空白数の警告が不要という場合には以下のようにする。

rubocop --except Layout/IndentationWidth work/class/sample1.rb

さらにそれぞれの警告をカンマ区切りで複数指定する事も可能だ。例えば以下のようにする事で、インデントの空白数と引数の変数名のみをチェックできる。

rubocop --only Layout/IndentationWidth,Naming/UncommunicativeMethodParamName work/class/sample1.rb

このように検査する内容は柔軟に指定可能なので、状況に応じて利用して頂きたい。

RuboCop による指摘は有効であり、特に使用されていない変数に対する警告はバグの発見に役立つだろう。警告されたら積極的に修正していこう。一方、循環的複雑度等、プログラムの複雑度に対する指摘については警告されても困ってしまう場合もあるかも知れない。一説によれと循環的複雑度が50を超えるとテストが不可能になり、75を超えると98%の確率でバグが混入すると言われている。あまり高い数値を放置するのは問題だが、ある程度小さな数値になっているなら、優先度の高い別の作業を先に行うべきだろう。

Flay

公式サイト::
https://github.com/seattlerb/flay

Flay も静的コード解析を行うツールだ。単一、または複数のソースファイルの中から重複した部分を指摘してくれる Flay を利用する事で、コピー&ペーストで構築されてしまったコードを見つける事ができる。インストールは以下の通りである。

gem install flay

コマンドラインの書式は以下の通りである。

flay [オプション] <ディレクトリ名>

実行すると<デイレクトリ名>配下にあるソースファイルを調査し、重複箇所を指摘してくれる。

実行例は以下の通りだ。

$ flay work/class
Total score (lower is better) = 184

1) Similar code found in :defn (mass = 184)
  work/class/Sample1.rb:2
  work/class/Sample2.rb:2

この例では Sample.rb と Sample2.rb が酷似している事を警告している。このように簡単に重複箇所を確認可能だ。

似て非なるコードの存在を許す事は、メンテナンス性の悪さを許容する事になる。コピペで増殖した似て非なるコードの1ヵ所にバグが見つかれば、その他の箇所にもバグはあると考えるべきだろう。当然修正していかなければならないのだが、完全に一致しているわけではないために機械的な置換では却って問題を大きくしてしまったりする場合もあり、慎重な対応が必要だ。このような問題を回避していくために、 Flay による指摘は積極的に解決していきたい。ただし、RuboCop の時にも述べたように、重要な問題が他にあればそちらを優先すべきである。

なお、Flay ではあまり短い類似点は警告されないので注意して欲しい。短い類似でもメンテナンス性を悪くするコードは色々ある。代表的なのがマジックナンバーだ。たとえば消費税率の値をプログラム中の至る所に 0.08 と記述しておけば、消費税率が更新された時にすべて修正しなければならない。うっかり一括置換した時に消費税率以外の何かを示す 0.08 まで置換してしまうと、バグを誘発するだろう。ツールに頼り切らず、こんな点も注意しながらプログラミングしていきたい所だ。

test/unit

test/unit はユニットテストを行うためのツールであり、Ruby の標準ライブラリだ。ユニットテストをCIに導入できればデグレードを素早く発見できる。標準ライブラリなのでインストールは不要だ。また、test/unit はライブラリの名前であり、RuboCop や Flay と違いコマンドラインツールではない。test/unit を利用して作ったテストスクリプトを実行する事で、テストを実行する。

テストスクリプトとはテストしたいクラスや関数を実際に呼び出し、適切な動作を行っているかの確認をするためのプログラムだ。今回の場合、Sample1.rb をテストするコードは以下のような感じになる。保存先ディレクトリは work/test としよう。

work/test/sample1_test.php

require 'test/unit'
require_relative '../class/Sample1'

# test for Sample1
class TestSample < Test::Unit::TestCase
  def test_go_1
    assert_equal 1001, Sample1.new.go(1, 2, 3, 4, 5, 6, 7, 8)
  end

  def test_go_2
    assert_equal 1008, Sample1.new.go(1, 1, 1, 1, 1, 1, 1, 1)
  end

  def test_go_3
    assert_equal 1000, Sample1.new.go(2, 2, 2, 2, 2, 2, 2, 2)
  end
end

テスト用クラスは Test::Unit::TestCase から継承して作成する。また、テスト対象となるクラスを require_relative 等で読み込んでおこう。

各テストメソッドでは、assert_equal メソッド等を利用して、各テストを記述する。このメソッドは第1引数と第2引数が等しい時にテストに成功したと判断するメソッドだ。ここでは Sample1 クラスの go メソッドの引数にさまざまな値を代入した結果と、その戻り値として期待する結果を比較している。

このテストを実行した結果は以下の通りだ。

$ ruby work/test/sample1_test.rb
Loaded suite work/test/sample1_test
Started
...

Finished in 0.000567982 seconds.
---------------------------------------------------------------------------------------
3 tests, 3 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
---------------------------------------------------------------------------------------
5281.86 tests/s, 5281.86 assertions/s

ピリオドが3つ出力されているが、これは3つのテストを実行してすべて成功した事を意味している。ここで test_go_2() を以下の内容に書き得よう。

  def test_go_2
    assert_equal 1008, Sample1.new.go(1, 1, 1, 1, 1, 1, 1, 2)
  end

実行結果は以下のようになる。

[root@ip-172-30-0-86 ~]# ruby work/test/sample1_test.rb
Loaded suite work/test/sample1_test
Started
.F
========================================================================================
Failure: test_go_2(TestSample)
work/test/sample1_test.rb:11:in `test_go_2'
      8:   end
      9:
     10:   def test_go_2
  => 11:     assert_equal 1008, Sample1.new.go(1, 1, 1, 1, 1, 1, 1, 2)
     12:   end
     13:
     14:   def test_go_3
<1008> expected but was
<1007>

diff:
? 1008
?    7
========================================================================================
.

Finished in 0.005205406 seconds.
----------------------------------------------------------------------------------------
3 tests, 3 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
66.6667% passed
----------------------------------------------------------------------------------------
576.32 tests/s, 576.32 assertions/s
[root@ip-172-30-0-86 ~]#

先ほどは

 ... 

と出力されていた部分が

 .F 

に置き換わり、テストで失敗した箇所のプログラムコードが表示されている。また、

<1008> expected but was
<1007>

と、出力されている通り、期待する結果が1008であるのに対し、実際にメソッドを実行した結果は 1007である事が分かるようになっている。今回はテストスクリプトを書き換えたためエラーになった。実際にはテスト対象クラスのメソッドが期待する値を返すかどうかをテストするわけだ。

ところで go メソッドは8個の引数を取り、各引数が1である数の合計に1000を足した値を返すメソッドだ。このクラスを精度良くテストしたいと思ったら、各引数が1かそうでないかの組み合わせをもう少しテストした方が良いだろう。理想としてはそれぞれの引数が1の場合とそうでない場合で、合計256通りのテストをしたい所だ。これは非常に面倒な作業となるのだが、go メソッドの設計に問題があるからそうなってしまったと言える。これが RuboCop で指摘された、

work/class/Sample1.rb:2:5: C: Metrics/CyclomaticComplexity: Cyclomatic complexity for go is too high. [9/6]
    def go(a, b, c, d, e, f, g, h) ...
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

という事に他ならない。短いプログラムでも、必要以上に複雑な分岐はテストを困難にしてしまう。テストが困難になれば当然バグも増えてしまうだろう。そのための指標として循環的複雑度を利用して欲しい。

話しを元に戻そう。test/unit は他にもさまざまな機能があるが、基本的な利用方法は今説明した通りだ。テストしたいメソッドを実行し、その結果を期待する結果と比較する事でテストが成功したかどうかを判定している。RuboCop や Flay と違い、実際にコードの一部を実行して評価している点が特徴だ。テストスクリプトを用意するのは面倒だが、用意できればバージョンアップの際等に、これまでのプログラムが問題なく動く事を確認できるようになるため非常に便利だ。

まとめ

今回は各ツールをインストールし、実行した結果を紹介した。それぞれのツールにはさまざまなオプションがあるので、より深い利用方法については各ツールの公式サイトや紹介サイトを参考にして欲しい。

次回は今回紹介できなかった他のツールについて紹介していきたい。

次回

  • 第4回 さまざまなジョブ(2):その他のツール(RSpec,Reek,RDoc,Selenium)の紹介

    本ブログは、Git / Subversionのクラウド型ホスティングサービス「tracpath(トラックパス)」を提供している株式会社オープングルーヴが運営しています。

    エンタープライズ向け Git / Subversion 導入や DevOps による開発の効率化を検討している法人様必見!

    tracpath(トラックパス)」は、企業内の情報システム部門や、ソフトウェア開発・アプリケーション開発チームに対して、開発の効率化を支援し、品質向上を実現します。
    さらに、システム運用の効率化・自動化支援サービスも提供しています。
    ”つくる情熱を支えるサービス”を提供し、まるで専属のインフラエンジニアのように、あなたのチームを支えていきます。

    クラウド型バグ管理・バージョン管理ソフトの「tracpath(トラックパス)」を提供しているオープングルーヴが運営しています

  • You Might Also Like

    1 Comment

    コメントを残す

    このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください