📝

テストを書くためにリファクタリングしたいけどそのためのテストがない問題

2023/08/26に公開

はじめに

タイトルの件よくありますよね。
テストが無くとも適切に依存、責務の分離ができていればそこまでこまることはないものの、ほとんどの場合そんなことはないかと思います。
そうは言ってもテストも書かずリファクタリングもせずだと、技術的負債がたまる一方です。
じゃあどうすればいいかの一例の紹介です。

対応例

規模が小さく IDE の機能で対応可能であったり、linter やビルド時に検知可能なリファクタリング

例えば以下のような場合です。

  • 特定の処理を関数やクラスに切り出す
  • 変数名を変更する

このように少しでもミスがあればエディタ上でエラーが表示されたり、ビルドエラーとなるようなものであれば、ある程度安全にリファクタリングが可能かと思います。

処理を切り出す例だと以下のようにAPIの実行処理のみを関数に切り出します。
(昨今のIDEだと選択範囲を関数に自動で切り出してくれたりもします。依存する変数によっては注意が必要ですが)
そうして切り出した関数を呼び出すように置き換えます。

class Logic {
  public async exec() {
    const getResult = await api.getRequest();
    if (getResult.result === 'error') {
      return;
    }
    // getResultを使った処理
    // …
    // …
  }
}

class Logic {
  public async exec() {
    const getResult = await this.getRequest();
    if (getResult.result === 'error') {
      return;
    }
    // getResultを使った処理
    // …
    // …
  }

  protected async getRequest(): ResultModel {
    return await api.getRequest();
  } 
}

最後にこの状態でテストを作成するために、テスト用にLogicクラスを継承したTestingLogicクラスを用意します。
このテスト用クラスでAPI実行部分をオーバーライドし、任意の値を返せるようにします。
これでテストを組むことができるようになったため、以降のリファクタリング作業をテストで確認しながら行うことができます。

class TestingLogic extends Logic {
  protected async getRequest(): ResultModel {
    return MockReturn;
  } 
}

それなりの規模でリファクタリグが必要

本題です。
やることとしては以下になってくるかと思います。

  • できるだけ小さい規模(テストがなくとも安全に変更可能な部分)でリファクタリング
    こちらはやるかどうかは場合によりけりかと思います。
    やる場合はできる限りIDEの機能などで自動でできる範囲にとどめるべきです。
    テストも無く手作業で置き換えを行うと不具合を埋め込む可能性が高くなってしまいます。
  • 大きな変更が必要な場合はその機能を満たすテストを書く
    このとき作成するテストは、外部に依存する処理もテストダブルに置き換えず(そもそも置き換えられないから困っていると思いますが)DBやサーバーなどを起動しておいた状態で動くテストを作成します。
    以降は小さくリファクタリングしつつ、作成したテストを動かしてデグレが起きていないかを確認します。
    そしてテストダブルに置き換えることが可能になれば、そのタイミングで正規のテストを用意して可能な限りテスト駆動でリファクタリングを進めます。

以下のコードのように外部に依存していてかつややこしい構成になっていても、可能な限り小さい範囲のリファクタを施したのちテストを用意します。
そのうえで都度テストを動かすことで動作が変わっていないかを確認しつつ、1要素ずつ書き換えを実施します。
可能ならばgitでローカルリポジトリを用意し、1ステップ毎にコミットしていくのもよいでしょう。
例のコードなら以下のようなフェーズに分かれて作業を行いその都度コミットしていくイメージになるかと思われます。

  • 依存部分を抽出してDIする
  • 複数のことを1関数で行っているため、やっていること毎に処理を抽出(1つ抽出するたび)
  • 必要に応じて切り出した処理をまとめたクラスに抽出
class Logic {
  public async exec() {
    const db = new DBAccess();
    const data = db.getData();
    const postResult = await this.postRequest(data);
    if (getResult.result !== 'error') {
      …
      …
      …
      if (~~) {
        if (~~) {
          while (~~) {
            const insertData = postReslt.hogehoge.filter(a => a);
            const result = db.insert(insertData);
            if (result) {
              …
              …
              …
            }
          }
        }
      }
    }
  }
}

そうは言っても外部依存する部分を使ってもうまくテストが組めない、テストを書くためにはどうしても手動での変更が必要などといったこともあるかと思います。
こういった場合にどうすればよいかなどの情報は是非以下の書籍などを読んでもらえればと思います。

最後に

テストコードも銀の弾丸ではありません。
リファクタリング時の寄るべとするべきですが、それだけに頼らず最後は結合試験などで確認が必要です。
そのためにも、最初に手を加える前にどういうケースの確認が必要なのか書き出すことや、変更前の動作を確認してからリファクタリングを行うようにしましょう。

Discussion