Tbpgr Blog

Employee Experience Engineer tbpgr(てぃーびー) のブログ

Ruby | リファクタリング | 冗長なコードを記述=>メソッドの抽出=>動的メソッド定義 の流れのサンプル

概要

冗長なコードをmetric_fuで検出=>メソッドの抽出=>動的メソッド定義 の流れのサンプル

詳細

・プロダクトコード、テストコードのひな型を生成
・テストコードを記述
・冗長なコードを記述
・テストコードをリファクタリング
・プロダクトコードのリファクタリングメソッドの共通処理を抽出(extract method)
・プロダクトコードのリファクタリングメソッド定義自体を動的に行う
の流れでTDD+リファクタリングを行ってみます。
(※RSpecを利用するので本当はBDDなのですが、BDDっぽい書き方をしていないのでTDDとします)

仕様

Hogeクラスにhoge1, hoge2, hoge3メソッドを作成。
メソッドメソッド名と同じ文字列を返却するのみ。
引数はなし。

rspec初期化
rspec -i
プロダクトコード、テストコードのひな型を生成

自作gemのRSpecPiccoloを利用します。
https://github.com/tbpgr/rspec_piccolo

下記のコマンドでHogeクラスをファイル名hoge.rbで作成して
hoge1,hoge2,hoge3のプロダクトコードのひな形とテストコードのひな形を生成します。

piccolo e Hoge hoge hoge1 hoge2 hoge3 -p

下記のようにファイルが生成されました。
構成

tree
├ lib
| └ hoge.rb
└ spec
    └ hoge_spec.rb

RSpecPiccoloによって生成されたプロダクトコードのひな形
lib/hoge.rb

# encoding: utf-8

class Hoge
  def hoge1
    # TODO: implement your code
  end

  def hoge2
    # TODO: implement your code
  end

  def hoge3
    # TODO: implement your code
  end
end

RSpecPiccoloによって生成されたテストコードのひな形
lib/hoge_spec.rb

# encoding: utf-8
require "spec_helper"
require "hoge"

describe Hoge do

  context :hoge1 do
    cases = [
      {
        case_no: 1,
        case_title: "case_title",
        expected: "expected",
      },
    ]

    cases.each do |c|
      it "|case_no=#{c[:case_no]}|case_title=#{c[:case_title]}" do
        begin
          case_before c

          # -- given --
          hoge = Hoge.new

          # -- when --
          # TODO: implement execute code
          # actual = hoge.hoge1

          # -- then --
          # TODO: implement assertion code
          # expect(actual).to eq(c[:expected])
        ensure
          case_after c
        end
      end

      def case_before(c)
        # implement each case before
      end

      def case_after(c)
        # implement each case after
      end
    end
  end

  context :hoge2 do
    cases = [
      {
        case_no: 1,
        case_title: "case_title",
        expected: "expected",
      },
    ]

    cases.each do |c|
      it "|case_no=#{c[:case_no]}|case_title=#{c[:case_title]}" do
        begin
          case_before c

          # -- given --
          hoge = Hoge.new

          # -- when --
          # TODO: implement execute code
          # actual = hoge.hoge2

          # -- then --
          # TODO: implement assertion code
          # expect(actual).to eq(c[:expected])
        ensure
          case_after c
        end
      end

      def case_before(c)
        # implement each case before
      end

      def case_after(c)
        # implement each case after
      end
    end
  end

  context :hoge3 do
    cases = [
      {
        case_no: 1,
        case_title: "case_title",
        expected: "expected",
      },
    ]

    cases.each do |c|
      it "|case_no=#{c[:case_no]}|case_title=#{c[:case_title]}" do
        begin
          case_before c

          # -- given --
          hoge = Hoge.new

          # -- when --
          # TODO: implement execute code
          # actual = hoge.hoge3

          # -- then --
          # TODO: implement assertion code
          # expect(actual).to eq(c[:expected])
        ensure
          case_after c
        end
      end

      def case_before(c)
        # implement each case before
      end

      def case_after(c)
        # implement each case after
      end
    end
  end
end
テストコードを記述

spec/hoge_spec.rb

# encoding: utf-8
require "spec_helper"
require "hoge"

describe Hoge do

  context :hoge1 do
    cases = [
      {
        case_no: 1,
        case_title: "hoge1 case",
        expected: "hoge1",
      },
    ]

    cases.each do |c|
      it "|case_no=#{c[:case_no]}|case_title=#{c[:case_title]}" do
        begin
          case_before c

          # -- given --
          hoge = Hoge.new

          # -- when --
          actual = hoge.hoge1

          # -- then --
          expect(actual).to eq(c[:expected])
        ensure
          case_after c
        end
      end

      def case_before(c)
        # implement each case before
      end

      def case_after(c)
        # implement each case after
      end
    end
  end

  context :hoge2 do
    cases = [
      {
        case_no: 1,
        case_title: "hoge2 case",
        expected: "hoge2",
      },
    ]

    cases.each do |c|
      it "|case_no=#{c[:case_no]}|case_title=#{c[:case_title]}" do
        begin
          case_before c

          # -- given --
          hoge = Hoge.new

          # -- when --
          actual = hoge.hoge2

          # -- then --
          expect(actual).to eq(c[:expected])
        ensure
          case_after c
        end
      end

      def case_before(c)
        # implement each case before
      end

      def case_after(c)
        # implement each case after
      end
    end
  end

  context :hoge3 do
    cases = [
      {
        case_no: 1,
        case_title: "hoge3 case",
        expected: "hoge3",
      },
    ]

    cases.each do |c|
      it "|case_no=#{c[:case_no]}|case_title=#{c[:case_title]}" do
        begin
          case_before c

          # -- given --
          hoge = Hoge.new

          # -- when --
          actual = hoge.hoge3

          # -- then --
          expect(actual).to eq(c[:expected])
        ensure
          case_after c
        end
      end

      def case_before(c)
        # implement each case before
      end

      def case_after(c)
        # implement each case after
      end
    end
  end
