0
0

RSpec の入門とその一歩先へ ~RubyGem 同時作成版~

Last updated at Posted at 2021-08-21

はじめに

有名な初心者向けの RSpec 入門記事として、和田卓人さん(@t_wada)の「RSpec の入門とその一歩先へ」という記事があります。そして、これの改良版として、Junichi Ito さん(@jnchito)の「RSpecの入門とその一歩先へ ~RSpec 3バージョン~」という記事があります。

この記事では、お二人の記事を RubyGem 同時作成版に書き直してみようと思います。なお、 source code を別途用意しませんので、記事のみを参考にして下さい。

各段階 (RubyGem 同時作成版) について

1 2 3 4
和田さん版 第1イテレーション 第2イテレーション 第3イテレーション
Ito さん版 第1イテレーション 第2イテレーション 第3イテレーション
第1段階(本記事) (いずれ書くかも) (いずれ書くかも)

本記事のライセンスについて

本記事は クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスで提供されています。

banner

備考

  • 原版を踏襲することはあまり重視していません。
  • カタカナ語は、なるべく和語または原語にしています。
  • 註釈なども、なるべく英語ではなく日本語にしています。
  • 英文として読み下しやすいかどうか、は無視しています。そのため、後半のかなりの部分が欠落しています。気になる方は改変前の資料をお読み下さい。
  • MS-Windows 上で作業しています。
  • git へ commit する作業は省略しています。必要であれば、ご自身で commit して下さい。
  • 私は Gemfile や Gemfile.lock を積極的には使わない派なので、bundle install はしていません。bundle installbundle exec をしたい方は適宜読み替えて下さい。
  • RuboCop の自動修正機能をバンバン使っています。ですが、RuboCop の自動修正機能は時々動作が怪しいことがあるので、本格的な開発では自動修正機能を使うかどうか自己判断して下さい。
  • 常に「RuboCop からの指摘 (offences) 0件」「YARD での文書化率 100%」「RSpec 全て成功」という状態を保つことを目指しています。そのため、file を作成したら git add .、file を変更したら RuboCop, YARD, RSpec を実行します (見出しではそれぞれ C, Y, S と表すことにします)。

使用するもの

  • ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x64-mingw-ucrt]
  • RSpec 3.13.0
    • spec が実行できない場合は gem install rspec してください。
  • Bundler 2.5.6
    • bundle が実行できない場合は gem install bundler してください。
  • RuboCop 1.62.0
  • rubocop-rspec 2.27.1
    • rubocop が実行できない場合は gem install rubocop-rspec してください。
  • rubocop-rake 0.6.0
    • 必要に応じて gem install rubocop-rake してください。
  • YARD 0.9.36
    • yard が実行できない場合は gem install yard してください。
  • git 2.44.0.windows.1

第1段階

以前 favotter というものがあったようで、そこでは不適切な単語の filtering 機能があったようです。RSpec を用いながらこの機能を実装していきましょう。まずは不適切な単語の検出機能を作成します。
この段階では最初ベタな形の test code と実装を書き、だんだんと洗練させてゆきます。

1.1 git の準備

あなたの名前が git に登録されているかどうか、確認します。

> git config user.name
YAMADA Taro

あなたの名前が表示されなかった場合は、以下のように git に登録して下さい (参考)。global に登録するのが嫌であれば、local に登録しても構いません。

> git config --global user.name "YAMADA Taro"

あなたの e-mail address が git に登録されているかどうか、確認します。

> git config user.email
yamada@example.com

あなたの e-mail address が表示されなかった場合は、以下のように git に登録して下さい (参考)。global に登録するのが嫌であれば、local に登録しても構いません。

> git config --global user.email "yamada@example.com"

1.2 RubyGem の雛型の作成

Bundler を使って開発用の directory を用意します。環境によっては license をどう設定するかなど質問されるかも知れません。

> bundle gem rspec3_for_beginners --test=rspec --linter=rubocop
Creating gem 'rspec3_for_beginners'...
MIT License enabled in config
RuboCop enabled in config
〔中略〕
Gem 'rspec3_for_beginners' was successfully created. For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html
> cd rspec3_for_beginners

今作ったばかりの directory に入ります。

> cd rspec3_for_beginners

1.3 rspec3_for_beginners.gemspec の変更

*.gemspec には、RubyGem を作成する際の設定が書き込まれています。

ですが、余計な設定項目も多いので、最低限の項目だけ残して残りは無効化します。

(なお、この記事の code は diff 風の記法で書かれています。 "+" が追加された行、 "-" が削除された行です。)

rspec3_for_beginners.gemspec
 # frozen_string_literal: true

 require_relative "lib/rspec3_for_beginners/version"

 Gem::Specification.new do |spec|
   spec.name          = "rspec3_for_beginners"
   spec.version       = Rspec3ForBeginners::VERSION
   spec.authors       = ["YAMADA Taro"]
   spec.email         = ["yamada@example.com"]

