Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Test-Commit-Revert:Rubyでレガシーコードをテストするための便利なワークフロー

それは私たち全員に起こります。ソフトウェアプロジェクトが成長するにつれて、コードベースの一部は、包括的なテストスイートなしで本番環境に移行します。数か月後に同じコード領域をもう一度見ると、理解するのが難しい場合があります。さらに悪いことに、バグがある可能性があり、どこから修正を開始すればよいかわかりません。

テストなしでコードを変更することは大きな課題です。プロセスで何かを壊すかどうかはわかりません。すべてを手動でチェックすることは、せいぜい間違いを犯しがちです。通常、それは不可能です。

この種のコードの処理は、開発者として実行する最も一般的なタスクの1つであり、以前の記事で説明した特性テストなど、多くの手法が長年にわたってこの問題に焦点を当ててきました。

今日は、特性テストに基づいて、何年も前にTDDを現代のプログラミングの世界に紹介したKentBeckによって紹介された別の手法について説明します。

TCRとは何ですか?

TCRは「test、commit、revert」の略ですが、「test &&commit||revert」と呼ぶ方が正確です。理由を見てみましょう。

この手法では、レガシーコードをテストするワークフローについて説明します。プロジェクトファイルを保存するたびにテストを実行するスクリプトを使用します。プロセスは次のとおりです。

  • 最初に、テストするレガシーコードの部分に対して空の単体テストを作成します。
  • 次に、単一のアサーションを追加してテストを保存します。
  • スクリプトが設定されているので、テストは自動的に実行されます。成功すると、変更がコミットされます。失敗した場合、変更は削除(元に戻され)、再試行する必要があります。

テストに合格したら、新しいテストケースを追加できます。

基本的に、TCRは、テスト駆動開発の場合のように、最初に失敗したテスト(赤)を記述してから合格(緑)にするのではなく、コードを「緑」の状態に保つことを目的としています。失敗したテストを書くと、それは消えて、再び「緑」の状態に戻ります。

目的

この手法の主な目標は、テストケースを追加するたびにコードを少しよく理解することです。これにより、テストカバレッジが自然に増加し、そうでなければ不可能だった多くのリファクタリングのブロックが解除されます。

TCRの利点の1つは、多くのシナリオで役立つことです。テストがまったくないコードでも、部分的にテストされたコードでも使用できます。テストに合格しなかった場合は、変更を元に戻して再試行します。

どのように使用できますか?

Kent Beckは、さまざまな記事やビデオ(最後にリンクされています)で、プロジェクト内の特定のファイルが保存された後に実行されるスクリプトを使用するのが良いアプローチであることを示しています。

これは、テストしようとしているプロジェクトに大きく依存します。エディターでプラグインを使用してファイルを保存するたびに実行される次のスクリプトのようなものは、良いスタートです:

(rspec && git commit -am "WIP") || git reset --hard

Visual Studio Codeを使用している場合、保存のたびに実行するのに適したプラグインは「runonsave」です。上記のコマンドまたは同様のコマンドをプロジェクトに含めることができます。この場合、構成ファイル全体は

になります。
{
  "folders": [{ "path": "." }],
  "settings": {
    "emeraldwalk.runonsave": {
      "commands": [
        {
          "match": "*.rb",
          "cmd": "cd ${workspaceRoot} && rspec && git commit -am WIP || git reset --hard"
        }
      ]
    }
  }
}

後で、コマンドラインで直接、またはGithubを使用している場合はPRをマージするときに、Gitを使用してコミットを潰すことができることを覚えておいてください。

Test-Commit-Revert:Rubyでレガシーコードをテストするための便利なワークフロー

これは、作業中のブランチで行ったすべてのコミットに対して、メインブランチで1つのコミットのみを取得することを意味します。 Githubのこの図は、それをよく説明しています:

Test-Commit-Revert:Rubyでレガシーコードをテストするための便利なワークフロー

TCRを使用した最初のテストの作成

簡単な例を使用して、テクニックを説明します。動作していることがわかっているクラスがありますが、それを変更する必要があります。

変更を加えて展開するだけです。ただし、プロセスで何も壊さないようにする必要があります。これは常に良い考えです。

# worker.rb
class Worker
  def initialize(age, active_years, veteran)
    @age = age
    @active_years = active_years
    @veteran = veteran
  end

  def can_retire?
    return true if @age >= 67
    return true if @active_years >= 30
    return true if @age >= 60 && @active_years >= 25
    return true if @veteran && @active_years > 25

    false
  end
end

最初のステップは、テスト用の新しいファイルを作成することです。これにより、そこにテストを追加できるようになります。 can_retire?の最初の行を見ました

を使用したメソッド
  def can_retire?
    return true if @age >= 67
    ...
    ...
  end

したがって、最初にこのケースをテストできます:

# specs/worker_spec.rb
require_relative './../worker'

describe Worker do
  describe 'can_retire?' do
    it "should return true if age is higher than 67" do

    end
  end
end

簡単なヒントを次に示します。TCRを使用している場合、保存するたびに、テストに合格しないと最新の変更が表示されなくなります。したがって、アサーションを含む1つまたは複数の行を実際に記述して保存する前に、テストを「セットアップ」するためのできるだけ多くのコードが必要です。

上記のファイルをそのように保存すると、テスト用の行を追加できます。

require_relative './../worker'

describe Worker do
  describe 'can_retire?' do
    it "should return true if age is higher than 67" do
      expect(Worker.new(70, 10, false).can_retire?).to be_true ## This line can disappear when we save now
    end
  end
end

保存するときに、新しい行が消えない場合は、良い仕事をしています。テストに合格しました!

