Java 1.8/JUnit 4等のレガシーな開発環境において、単体テストでC1網羅を求められるケースがありました。
関連するライブラリの過去バージョンでは、カバレッジ検出時にコンパイラがコンパイル後に出力した分岐が未到達とされ、一見しただけでは対処しにくい状況になります。
以下に対処方法をまとめたため、同事例に遭遇した方の一助となれば幸いです。
問題
- catchの行で分岐が8あると表示される
※tryブロック内の書きようによっては8以上になる - なぜか1パターンだけどうしても通らない
結論
以下を網羅する。
- try→finally(tryが正常終了)
- 取得したリソースが非nullで、リソース破棄が正常終了
- 取得したリソースがnull
- try→finally→catch(tryで異常発生)
- 取得したリソースがnull
- 取得したリソースが非nullで、リソース破棄が正常終了
- 取得したリソースが非nullで、リソース破棄が異常終了
- resources→finally→catch
※tryに入る前に例外が発生したパターン
サンプルコード
public class TryWithResources {
/**
* テスト対象の処理
*/
public void perform(Logic logic, CloseableFactory resourceFactory) throws IOException {
// ※ログ用メソッドの詳細は最下部参照
printLine("try-with-resources:開始");
// try-with-resources句
try (
// パターンは三通り。
// 1.正常(例外なしでリソースを取得)
// 2.nullを取得(nullを取得しても例外にはならない)
// 3.例外が発生
Closeable resource = resourceFactory.generate()) {
printLine("ビジネスロジック:開始");
// 処理の本体
logic.doLogic();
printLine("ビジネスロジック:終了");
} catch (Throwable t) {
// 例外発生
printLine("(catch)" + t.getMessage());
}
// コンパイル後展開されるfinally節では、以下の分岐が発生
// 1.リソースが非nullで、リソース破棄が正常終了した
// 2.リソースが非nullで、リソース破棄で異常が発生した
// 3.リソースがnullだった
printLine("try-with-resources:終了");
}
/**
* ビジネスロジックの実行インタフェース
*/
interface Logic {
public void doLogic() throws Throwable;
}
/**
* リソースのファクトリ・インタフェース
*/
interface CloseableFactory {
public Closeable generate() throws Throwable;
}
/**
* ビジネスロジックの処理パターン
*/
static enum LogicCases implements Logic {
/** 正常 */
NORMAL {
@Override
public void doLogic() throws Throwable {
printLine("ビジネスロジック:正常");
}
},
/** 異常 */
ERROR {
@Override
public void doLogic() throws Throwable {
printLine("ビジネスロジック:異常");
throw new Throwable("異常:ビジネスロジック");
}
};
@Override
public abstract void doLogic() throws Throwable;
}
/**
* リソースの処理パターン
*/
static enum ResourceCases implements CloseableFactory {
/** リソースがnullのケース */
NULL_RESOURCE {
@Override
public Closeable generate() {
printLine("リソース取得:null");
return null;
}
},
/** リソース取得時に例外が発生したケース */
ERROR_IN_RESOURCE {
@Override
public Closeable generate() throws Throwable {
printLine("リソース取得:例外発生");
throw new Throwable("異常:リソース取得");
}
},
/** 正常ケース */
NORMAL {
@Override
public Closeable generate() {
printLine("リソース取得:正常");
return new Closeable() {
@Override
public void close() throws IOException {
printLine("リソース破棄:正常");
}
};
}
},
/** リソースのclose時に例外が発生したケース */
ERROR_IN_CLOSE {
@Override
public Closeable generate() {
return new Closeable() {
@Override
public void close() throws IOException {
printLine("リソース破棄:異常");
throw new IOException("異常:リソース破棄");
}
};
}
};
@Override
public abstract Closeable generate() throws Throwable;
}
// ~~以下、テストケース~~
/**
* ビジネスロジック/リソース取得/リソース破棄が全て正常終了した
*/
@Test
public void test1() {
System.out.println("テスト1:全て正常");
try {
perform(LogicCases.NORMAL, ResourceCases.NORMAL);
} catch (Throwable e) {
e.printStackTrace();
}
}
// << コンソールへの出力内容 >>
//
// テストケースを開始します。
// テスト1:全て正常
// [1] try-with-resources:開始
// [2] リソース取得:正常
// [3] ビジネスロジック:開始
// [4] ビジネスロジック:正常
// [5] ビジネスロジック:終了
// [6] リソース破棄:正常
// [7] try-with-resources:終了
// テストケースを終了します。
/**
* ビジネスロジック=正常/リソース=null
*/
@Test
public void test2() {
System.out.println("テスト2:ビジネスロジック=正常/リソース=null");
try {
perform(LogicCases.NORMAL, ResourceCases.NULL_RESOURCE);
} catch (Throwable e) {
e.printStackTrace();
}
}
// << コンソールへの出力内容 >>
//
// テストケースを開始します。
// テスト2:ビジネスロジック=正常/リソース=null
// [1] try-with-resources:開始
// [2] リソース取得:null
// [3] ビジネスロジック:開始
// [4] ビジネスロジック:正常
// [5] ビジネスロジック:終了
// [6] try-with-resources:終了
// テストケースを終了します。
/**
* ビジネスロジック=異常/リソース=null
*/
@Test
public void test3() {
System.out.println("テスト3:ビジネスロジック=異常/リソース=null");
try {
perform(LogicCases.ERROR, ResourceCases.NULL_RESOURCE);
} catch (Throwable e) {
e.printStackTrace();
}
}
// << コンソールへの出力内容 >>
//
// テストケースを開始します。
// テスト3:ビジネスロジック=異常/リソース=null
// [1] try-with-resources:開始
// [2] リソース取得:null
// [3] ビジネスロジック:開始
// [4] ビジネスロジック:異常
// [5] (catch)異常:ビジネスロジック
// [6] try-with-resources:終了
// テストケースを終了します。
/**
* ビジネスロジック=異常/リソース取得&破棄=正常
*/
@Test
public void test4() {
System.out.println("テスト4:ビジネスロジック=異常/リソース取得&破棄=正常");
try {
perform(LogicCases.ERROR, ResourceCases.NORMAL);
} catch (Throwable e) {
e.printStackTrace();
}
}
// << コンソールへの出力内容 >>
//
// テストケースを開始します。
// テスト4:ビジネスロジック=異常/リソース取得&破棄=正常
// [1] try-with-resources:開始
// [2] リソース取得:正常
// [3] ビジネスロジック:開始
// [4] ビジネスロジック:異常
// [5] リソース破棄:正常
// [6] (catch)異常:ビジネスロジック
// [7] try-with-resources:終了
// テストケースを終了します。
/**
* ビジネスロジック=異常/リソース取得=正常/リソース破棄=異常
*/
@Test
public void test5() {
System.out.println("テスト5:ビジネスロジック=異常/リソース取得=正常/リソース破棄=異常");
try {
perform(LogicCases.ERROR, ResourceCases.ERROR_IN_CLOSE);
} catch (Throwable e) {
e.printStackTrace();
}
}
// << コンソールへの出力内容 >>
//
// テストケースを開始します。
// テスト5:ビジネスロジック=異常/リソース取得=正常/リソース破棄=異常
// [1] try-with-resources:開始
// [2] ビジネスロジック:開始
// [3] ビジネスロジック:異常
// [4] リソース破棄:異常
// [5] (catch)異常:ビジネスロジック
// [6] try-with-resources:終了
// テストケースを終了します。
/**
* ビジネスロジック=正常/リソース=異常発生
*/
@Test
public void test6() {
System.out.println("テスト6:ビジネスロジック=正常/リソース=異常発生");
try {
perform(LogicCases.NORMAL, ResourceCases.ERROR_IN_RESOURCE);
} catch (Throwable e) {
e.printStackTrace();
}
}
// << コンソールへの出力内容 >>
//
// テストケースを開始します。
// テスト6:ビジネスロジック=正常/リソース=異常発生
// [1] try-with-resources:開始
// [2] リソース取得:例外発生
// [3] (catch)異常:リソース取得
// [4] try-with-resources:終了
// テストケースを終了します。
// ~~ここからログ出力関連の処理~~
@Before
public void setup() {
_LINE_NUM = 1;
System.out.println("テストケースを開始します。");
}
@After
public void tearDown() {
System.out.println("テストケースを終了します。");
System.out.println();
}
private static int _LINE_NUM = 1;
public static void printLine(String message) {
System.out.println("[" + _LINE_NUM++ + "] " + message);
}
// ~~ここまでログ出力関連の処理~~
}
以上です。