外部リソースに依存したコードを外部リソースに依存しないようにテストしたいんだけど、どうしたらいいの

こんにちわ。テスト熱中症真っ盛りな自覚はありますが、それほどテストコードは書いてないプログラマーのクズです。

今回はタイトルについて、ちょっとうまい方法が思いつかないのと、寝て起きたら忘れてしまうのを防ぐために自分の考えをメモします。(思いついて書いてる部分がほとんどなので変なところがありそう)

その前に用語説明として、ここでいう"外部リソースに依存したコード"について定義しておきます。

  • DBやファイルといった、マシン内のリソースにアクセスするコード
  • WebAPIを使ったHTTPリクエストといった、外部との通信を必要とするコード

さて、突然ですが、JavaでWEBからDB上にあるユーザ情報を更新するロジックを書きたいと思います。

(本題は後半の"外部リソースに依存している箇所"あたりです。)

class UserInfoDao{
    private Connection con; // コンストラクタでsetする
    public boolean updateUserInfo(UserInfo userInfo){
        PreparedStatement statement = con.preparedStatement("UPDATE FROM UserInfo SET userid = ? , param1 = ? ...");
        // ... statementへuserInfoのデータを詰め込む処理
        return statement.executeUpdate();
    }
    // ... こんな感じで selectUserInfo的なものやdeleteUserInfo的なものを作っていく
}

自分がよく仕事で書いてたデータアクセスオブジェクト(Dao)です。SQLとか臭いものはここで蓋をします。

これを使うサーブレットを書いておきましょう。

class UserServlet{
    ConnectionPool connectionPool;
    public void init(){
        connectionPool = ConnectionPoolFactory.getConnectionPool(); // どこかで管理しているコネクションプールを取得
    }

    // POSTメソッドでリクエストを受けた時は更新する
    public boolean doPost(HttpServletRequest request, HttpServletResponse response){
        validateRequest(request);
        UserInfo userInfo = parseUserInfoFromRequest(request);
        UserInfoDao userInfoDao = new UserInfoDao(connectionPool.getConnection());
        boolean result = userInfoDao.updateUserInfo(userInfo);

        // ... resultの結果とかをみて、responseに適切な値を詰め込む
        return result;
    }

    public void validateRequest(HttpServletRequest request) throws RuntimeException {
        // requestの内容を検証... 異常だったら実行時例外を投げる
    }

    public UserInfo parseUserInfoFromRequest(HttpServletRequest request){
        UserInfo userInfo = new UserInfo();
        // ... request内容からユーザの情報を取り出し、userInfoへセットする
        return userInfo;
    }
}

いいコードがかけました。

さて、これをテストしたい、となると、どのようなアプローチがあるでしょうか。

まずはUserServletをテストすることを考えますと、以下の流れ作業を行うことになるでしょう。

  1. HTTPサーバ(J2EEコンテナ)とDBサーバを用意
  2. テスト環境のDBを使うための設定
  3. 実行前にあらかじめDBのUserInfoへデータを投入
  4. POSTリクエス
  5. レスポンス結果を確認
  6. DBのUserInfoのデータを確認

これは結合試験でやる流れですし、テストケースを考えると結構な数になります。
でも今回は結合試験はどうでもいいです。単体試験がしたいんです。

では単体試験が出来るところを探してみましょう。

メソッドから

UserServlet#validateRequestはチョロそう

UserServlet#validateRequestはHttpServletRequestのパラメータとthrowする時の例外に注目すればよさそうです。

HttpServletRequestは継承できますので、DummyHttpServletRequestとか自分でgetParameterの値をカスタムできるようなクラスを1個作れば簡単にテストできそうです。

これぐらいのケースをやっておけば単体試験としては問題なさそうです

  1. 問題なく通過するケース
  2. 異常なパターン(byte数の境界値チェックとか異常値チェックとか) x パラメータの数
    • 同時にどういう例外になるのかチェック

結構な数ありそうですが、結合試験で網羅するよりは簡単ですね。

このぐらいのテストならJUnitとかで書くのも簡単ですし、書いておくとリファクタリングも捗る事でしょう。

同じ理由でUserServlet#parseUserInfoFromRequestもチョロそうです。

こういった外部リソースを使わないメソッドのテストは簡単そうです。


Servletの単体試験

Servlet単位のテストは、今まではやらなくていいかなと思ってました。

だって後で結合試験でやるのだし、不要かもしれません。コストに見合わないかもしれません。

でも今の自分はこれをテストしたいんです。

UserServlet#doPostとしては、

  • リクエスト内容のチェック
  • ユーザ情報の更新
  • 適切なレスポンス

という事が行われればOKなわけです。

ただ、このうち上の2つは単体試験に落としこめる内容だと思います。


つまりServletのテストでは "上記の処理が順番に行われている事を担保すること"と、"適切なレスポンス"のチェック ができれば、後は他の単体試験でカバーできるはずです。

"上記の処理が順番に行われている事を担保すること"については、
処理をメソッド単位で切り出し、メソッドが順番に呼ばれたことを見る、ことで出来ると思います。

どのメソッドが呼ばれたかをチェックするにはProxy.newProxyInstanceなどでプロキシクラスを作れば検証できます。
(実際はmockitでspyとかする感じでしょうか。)


なので、ServletのdoPostを書き換えて、メソッドをいくつか追加します。