テストを追加する

最初のテストが完了したら、誤ったケースを考慮しながら、さらにケースを追加し続けることができます。いくつかの作業の後、次のようなものがあります:

# frozen_string_literal: true

require_relative './../worker'

describe Worker do
  describe 'can_retire?' do
    it 'should return true if age is higher than 67' do
      expect(Worker.new(70, 10, false).can_retire?).to be true
    end

    it 'should return true if age is 67' do
      expect(Worker.new(67, 10, false).can_retire?).to be true
    end

    it 'should return true if age is less than 67' do
      expect(Worker.new(50, 10, false).can_retire?).to be false
    end

    it 'should return true if active years is higher than 30' do
      expect(Worker.new(60, 31, false).can_retire?).to be true
    end

    it 'should return true if active years is 30' do
      expect(Worker.new(60, 30, false).can_retire?).to be true
    end
  end
end

いずれの場合も、最初に「it」ブロックを記述し、保存してから、expect(...)を使用してアサーションを追加します。 。

いつものように、できるだけ多くのテストを追加できますが、すべてがカバーされていることが比較的確実になったら、追加しすぎないようにするのが理にかなっています。

カバーするケースはまだいくつかあるので、完全を期すために追加する必要があります。

最終テスト

これが最終的な形式のスペックファイルです。ご覧のとおり、さらにケースを追加することもできますが、TCRのプロセスを説明するにはこれで十分だと思います。

# frozen_string_literal: true

require_relative './../worker'

describe Worker do
  describe 'can_retire?' do
    it 'should return true if age is higher than 67' do
      expect(Worker.new(70, 10, false).can_retire?).to be true
    end

    it 'should return true if age is 67' do
      expect(Worker.new(67, 10, false).can_retire?).to be true
    end

    it 'should return true if age is less than 67' do
      expect(Worker.new(50, 10, false).can_retire?).to be false
    end

    it 'should return true if active years is higher than 30' do
      expect(Worker.new(60, 31, false).can_retire?).to be true
    end

    it 'should return true if active years is 30' do
      expect(Worker.new(20, 30, false).can_retire?).to be true
    end

    it 'should return true if age is higher than 60 and active years is higher than 25' do
      expect(Worker.new(60, 30, false).can_retire?).to be true
    end

    it 'should return true if age is higher than 60 and active years is higher than 25' do
      expect(Worker.new(61, 30, false).can_retire?).to be true
    end

    it 'should return true if age is 60 and active years is higher than 25' do
      expect(Worker.new(60, 30, false).can_retire?).to be true
    end

    it 'should return true if age is higher than 60 and active years is 25' do
      expect(Worker.new(61, 25, false).can_retire?).to be true
    end

    it 'should return true if age is 60 and active years is 25' do
      expect(Worker.new(60, 25, false).can_retire?).to be true
    end

    it 'should return true if is veteran and active years is higher than 25' do
      expect(Worker.new(60, 25, false).can_retire?).to be true
    end
  end
end

リファクタリングの方法

ここまで読んだことがあるなら、おそらくコードに少し違和感を覚える何かがあるでしょう。テストとWorkerクラスの両方で、定数に抽出する必要のある「魔法の数」がたくさんあります。

メインのcan_retireでケースごとにプライベートメソッドを作成することもできますか?パブリックメソッド。

両方の潜在的なリファクタリングを演習として残しておきます。ただし、現在テストが行​​われているため、いずれかのステップでミスをした場合は、テストで教えてくれます。

結論

プロジェクトでTCRを試してみることをお勧めします。外部サーバーへの継続的インテグレーションや新しいライブラリとの依存関係を必要としないため、これは非常に安価な実験です。必要なのは、特定のファイルをコンピューターに保存するたびにコマンドを実行する方法だけです。

また、テストを追加するときに「ゲーム」体験を提供します。これは常に楽しく興味深いものです。さらに、失敗したテストをエディターから削除するという規律は、リポジトリにプッシュしているテストが合格していることを確認することで、追加のセーフティネットを提供します。

この新しい手法がレガシーコードを処理するときに役立つことを願っています。私は過去数か月に何度も使用しましたが、それはいつも喜びでした。

追加のリソース
  • 紹介としての優れた動画。
  • ケントベックがVSCodeでTCRを使用する方法について説明します。
  • 保存時にスクリプトを実行するためのVSCodeのプラグイン。

  1. Ruby開発者向けのラックの説明

    すべてのRails、Sinatra、およびその他のRuby Webフレームワークの舞台裏で何が起こっていますか? 答えは、これを可能にする重要なコンポーネントであるラックです。 しかし、ラックとは正確には何ですか? Rackは、フレームワーク(Rails)とアプリケーションサーバー(Puma)の間のレイヤーです。 彼らがコミュニケーションをとることを可能にするのは接着剤です。 なぜラックを使用するのですか? さまざまなフレームワークとサーバーを交換できるため、Rackを使用しています。 それらはプラグインできるコンポーネントになります 。 これは、Rails、Sinatr

  2. Rubyでの静的分析

    ソースコードを解析して、すべてのメソッド、それらが定義されている場所、およびそれらが取る引数を見つけたいとします。 どうすればこれができますか? あなたの最初のアイデアはそれのために正規表現を書くことかもしれません… しかし、もっと良い方法はありますか? はい! 静的分析 は、ソースコード自体から情報を抽出する必要がある場合に使用できる手法です。 これは、ソースコードをトークンに変換する(解析する)ことによって行われます。 さっそく始めましょう! パーサージェムの使用 Rubyには標準ライブラリで利用可能なパーサーがあります。名前はRipperです。出力を操作するのは難し