end

テストを実行します(まだプロダクトコードを実装していないので全件失敗します)

$ rspec
Run options: include {:focus=>true}

All examples were filtered out; ignoring {:focus=>true}
FFF

Failures:

  1) Hoge hoge3 |case_no=1|case_title=hoge3 case
     Failure/Error: expect(actual).to eq(c[:expected])

       expected: "hoge3"
            got: nil

       (compared using ==)
     # ./spec/hoge_spec.rb:102:in `block (4 levels) in <top (required)>'

  2) Hoge hoge2 |case_no=1|case_title=hoge2 case
     Failure/Error: expect(actual).to eq(c[:expected])

       expected: "hoge2"
            got: nil

       (compared using ==)
     # ./spec/hoge_spec.rb:65:in `block (4 levels) in <top (required)>'

  3) Hoge hoge1 |case_no=1|case_title=hoge1 case
     Failure/Error: expect(actual).to eq(c[:expected])

       expected: "hoge1"
            got: nil

       (compared using ==)
     # ./spec/hoge_spec.rb:28:in `block (4 levels) in <top (required)>'

Finished in 0.002 seconds
3 examples, 3 failures

Failed examples:

rspec ./spec/hoge_spec.rb:91 # Hoge hoge3 |case_no=1|case_title=hoge3 case
rspec ./spec/hoge_spec.rb:54 # Hoge hoge2 |case_no=1|case_title=hoge2 case
rspec ./spec/hoge_spec.rb:17 # Hoge hoge1 |case_no=1|case_title=hoge1 case

Randomized with seed 21394
まずは冗長なコードを作成

まずは共通化など一切無しで実装してみます。

# encoding: utf-8

class Hoge
  def hoge1
    'hoge1'
  end

  def hoge2
    'hoge2'
  end

  def hoge3
    'hoge3'
  end
end

テストを実行します。(全件成功)

$ rspec
Run options: include {:focus=>true}

All examples were filtered out; ignoring {:focus=>true}
...

Finished in 0.002 seconds
3 examples, 0 failures

Randomized with seed 20945
テストコードをリファクタリング

テストコードが冗長なのでリファクタリングします。
spec/hoge_spec.rb

# encoding: utf-8
require "spec_helper"
require "hoge"

describe Hoge do
  %w{1 2 3}.each do |i|
    context "hoge#{i.to_s}".to_sym do
      cases = [
        {
          case_no: 1,
          case_title: "hoge#{i} case",
          expected: "hoge#{i}",
        },
      ]

      cases.each do |c|
        it "|case_no=#{c[:case_no]}|case_title=#{c[:case_title]}" do
          begin
            case_before c

            # -- given --
            hoge = Hoge.new

            # -- when --
            actual = hoge.send "hoge#{i}"

            # -- then --
            expect(actual).to eq(c[:expected])
          ensure
            case_after c
          end
        end

        def case_before(c)
          # implement each case before
        end

        def case_after(c)
          # implement each case after
        end
      end
    end
  end
end

テストを実行します

$ rspec
Run options: include {:focus=>true}

All examples were filtered out; ignoring {:focus=>true}
...

Finished in 0.002 seconds
3 examples, 0 failures

Randomized with seed 21035
プロダクトコードのリファクタリングメソッドの共通処理を抽出(extract method)

lib/hoge.rb

# encoding: utf-8

class Hoge
  def hoge1
    hoge(1)
  end

  def hoge2
    hoge(2)
  end

  def hoge3
    hoge(3)
  end

  private
    def hoge(index)
      "hoge#{index}"
    end
end

テストを実行します

$ rspec
Run options: include {:focus=>true}

All examples were filtered out; ignoring {:focus=>true}
...

Finished in 0.00208 seconds
3 examples, 0 failures

Randomized with seed 8826
プロダクトコードのリファクタリングメソッド定義自体を動的に行う

lib/hoge.rb

# encoding: utf-8

class Hoge
  %w{1 2 3}.each do |i|
    define_method "hoge#{i}".to_sym do
      "hoge#{i}"
    end
  end
end

テストを実行します

$ rspec
Run options: include {:focus=>true}

All examples were filtered out; ignoring {:focus=>true}
...

Finished in 0.00208 seconds
3 examples, 0 failures

Randomized with seed 56847

リファクタリング完了です。