はじめに
まだエンジニアに成りたての頃、3年ほど継続で改修している案件で単体テストを書いたことがあります。
創業から継ぎ足し続けているタレのような案件(?)だったため、かなりつらみのある単体テストになり一時期はテスト嫌いになりました。
昔の私に限らず、テストに対して嫌なイメージを持っている方は多いような気がします。
しかし、何年かエンジニアをやっていて、実際には単体テストが悪いのではなく、Testableでないコードが悪いのだと思うようになりました。
単体テストの何がつらいって?
それはモックアップとかテストデータの用意とかです。それですよ。
JMockitとか使ってモックを用意する。単純なメソッドなら簡単でもstaticとかprivateとかmember変数とか...そういった面倒なモックに心を削られるんです。
あとは、工夫を凝らしたテストデータをDBに流し込んで...、まだ完成していないサーバ側を再現するためのモックサーバを構築して...みたいな地獄。
いや、それ単体テストじゃないから
さて、考えるだけでも恐ろしい単体テストですが、そもそも上で挙げた例の一部はもはや単体テストではありません。
DBとかネットワークとかと結合してしまった段階で、それは結合テストです。
なぜかエンジニア業界の一部界隈では単体テストと言いながら結合テストをする文化がありますが、理由は簡単です。
プログラムがちゃんと単体に分解できないからです。
みんな、DIしようぜ!!
ちゃんとプログラムを単体にするために必要なのがDIです。まず、DIしていないコードを見てみましょう。
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
// CSVファイルを配列に格納するJavaコード
class CsvConverter {
public List<String[]> getData(Path filepath) throws Exception {
// ファイルから文字列を直接読み取って
List<String> lines = Files.readAllLines(filepath);
// カンマで区切ってリストに突っ込む
List<String[]> result = new ArrayList<>();
for (String line : lines) {
result.add(line.split(","));
}
return result;
}
}
このプログラムは適当に組んだので動かないかもしれませんが...問題はそこではありません。
このコードで正しくCSVファイルを配列に変換できているか単体テストしようと思ったとき、問題が発生します。
そう、実際にファイルが無いと動かないんです。これでは、環境によってはテスト出来ないかもしれません。
JMockitでFiles.readAllLinesをモックしますか?精神が擦り減るので止めましょう。
DIを用いたTestableなコードを示します。
まず、インターフェースを定義します。
import java.nio.file.Path;
import java.util.List;
interface IFileService {
// ファイルパスを与えるとファイルを読み込む
List<String> readFile(Path filepath) throws Exception;
}
このインターフェースを実装したクラスも用意します。
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class FileService implements IFileService {
// Filesを用いた実際の処理
@Override
public List<String> readFile(Path filepath) throws Exception {
return Files.readAllLines(filepath);
}
}
さらに、テスト用のモッククラスも用意します。
このクラスではコンストラクタで与えたデータをオウム返しするようになっています。
import java.nio.file.Path;
import java.util.List;
public class MockFileService implements IFileService {
private List<String> data;
// コンストラクタでテストデータを受け取って
public MockFileService(List<String> data) {
this.data = data;
}
// オウム返しする
@Override
public List<String> readFile(Path filepath) throws Exception {
return data;
}
}
これらを使ったTestableなコードがこちらになります。
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
class CsvConverter {
private IFileService fileService;
// コンストラクタで依存関係(ファイルシステム)を受け取って
public CsvConverter(IFileService fileService) {
this.fileService = fileService;
}
// 依存関係を用いて処理を行う
public List<String[]> getData(Path filepath) throws Exception {
List<String> lines = fileService.readFile(filepath);
List<String[]> result = new ArrayList<>();
for (String line : lines) {
result.add(line.split(","));
}
return result;
}
}
何が良くなったのか?
DIをする前は、実際のファイルが無ければ単体テストが出来ませんでした。
そもそも、プログラムとファイルシステムが結合してしまっているので、単体ではなかったのです。
一方で、DIするプログラムではFileServiceとMockFileServiceを使い分けることで、プログラムをファイルシステムから切り離すことができるため、ロジックだけをテストすることが可能です。
それならJMockitとか使っても同じじゃん!って思うかもしれませんが、プログラムの様々な場面で使われる依存関係をその都度モックするのはテストコードの美しさからも、テストコードの作成コストからも最適ではありません。
そもそも、モックしないといけない依存関係があっちこっちに散らばっていることがまず問題なのです。
また、これは主観的な問題になってしまいますが、依存関係や単体テストを意識することで、全体が疎結合になり、処理があるべき場所に収まりやすくなると思います。
気を付けるべきポイント
Testableなコードを書くために必要なことは次の通りです。
・最低限、インフラストラクチャ(主にDB、ネットワーク、ファイルシステム)は切り出す(今回のFileServiceのように)
・その他にも、ある程度大きな処理の塊にはインターフェースを設けて切り出す
以上、ありがとうございました。