-  spec.summary       = "TODO: Write a short summary, because RubyGems requires one."
+  spec.summary       = "不適切な単語を検出する"
-  spec.description   = "TODO: Write a longer description or delete this line."
-  spec.homepage      = "TODO: Put your gem's website or public repo URL here."
-  spec.required_ruby_version = ">= 2.6.0"
+  spec.required_ruby_version = ">= 3.3.0"
-
+  spec.metadata["rubygems_mfa_required"] = "true"
-  spec.metadata["allowed_push_host"] = "TODO: Set to 'https://mygemserver.com'"
-
-  spec.metadata["homepage_uri"] = spec.homepage
-  spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
-  spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
-
   # Specify which files should be added to the gem when it is released.
   # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
   spec.files = Dir.chdir(File.expand_path(__dir__)) do
     `git ls-files -z`.split("\x0").reject do |f|
       (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
     end
   end
   spec.bindir        = "exe"
   spec.executables   = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
   spec.require_paths = ["lib"]

   # Uncomment to register a new dependency of your gem
   # spec.add_dependency "example-gem", "~> 1.0"

   # For more information and examples about making a new gem, checkout our
   # guide at: https://bundler.io/guides/creating_gem.html
 end

1.4 Rakefile の変更

この後、RuboCop や YARD は頻繁に実行します。RuboCop や YARD を簡単に呼び出せるように、Rakefile を変更しておきます。

Rakefile
 # frozen_string_literal: true

 require "bundler/gem_tasks"
 require "rspec/core/rake_task"

 RSpec::Core::RakeTask.new(:spec)

-require "rubocop/rake_task"
-
-RuboCop::RakeTask.new
+desc "1回目の RuboCop 実行"
+task :rubocop1st do
+  puts `rubocop --enable-pending-cops --auto-gen-config ./exe/**/* ./lib/**/*.rb ./spec/**/*.rb ./*.gemspec ./Gemfile ./Rakefile`
+end
+
+desc "RuboCop 実行"
+task :rubocop do
+  puts `rubocop --enable-pending-cops --autocorrect-all ./exe/**/* ./lib/**/*.rb ./spec/**/*.rb ./*.gemspec ./Gemfile ./Rakefile`
+end
+
+desc "YARD 実行"
+task :yard do
+  puts `yard doc ./lib/**/*.rb`
+end

-task default: %i[spec rubocop]
+task default: %i[rubocop yard spec]

これで、rake rubocop で RuboCop を呼び出すことができ、rake yard で YARD を呼び出すことができるようになりました。(ただし、初めて RuboCop を呼び出すときは rake rubocop1st とします。)

また、RSpec は初期状態から (上記の改変抜きで) rake spec で呼び出せるようになっています。

1.5 初めての RuboCop, YARD, RSpec

気が早いようですが、この時点で RuboCop, YARD, RSpec を一旦実行してみましょう。

1.5(C) RuboCop による整形・修正

RuboCop は、Ruby の code を自動的に整形・修正したり様式を検査したりしてくれます。そのための前準備をします。

以下のように書き換えて下さい。(本当は設定したくない項目もあるのですが、本記事の説明に都合が悪い動作を避けるため、あえてこのようにしています。)

.rubocop.yaml
+ require:
+   - rubocop-rake
+   - rubocop-rspec

 AllCops:
-  TargetRubyVersion: 2.6
+  TargetRubyVersion: 3.3
+  NewCops: enable

 Style/StringLiterals:
   Enabled: true
   EnforcedStyle: double_quotes

 Style/StringLiteralsInInterpolation:
   Enabled: true
   EnforcedStyle: double_quotes

 Layout/LineLength:
-  Max: 120
+  Max: 144
+
+Layout/EndOfLine:
+  Enabled: false
+
+Lint/EmptyBlock:
+  Enabled: false
+
+Lint/EmptyClass:
+  Enabled: false
+
+Lint/UnusedMethodArgument:
+  Enabled: false
+
+RSpec/EmptyExampleGroup:
+  Enabled: false
+
+RSpec/FilePath:
+  Enabled: false
+
+RSpec/ImplicitSubject:
+  Enabled: false
+
+RSpec/InstanceVariable:
+  Enabled: false
+
+RSpec/NoExpectationExample:
+  Enabled: false
+
+RSpec/SpecFilePathFormat:
+  Enabled: false
+
+Style/AsciiComments:
+  Enabled: false
+
+Style/EmptyMethod:
+  Enabled: false
+
+Style/RedundantInitialize:
+  Enabled: false

続いて、RuboCop で自動的に整形できる部分は今のうちに整形してしまいましょう。(RuboCop を初めて呼び出すので rake rubocop1st としています。)

> rake rubocop1st
Added inheritance from `.rubocop_todo.yml` in `.rubocop.yml`.
Phase 1 of 2: run Layout/LineLength cop (skipped because the default Layout/LineLength:Max is overridden)
Phase 2 of 2: run all cops
Inspecting 7 files
..C....

7 files inspected, 3 offenses detected, 2 offenses autocorrectable
Created .rubocop_todo.yml.

下から2行目の「3 offenses detected」が、RuboCop による検査で問題点 (offences) が3点見つかったことを表しています。しかし、初回検査で見つかった問題点は次回以降は無視してくれるので、このまま進めます。

1.5(Y) 初めての YARD 実行

YARD を実行してみましょう。

> rake yard
Files:           2
Modules:         1 (    1 undocumented)
Classes:         1 (    1 undocumented)
Constants:       1 (    1 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         0 (    0 undocumented)
 0.00% documented

「undocumented」と書かれているものが、説明文がない状態にあります。つまり、上記の表示は「説明文がない module が1つ」「説明文がない class が1つ」「説明文がない定数が1つ」存在するということを表しています。

以下の2つの files を修正して下さい。(「こんな説明文、書くほどのことではない」と感じるかもしれませんが、文書化率 100% を保持するために書いています。)

lib/rspec3_for_beginners.rb
 # frozen_string_literal: true

 require_relative "rspec3_for_beginners/version"

+# RSpec3 の習作のための名前空間。
 module Rspec3ForBeginners
+
+  # error 定義。
   class Error < StandardError; end
   # Your code goes here...
 end
lib/rspec3_for_beginners/version.rb
 # frozen_string_literal: true

 module Rspec3ForBeginners
-  VERSION = "0.1.0"
+  VERSION = "0.1.0" # 版番号。
 end

再度 YARD を実行してみます。

> rake yard
Files:           2
Modules:         1 (    0 undocumented)
Classes:         1 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         0 (    0 undocumented)
 100.00% documented

今度は大丈夫なようです。今後も「100.00% documented」状態を維持することにします。

なお、YARD によって自動的に生成された文書は doc/index.html で読むことができます。

1.5(S) 初めての RSpec 実行

RSpec を実行してみましょう。

> rake spec

Rspec3ForBeginners
  has a version number
  does something useful (FAILED - 1)

Failures:

  1) Rspec3ForBeginners does something useful
     Failure/Error: expect(false).to eq(true)

       expected: true
            got: false

       (compared using ==)

       Diff:
       @@ -1 +1 @@
       -true
       +false

     # ./spec/rspec3_for_beginners_spec.rb:9:in `block (2 levels) in <top (required)>'

Finished in 0.08607 seconds (files took 0.71022 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/rspec3_for_beginners_spec.rb:8 # Rspec3ForBeginners does something useful

code を1行も書いていないのに、いきなり失敗 (Failure) として赤い文字がたくさん表示されました。

実は、bundle gem を実行した時点で、必ず失敗する test が最初に作られているのです。

該当する部分を削除しましょう。

spec/rspec3_for_beginners_spec.rb
 # frozen_string_literal: true

 RSpec.describe Rspec3ForBeginners do
   it "has a version number" do
     expect(Rspec3ForBeginners::VERSION).not_to be nil
   end
-
-  it "does something useful" do
-    expect(false).to eq(true)
-  end
 end

再度 RSpec を実行してみます。

> rake spec

Rspec3ForBeginners
  has a version number

Finished in 0.0134 seconds (files took 0.65739 seconds to load)
1 example, 0 failures

今度は無事に全て成功しました (赤字は表示されませんでした)。

今後も「0 failures」状態を維持することにします。

1.6 message_filter_spec.rb を作成

次に、これから育てていく spec file を作成します。手作業で spec directory 内に message_filter_spec.rb を作成するか、MS-Windows 上で PowerShell を使っているなら

> New-Item spec/message_filter_spec.rb

と入力するか、touch が使えるなら

> touch spec/message_filter_spec.rb

と入力して下さい。

1.6(G) git

message_filter_spec.rb を作成したら、いったん git へ add します (commit はご自由に)。

> git add .

続いて message_filter_spec.rb を編集します。

spec/rspec3_for_beginners/message_filter_spec.rb
+RSpec.describe Rspec3ForBeginners::MessageFilter do
+end

1.6(C) RuboCop

すかさず message_filter_spec.rb を保存し、RuboCop を適用します。

> rake rubocop
Inspecting 8 files
..C.....

Offenses:

spec/message_filter_spec.rb:1:1: C: [Corrected] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
RSpec.describe Rspec3ForBeginners::MessageFilter do
^
spec/message_filter_spec.rb:2:1: C: [Corrected] Layout/EmptyLineAfterMagicComment: Add an empty line after magic comments.
RSpec.describe Rspec3ForBeginners::MessageFilter do
^

8 files inspected, 2 offenses detected, 2 offenses corrected

RuboCop のおかげで message_filter_spec.rb が自動的に変更されています。

spec/rspec3_for_beginners/message_filter_spec.rb
+# frozen_string_literal: true
+
 RSpec.describe Rspec3ForBeginners::MessageFilter do
 end

1.7 message_filter.rb を作成

それから MessageFilter class を作成します。touch が使えるなら CUI から

> touch lib/rspec3_for_beginners/message_filter.rb

それから MessageFilter class を作成します。手作業で lib/rspec3_for_beginners directory 内に message_filter を作成するか、MS-Windows 上で PowerShell を使っているなら

> New-Item lib/rspec3_for_beginners/message_filter.rb

と入力するか、touch が使えるなら

> touch lib/rspec3_for_beginners/message_filter.rb

と入力して下さい。

1.7(G) git

作成したらすぐに git add . しましょう。

> git add .

続いて中身を書きます。

lib/rspec3_for_beginners/message_filter.rb
+module Rspec3ForBeginners
+  class MessageFilter
+  end
+end

1.7(C) RuboCop

すかさず message_filter.rb を保存し、RuboCop を適用します。

> rake rubocop

RuboCop のおかげで message_filter.rb が自動的に変更されています。

lib/rspec3_for_beginners/message_filter.rb
+# frozen_string_literal: true
+
 module Rspec3ForBeginners
   class MessageFilter
   end
 end

1.7(Y) YARD

YARD を実行します。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    1 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         0 (    0 undocumented)
 75.00% documented

該当箇所に説明文を書き込みます。

lib/rspec3_for_beginners/message_filter.rb
 # frozen_string_literal: true

 module Rspec3ForBeginners
+  # filtering 機能を持つ message の class。
   class MessageFilter
   end
 end

再度 YARD を実行します。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         0 (    0 undocumented)
 100.00% documented

今度は大丈夫です。

1.8 自動読み込み

RSpec を実行する際に自動的に message_filter.rb が読み込まれるようにします。lib/rspec3_for_beginners.rbrequire_relative を使うことで読み込めるようになります。

lib/rspec3_for_beginners.rb
 # frozen_string_literal: true

 require_relative "rspec3_for_beginners/version"
+require_relative "rspec3_for_beginners/message_filter"

 module Rspec3ForBeginners
   class Error < StandardError; end
   # Your code goes here...
 end

1.8(C) RuboCop

RuboCop を実行します。

> rake rubocop
Inspecting 9 files
.........

9 files inspected, no offenses detected

RuboCop を実行しても、指摘は増えません。

1.8(Y) YARD

YARD を実行しても、説明文の不足は指摘されません (undocumented は0件です)。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         0 (    0 undocumented)
 100.00% documented

1.8(S) RSpec

では RSpec を実行してみましょう。

次のような結果が表示されれば OK です (赤字が表示されなければ OK です)。

> rake spec

Rspec3ForBeginners
  has a version number

Finished in 0.0116 seconds (files took 0.68858 seconds to load)
1 example, 0 failures

1.8(CYS)

実は rake と入力するだけで RuboCop, YARD, RSpec を全て実行してくれるように設定しています。

RuboCop, YARD, RSpec を1つずつ実行することが面倒になったら、rake とだけ入力して下さい。

1.9 最初の test (code example) を書きましょう

非常にベタな書き方ですが、最初の test (code example) を書きます。第1段階の後半で、test の書き方も洗練させていきます。

spec/message_filter_spec.rb
 # frozen_string_literal: true

 RSpec.describe Rspec3ForBeginners::MessageFilter do
+  it "不適切な単語を含んでいたら検出される" do
+    filter = described_class::MessageFilter.new("foo")
+    expect(filter.detect?("hello from foo")).to be true
+  end
 end

1.9(C) RuboCop

すかさず message_filter_spec.rb を保存し、RuboCop を適用します。

> rake rubocop

今回は自動的に整形される部分はなかったようです。

1.9(Y) YARD

YARD を実行しても、説明文の不足は指摘されません (undocumented は0件です)。Spec files (今回は message_filter_spec.rb) は YARD の対象外としているので、当然と言えば当然の結果です。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         0 (    0 undocumented)
 100.00% documented

1.9(S) RSpec

では RSpec を実行してみましょう。

> rake spec

Rspec3ForBeginners::MessageFilter
  不適切な単語を含んでいたら検出される (FAILED - 1)

Rspec3ForBeginners
  has a version number

Failures:

  1) Rspec3ForBeginners::MessageFilter 不適切な単語を含んでいたら検出される
     Failure/Error: filter = Rspec3ForBeginners::MessageFilter.new("foo")

     ArgumentError:
       wrong number of arguments (given 1, expected 0)
     # ./spec/message_filter_spec.rb:5:in `initialize'
     # ./spec/message_filter_spec.rb:5:in `new'
     # ./spec/message_filter_spec.rb:5:in `block (2 levels) in <top (required)>'

Finished in 0.03031 seconds (files took 0.70397 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/message_filter_spec.rb:4 # Rspec3ForBeginners::MessageFilter 不適切な単語を含んでいたら検出される

constructor の引数の数が不正であると言われました。それはそうですね、

constructor を作成します。

1.10 constructor の作成

lib/rspec3_for_beginners/message_filter.rb
 # frozen_string_literal: true

 module Rspec3ForBeginners
   # filtering 機能を持つ message の class。
   class MessageFilter
+    def initialize(word)
+      @word = word
+    end
   end
 end

1.10(C) RuboCop

すかさず message_filter.rb を保存し、RuboCop を適用します。

> rake rubocop
Inspecting 9 files
.........

9 files inspected, no offenses detected

今回は自動的に整形される部分はなかったようです。

1.10(Y) YARD

YARD を実行しても、説明文の不備は指摘されません (undocumented は0件です)。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         1 (    0 undocumented)
 100.00% documented

1.10(S) RSpec

実行してみましょう。

> rake spec

Rspec3ForBeginners::MessageFilter
  不適切な単語を含んでいたら検出される (FAILED - 1)

Rspec3ForBeginners
  has a version number

Failures:

  1) Rspec3ForBeginners::MessageFilter 不適切な単語を含んでいたら検出される
     Failure/Error: expect(filter.detect?("hello from foo")).to be true

     NoMethodError:
       undefined method `detect?' for #<Rspec3ForBeginners::MessageFilter:0x0000026a4d0677f0 @word="foo">
     # ./spec/message_filter_spec.rb:6:in `block (2 levels) in <top (required)>'

Finished in 0.03485 seconds (files took 0.79141 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/message_filter_spec.rb:4 # Rspec3ForBeginners::MessageFilter 不適切な単語を含んでいたら検出される

test はまだ通りません。detect? という method が無いよと言われました。無いですね。作りましょう。

1.11 detect? method 作成

空で良いので、detect? method を作成します。

lib/rspec3_for_beginners/message_filter.rb
 # frozen_string_literal: true

 module Rspec3ForBeginners
   # filtering 機能を持つ message の class。
   class MessageFilter
     def initialize(word)
       @word = word
     end
+
+    def detect?(text)
+    end
   end
 end

1.11(C) RuboCop

すかさず message_filter.rb を保存し、RuboCop を適用します。

> rake rubocop
Inspecting 9 files
.........

9 files inspected, no offenses detected

今回は自動的に整形される部分はなかったようです。

1.11(Y) YARD

YARD を実行しても、説明文の不備は指摘されません (undocumented は0件です)。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         2 (    0 undocumented)
 100.00% documented

1.11(S) RSpec

実行してみましょう。

> rake spec

Rspec3ForBeginners::MessageFilter
  不適切な単語を含んでいたら検出される (FAILED - 1)

Rspec3ForBeginners
  has a version number

Failures:

  1) Rspec3ForBeginners::MessageFilter 不適切な単語を含んでいたら検出される
     Failure/Error: expect(filter.detect?("hello from foo")).to be true

       expected: true
            got: nil
     # ./spec/message_filter_spec.rb:6:in `block (2 levels) in <top (required)>'

Finished in 0.09777 seconds (files took 0.70221 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/message_filter_spec.rb:4 # Rspec3ForBeginners::MessageFilter 不適切な単語を含んでいたら検出される

true が返ってきてほしいところに nil が返ってきました。detect? method の中身が空だからですね。ではこの test を通すもっとも簡単な実装はどうなるでしょうか。ここに TDD の trick があります。それが、「 仮実装 (fake it) 」です。

1.12 仮実装 (fake it)

先ほどの test を通すためのもっとも単純な実装はどうなるでしょうか? 書いてみましょう。

lib/rspec3_for_beginners/message_filter.rb
 # frozen_string_literal: true

 module Rspec3ForBeginners
   # filtering 機能を持つ message の class。
   class MessageFilter
     def initialize(word)
       @word = word
     end

     def detect?(text)
+      true
     end
   end
 end

こ れ は ひ ど い !! しかし、これが TDD の「仮実装」です。

1.12(C) RuboCop

すかさず message_filter.rb を保存し、RuboCop を適用します。

> rake rubocop
Inspecting 9 files
.........

9 files inspected, no offenses detected

今回は自動的に整形される部分はなかったようです。

1.12(Y) YARD

YARD を実行しても、説明文の不備は指摘されません (undocumented は0件です)。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         2 (    0 undocumented)
 100.00% documented

1.12(S) RSpec

実行してみましょう。

> rake spec

Rspec3ForBeginners::MessageFilter
  不適切な単語を含んでいたら検出される

Rspec3ForBeginners
  has a version number

Finished in 0.03476 seconds (files took 0.68444 seconds to load)
2 examples, 0 failures

detect? method から true を返すベタな実装を行ったので test が通るのは当たり前ですね。こんな行為に何か意味があるのでしょうか?

仮実装とは、test の test、と考えることが出来ます。 例えば今回の例で、 true を返すという「絶対 test が通るだろう」という実装 code を書いても、test が失敗したらどうでしょうか? それは、test code の方にこそ bugs が潜んでいることを示唆しています。仮実装で成功しない test は、本実装でも成功しないでしょう。本実装でも test が通らなかったときに、なぜ test が通らないのか本実装を長い時間 debug した結果、test code が間違っていたのでは目も当てられません。

1.13 三角測量

しかし、このままでは実装はいつまでも安易過ぎるものになってしまうので、別の data を使った test を足しましょう。これを TDD では 「三角測量 (triangulation)」 といいます。

spec/message_filter_spec.rb
 # frozen_string_literal: true

 RSpec.describe Rspec3ForBeginners::MessageFilter do
   it "不適切な単語を含んでいたら検出される" do
     filter = Rspec3ForBeginners::MessageFilter.new("foo")
     expect(filter.detect?("hello from foo")).to eq true
   end
+
+  it "不適切な単語が含まれていなければ検出されない" do
+    filter = described_class::MessageFilter.new("foo")
+    expect(filter.detect?("hello, world!")).to be false
+  end
 end

1.13(C) RuboCop

すかさず message_filter_spec.rb を保存し、RuboCop を適用します。

> rake rubocop
Inspecting 9 files
.........

9 files inspected, no offenses detected

今回は自動的に整形される部分はなかったようです。

1.13(Y) YARD

YARD を実行しても、説明文の不備は指摘されません (undocumented は0件です)。Spec files (今回は message_filter_spec.rb) は YARD の対象外としているので、当然と言えば当然の結果です。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         2 (    0 undocumented)
 100.00% documented

1.13(S) RSpec

実行してみましょう。

> rake spec

Rspec3ForBeginners::MessageFilter
  不適切な単語を含んでいたら検出される
  不適切な単語が含まれていなければ検出されない (FAILED - 1)

Rspec3ForBeginners
  has a version number

Failures:

  1) Rspec3ForBeginners::MessageFilter 不適切な単語が含まれていなければ検出されない
     Failure/Error: expect(filter.detect?("hello, world!")).to be false

       expected: false
            got: true

     # ./spec/message_filter_spec.rb:11:in `block (2 levels) in <top (required)>'

Finished in 0.11162 seconds (files took 0.70189 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./spec/message_filter_spec.rb:9 # Rspec3ForBeginners::MessageFilter 不適切な単語が含まれていなければ検出されない

「仮実装」で書いた code はあっという間に破綻しました。そろそろきちんと実装しないといけないですね。

1.14 明白な実装

lib/rspec3_for_beginners/message_filter.rb
 # frozen_string_literal: true

 module Rspec3ForBeginners
   # filtering 機能を持つ message の class。
   class MessageFilter
     def initialize(word)
       @word = word
     end

     def detect?(text)
-      true
+      text.include?(@word)
     end
   end
 end

1.14(C) RuboCop

すかさず message_filter.rb を保存し、RuboCop を適用します。

> rake rubocop
Inspecting 9 files
.........

9 files inspected, no offenses detected

今回は自動的に整形される部分はなかったようです。

1.14(Y) YARD

YARD を実行しても、説明文の不備は指摘されません (undocumented は0件です)。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         2 (    0 undocumented)
 100.00% documented

1.14(S) RSpec

実行してみましょう。

> rake spec

Rspec3ForBeginners::MessageFilter
  不適切な単語を含んでいたら検出される
  不適切な単語が含まれていなければ検出されない

Rspec3ForBeginners
  has a version number

Finished in 0.03478 seconds (files took 0.70214 seconds to load)
3 examples, 0 failures

今度は成功しました。

実装が見えているときは、三角測量を介さずにそのまま仮実装を変更して実装してもかまいません。これを 「明白な実装 (obvious implementation)」 といいます。

大事なのは自分の不安を制御することです。 TDD では、test と実装両方に自信がある時は「明白な実装」、一歩一歩進めたい、つまり test を test して、そのあとで実装を test したい時は「仮実装」と「三角測量」の組み合わせを使います。

1.15 test の refactoring

さて、test が全部通っているので、実装 class の refactoring を行いましょう。refactoring とは、code に重複があったり、無駄がある場合に、test が通ったままで実装を綺麗にしていくことです。

実装に code の重複や無駄はあるでしょうか? 現時点では refactoring の余地がないほど単純ですね。では test code はどうでしょうか? …かなり重複が見られますね。

test を書いたすぐ後の時点で、test code の重複も積極的に排除していこう、というのが最近の考え方です。test の「refactoring」というと厳密にはもっと難しく、時機が遅れるほど困難なものですが、test 実装直後では自分の頭にも test 設計が残っているでしょうし、この時点では大胆に行動できます。

では重複を排除していきましょう。

1.16 before method の抽出

before method を作成し、filter の instance 作成部分をそこに移動します。

before とは、 xUnit でいうと setUp に相当します。before method は各 tests の実行前に毎回実行されますので、重複部を before に書くことで test code の重複を排除することができます。

spec/message_filter_spec.rb
 # frozen_string_literal: true

 RSpec.describe Rspec3ForBeginners::MessageFilter do
+  before do
+    @filter = described_class.new("foo")
+  end
+
  it "不適切な単語を含んでいたら検出される" do
-    filter = described_class.new("foo")
-    expect(filter.detect?("hello from foo")).to be true
+    expect(@filter.detect?("hello from foo")).to be true
  end

  it "不適切な単語が含まれていなければ検出されない" do
-    filter = described_class.new("foo")
-    expect(filter.detect?("hello, world!")).to be false
+    expect(@filter.detect?("hello, world!")).to be false
  end
end

1.16(C) RuboCop

すかさず message_filter_spec.rb を保存し、RuboCop を適用します。

> rake rubocop
Inspecting 9 files
.........

9 files inspected, no offenses detected

今回は自動的に整形される部分はなかったようです。

1.16(Y) YARD

YARD を実行しても、説明文の不備は指摘されません (undocumented は0件です)。Spec files (今回は message_filter_spec.rb) は YARD の対象外としているので、当然と言えば当然の結果です。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         2 (    0 undocumented)
 100.00% documented

1.16(S) RSpec

実行してみましょう。

> rake spec

Rspec3ForBeginners::MessageFilter
  不適切な単語を含んでいたら検出される
  不適切な単語が含まれていなければ検出されない

Rspec3ForBeginners
  has a version number

Finished in 0.03524 seconds (files took 0.6984 seconds to load)
3 examples, 0 failures

1.17 be_[predicate] matchers

expect(XXX?).to eq trueexpect(XXX).to be_truthy という記述は、 be_[predicate] matchers という書き方に変換することができます。こういう書き方をすることで文書としての test codeの意味を高め、かつ記述自体も簡潔にすることが出来ます。

expect(hoge.fuga?).to eq trueexpect(hoge).to be_fuga と書き直すことができます。 be_fuga という method は当然存在しませんが、 RSpec が method_missing を使い、test 用の method であると解釈してくれます。これも metaprogramming の例と言うことも出来ます。

spec/message_filter_spec.rb
 # frozen_string_literal: true

 RSpec.describe Rspec3ForBeginners::MessageFilter do
   before do
     @filter = described_class.new("foo")
   end

   it "不適切な単語を含んでいたら検出される" do
-    expect(@filter.detect?("hello from foo")).to be true
+    expect(@filter).to be_detect("hello from foo")
   end

   it "不適切な単語が含まれていなければ検出されない" do
-    expect(@filter.detect?("hello, world!")).to be false
+    expect(@filter).not_to be_detect("hello, world")
   end
 end

1.17(C) RuboCop

すかさず message_filter_spec.rb を保存し、RuboCop を適用します。

> rake rubocop
Inspecting 9 files
.........

9 files inspected, no offenses detected

今回は自動的に整形される部分はなかったようです。

1.17(Y) YARD

YARD を実行しても、説明文の不備は指摘されません (undocumented は0件です)。Spec files (今回は message_filter_spec.rb) は YARD の対象外としているので、当然と言えば当然の結果です。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         2 (    0 undocumented)
 100.00% documented

1.17(S) RSpec

実行してみましょう。

> rake spec

Rspec3ForBeginners::MessageFilter
  不適切な単語を含んでいたら検出される
  不適切な単語が含まれていなければ検出されない

Rspec3ForBeginners
  has a version number

Finished in 0.06135 seconds (files took 0.71848 seconds to load)
3 examples, 0 failures

【注目!】
ここではexpect(...).not_to ...の形式で書いていますが、expect(...).to_not ...と書いても同じように動きます。
どちらが良いのか、という点については Ito さんがこちらの記事にまとめています。(結論としては「どっちでも良い」のですが)

1.18 まだまだ重複がある!

@filter が重複していませんか? これも取り去ることができます。RSpec の subject という機能を使います。subject を使うと、 subject block の評価結果が it 内の is_expected の receiver になります。

spec/message_filter_spec.rb
 # frozen_string_literal: true

+  subject { @filter }
+
 RSpec.describe Rspec3ForBeginners::MessageFilter do
   before do
     @filter = described_class.new("foo")
   end

   it "不適切な単語を含んでいたら検出される" do
-    expect(@filter).to be_detect("hello from foo")
+    is_expected.to be_detect("hello from foo")
   end

   it "不適切な単語が含まれていなければ検出されない" do
-    expect(@filter).not_to be_detect("hello, world")
+    is_expected.not_to be_detect("hello, world")
   end
 end

1.18(C) RuboCop

すかさず message_filter_spec.rb を保存し、RuboCop を適用します。

> rake rubocop
Inspecting 9 files
.........

9 files inspected, no offenses detected

今回は自動的に整形される部分はなかったようです。

1.18(Y) YARD

YARD を実行しても、説明文の不備は指摘されません (undocumented は0件です)。Spec files (今回は message_filter_spec.rb) は YARD の対象外としているので、当然と言えば当然の結果です。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         2 (    0 undocumented)
 100.00% documented

1.18(S) RSpec

実行してみましょう。

> rake spec

Rspec3ForBeginners::MessageFilter
  不適切な単語を含んでいたら検出される
  不適切な単語が含まれていなければ検出されない

Rspec3ForBeginners
  has a version number

Finished in 0.0373 seconds (files took 0.81652 seconds to load)
3 examples, 0 failures

実行結果に変化はありません。

1.19 まだまだまだ重複がある!!

今回の例では before block はほとんど仕事していないですね、 subject block の行内に入れてしまいましょう。

spec/message_filter_spec.rb
 # frozen_string_literal: true

 RSpec.describe Rspec3ForBeginners::MessageFilter do
-  subject { @filter }
+  subject { described_class.new("foo") }

-  before do
-    @filter = described_class.new("foo")
-  end
 
   it "不適切な単語を含んでいたら検出される" do
     is_expected.to be_detect("hello from foo")
   end

   it "不適切な単語が含まれていなければ検出されない" do
     is_expected.not_to be_detect("hello, world")
   end
 end

1.19(C) RuboCop

すかさず message_filter_spec.rb を保存し、RuboCop を適用します。

> rake rubocop
Inspecting 9 files
.........

9 files inspected, no offenses detected

今回は自動的に整形される部分はなかったようです。

1.19(Y) YARD

YARD を実行しても、説明文の不備は指摘されません (undocumented は0件です)。Spec files (今回は message_filter_spec.rb) は YARD の対象外としているので、当然と言えば当然の結果です。

> rake yard
Files:           3
Modules:         1 (    0 undocumented)
Classes:         2 (    0 undocumented)
Constants:       1 (    0 undocumented)
Attributes:      0 (    0 undocumented)
Methods:         2 (    0 undocumented)
 100.00% documented

1.19(S) RSpec

実行してみましょう。

> rake spec

Rspec3ForBeginners::MessageFilter
  不適切な単語を含んでいたら検出される
  不適切な単語が含まれていなければ検出されない

Rspec3ForBeginners
  has a version number

Finished in 0.04172 seconds (files took 0.7074 seconds to load)
3 examples, 0 failures

1.20 第1段階終了

さて、第1段階では最終的に code は以下のようになりました。

lib/rspec3_for_beginners/message_filter.rb
# frozen_string_literal: true

module Rspec3ForBeginners
  # filtering 機能を持つ message の class。
  class MessageFilter
    def initialize(word)
      @word = word
    end

    def detect?(text)
      text.include?(@word)
    end
  end
end
spec/message_filter_spec.rb
# frozen_string_literal: true

RSpec.describe Rspec3ForBeginners::MessageFilter do
  subject { described_class.new("foo") }

  it "不適切な単語を含んでいたら検出される" do
    is_expected.to be_detect("hello from foo")
  end

  it "不適切な単語が含まれていなければ検出されない" do
    is_expected.not_to be_detect("hello, world")
  end
end

1.21 RubyGem 作成

> rake build
rspec3_for_beginners 0.1.0 built to pkg/rspec3_for_beginners-0.1.0.gem.

これで pkg/rspec3_for_beginners-0.1.0.gem として作成されました。

この gem file さえあれば、他人へ配布する場合も、自分が別の環境に移った場合も、gem install rspec3_for_beginners-0.1.0.gem --local として簡単に導入することができます。

1.22 終わりに

元記事では、英語表示に頼りながら RSpec file を簡潔に表現する方針となっていますが、本記事では英語表示になるべく頼らずに日本語表示のままとし、また簡潔な表現は目指しませんでした。

私 (改変者) にとって、RSpec 実行時に日本語で表示されることが最重要であり、RSpec files を簡潔にすることを重視していないためです。

すみません、ここまででかなり力尽きてしまったので、第2段階、第3段階を書く余裕はなさそうです。

0
0
0

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
0
0