class UserServlet{
    public boolean doPost(HttpServletRequest request, HttpServletResponse response){
        validateRequest(request);
        UserInfo userInfo = parseUserInfoFromRequest(request);
        boolean result = updateUserInfo(userInfo);
        mappingResultResponse(response);
        return result;
    }

    private boolean updateUserInfo(UserInfo userinfo){
        UserInfoDao userInfoDao = new UserInfoDao(connectionPool.getConnection());
        return userInfoDao.updateUserInfo(userInfo);
    }

    private void mappingResultResponse(boolean result, HttpServletResponse response){
        // ... resultの結果とかをみて、responseに適切な値を詰め込む
    }
}

あとはプロキシクラスを作り検証すれば、処理の順序を担保するようなコードはかけそうです。

あとdoPostにあった汚らしい部分が消えてよりロジックに集中できてる感じでとてもいい感じに見えませんか?

個人的にはメソッド切り出しは積極的にやるべきだと思うんですよ。

外部リソースに依存している箇所

さて、前振りが長くなりました。本題です。

上のServletの単体試験の部分でさらっと、ユーザ情報の更新は単体試験に落としこめる内容だ、といってますが、

これが自分の中では非常に難しいと考えてます。

UserServlet#updateUserInfoを単体試験するときは、以下のようにすれば可能です。

  • 引数を与える
  • UserInfoDao#updateUserInfoを呼ぶ
    • updtaeUserInfoの振る舞いのテストはここでは行わない。UserInfoDaoの単体試験でやる。
  • 戻り値をチェック

しかし、UserServlet#updateUserInfoのコードを見てください。
UserServlet内でnew UserInfoDao(conn)をしてます。
なので、UseServlet#updateUserInfoの単体試験をする場合は、UserInfoDaoが正しく振舞う必要があります。

ここでいう"UserInfoDaoが正しく振舞う"というのはUserInfoDaoはConnectionを使い、DBのデータ(外部リソース)を操作することです。
この外部リソースというのが非常に厄介です。


まずミドルウェアを用意する必要があります。MySQL/PostgreSQL/Orecle Database など何でもいいですが、とにかく必要です。
次にそいつにデータを投入しておく必要があります。とにかく必要です

UserInfoDaoの振る舞いが正しい事を確認するには、外部リソースを準備する必要がありますが、

UserInfoDaoを使うクラスの振る舞いぐらいは外部リソースを意識しないようにできたらいいのではー?

管理する仕組みを作る

管理する仕組み、というと大仰そうですが、
必要なデータを取るためのクラス(Dao)を取るためのクラス(DaoManager)を作ります。

普通はUserInfoDaoを返すようにして、テストのときはカスタムされたUserInfoDaoを返すようなクラスです。

     // Daoインスタンスを生成するだけのクラス
     DaoManager daoManager = DaoManager.getInstance();
     // 使う時はこんな感じで 
     UserInfoDao userInfoDao = daoManager.getUserInfoDao(con);

テストのときを判定するなら、環境変数Javaならシステムプロパティを使う事でこの辺りの振る舞いを変えることはできそうです。

ただ、テスト毎に返すインスタンスを変化させる要件を満たそうとすると難し気がします。

DaoManagerクラスですべてやろうとすると地獄絵図と化すことは目に見えてますし、Dao追加時にメンテナンスコストが高くつきます。

この方法は、やめたほうが良さそうです。

外部リソースを使うクラスのインスタンス生成はメソッドで隠す

アイディアとしてUserInfoDaoを返すメソッドを作るのはどうでしょう。

    // Protectedにすることで継承可能にする
    protected UserInfoDao createUserInfoDao(){
        return new UserInfoDao(con);
    }

new UserInfoDao(con)としている箇所をcreateUserInfoDao()に置き変えます。

単体試験のときはここを無名クラスでオーバーライドし、何もしないDummyUserInfoDaoを返すようにします。

    UserServler servlet = new UserServler(){
        protected UserInfoDao createUserInfoDao(){
            return new DummyUserInfoDao();
        }
    };

DummyUserInfoDaoにはconnectionを使わず、あたかも処理したかのように振舞わせます。

updateUserInfoならfalseを返したり、selectUserInfoなら適当なUserInfoを作って返したり。

DummyUserInfoDaoのクラスが大量に出来上がってしまいそうですが。少なくともUserInfoDaoにあるメソッド1個に付き、1クラスレベルの粒度になりそうです。

それに外部リソースは1つじゃないので、2つ,3つとなると結構な負担になりそうです。

同名だけど別のクラスを返す

メソッドに切り出すのを認められないとなると、テスト時はUserInfoDaoクラスを黒魔術により変化させるしかありません。

独自ClassLoaderを使う事で、テスト時だけ優先的に読ませるクラスファイルを定義することもできます。

Instrumentation/ClassFileTransformerを使うことで実行時にUserInfoDaoクラスをすり返ることもできます

しかしそこまでやる必要があるのでしょうか。後者はjavaagentです。


実際はどうなの

試してないのでわからないです。

仕事だと適用する気にならないし、同僚はテストなんか興味ないし、個人ではもうほとんどコード書いてないし・・・

Perl界隈のテスト事情を調べてみたら、こういう場合はTest::mysqldを使ってテスト用のmysqlを立ち上げるようだし、あんまりこういうことはしないのかなー。