[[JUnit 5]]を使ってテストを書いてみた。 ## プロジェクト作成 [[IntelliJ IDEA]]でプロジェクト作成する。[[Gradle]]を選択。 ## テスト対象コードを書く `src/main/java/org/example/Calculator.java` ```java package org.example; public class Calculator { public int sum(int a, int b) { return a + b; } } ``` ## テスト `test/java/org/example/Calculator.java` ```java package org.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class CalculatorTest { private final Calculator calculator = new Calculator(); @Test void add_正の数と正の数を加算できる() { assertEquals(3, calculator.sum(1, 2)); } @Test void add_正の数と負の数を加算できる() { assertEquals(-1, calculator.sum(1, -2)); } @Test void add_負の数と正の数を加算できる() { assertEquals(1, calculator.sum(-1, 2)); } @Test void add_負の数と負の数を加算できる() { assertEquals(-3, calculator.sum(-1, -2)); } @Test void add_0を加算できる() { assertEquals(0, calculator.sum(0, 0)); } } ``` ### 実行 OK。 ![[Pasted image 20230208214140.png]] > [!hint] 文字化けする場合 > [[📝IntelliJ IDEAでJavaのテストを実行するとテスト結果のViewが文字化けする]] を参照。 ## [[Parameterized Test]] 公式を参考に。 <div class="link-card"> <div class="link-card-header"> <img src="https://junit.org/junit5/assets/img/junit5-logo.png" class="link-card-site-icon"/> <span class="link-card-site-name">junit.org</span> </div> <div class="link-card-body"> <div class="link-card-content"> <div> <p class="link-card-title">JUnit 5 User Guide</p> </div> <div class="link-card-description"> </div> </div> </div> <a href="https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests"></a> </div> [[dependencies]]に[[junit-jupiter-params]]を追加する。 [[build.gradle]] ```diff dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' } ``` ### [[@CsvSource]]を使う場合 `test/java/org/example/Calculator.java` ```java package org.example; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.assertEquals; class CalculatorTest { private final Calculator calculator = new Calculator(); @ParameterizedTest @CsvSource({ "0, 0, 100000", "1, 2, 3", "-1, -2, -3", "1, -2, -1", "-1, 2, 1", "1, 0, 1", "0, 2, 2", "-1, 0, -1", "0, -2, -2", }) void add_parameterized_test(int a, int b, int expected) { assertEquals(expected, calculator.sum(a, b)); } } ``` ### [[@MethodSource]]を使う場合 `test/java/org/example/Calculator.java` ```java package org.example; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.params.provider.Arguments.arguments; class CalculatorTest { private final Calculator calculator = new Calculator(); @ParameterizedTest @MethodSource("addTestParameterProvider") void add_parameterized_test(int a, int b, int expected) { assertEquals(expected, calculator.sum(a, b)); } static Stream<Arguments> addTestParameterProvider() { return Stream.of( arguments(0, 0, 100000), arguments(1, 2, 3), arguments(-1, -2, -3), arguments(1, -2, -1), arguments(-1, 2, 1), arguments(1, 0, 1), arguments(0, 2, 2), arguments(-1, 0, -1), arguments(0, -2, -2) ); } } ``` ### 結果 ![[Pasted image 20230208221220.png]] ## [[モック]]や[[スタブ]]を使ったテスト [[mockito]]を使う。まずはインストールから。 <div class="link-card"> <div class="link-card-header"> <img src="https://site.mockito.org/favicon.ico" class="link-card-site-icon"/> <span class="link-card-site-name">site.mockito.org</span> </div> <div class="link-card-body"> <div class="link-card-content"> <div> <p class="link-card-title">Mockito framework site</p> </div> <div class="link-card-description"> A landing page for information about Mockito framework, a mocking framework for unit tests written i... </div> </div> </div> <a href="https://site.mockito.org/"></a> </div> `How do I drink it?`から。[[build.gradle]]に追加。 ```diff dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.1' + testImplementation 'org.mockito:mockito-core:3.+' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' } ``` テストコードで[[mockito]]の挙動を確かめてみる。 ### mockで[[スタブ]]を作る `mock`関数で[[スタブ]]を作成できる。 ```java package org.example; import org.junit.jupiter.api.Test; import java.util.ArrayList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class MainTest { private final Main main = new Main(); @Test void mockのテスト() { ArrayList<String> mockedList = mock(ArrayList.class); // mockList.get(0) が "mocked value" を返すようになる when(mockedList.get(0)).thenReturn("mocked value"); assertEquals("mocked value", mockedList.get(0)); // addは機能していない mockedList.add("real value 0"); assertEquals("mocked value", mockedList.get(0)); assertNull(mockedList.get(1)); // addは機能していない mockedList.add("real value 1"); assertEquals("mocked value", mockedList.get(0)); assertNull(mockedList.get(1)); } } ``` ### spyで[[スタブ]]を作る [[#mockで スタブ を作る]]と、`ArrayList#add`のように他のメソッドの効果も無効化されてしまう。`spy`関数を使って、部分的に実装をすり替えてみる。 <div class="link-card"> <div class="link-card-header"> <img src="https://javadoc.io/assets/images/fb2db6ea7688d54ae047109e0d71e3a0-favicon-32.png" class="link-card-site-icon"/> <span class="link-card-site-name">javadoc.io</span> </div> <div class="link-card-body"> <div class="link-card-content"> <div> <p class="link-card-title">Mockito - mockito-core 5.1.1 javadoc</p> </div> <div class="link-card-description"> </div> </div> </div> <a href="https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0"></a> </div> ```java @Test void spyのテスト() { List<String> list = new ArrayList<>(); List<String> mockedList = spy(list); // mockList.get(0) はリアルインスタンスの挙動なので IndexOutOfBoundsException になってしまう.. // > when(mockedList.get(0)).thenReturn("mocked value"); // 代わりにdoReturnを使う doReturn("mocked value").when(mockedList).get(0); // mockedList.get(0) はスタブ化したので、"mocked value"が返る assertEquals("mocked value", mockedList.get(0)); assertEquals(0, mockedList.size()); // spyではaddができない。。。 // mockedList.add("real value 0"); // mockedList.add("real value 1"); } ``` ただ、期待とは異なり、`add`を呼び出すことができなかった...。 ``` Cannot read the array length because "elementData" is null java.lang.NullPointerException: Cannot read the array length because "elementData" is null at java.base/java.util.ArrayList.add(ArrayList.java:453) ``` `ArrayList.class`の`this.elementData`が`null`になっている。 ```java boolean batchRemove(Collection<?> c, boolean complement, int from, int end) { Objects.requireNonNull(c); Object[] es = this.elementData; ``` `ArrayList.class`のコンストラクタで代入されていることから、コンストラクタが実行されていないことによる問題の可能性が高い。 ```java public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else { if (initialCapacity != 0) { throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } this.elementData = EMPTY_ELEMENTDATA; } } ``` `spy`は微妙かも...? ### mockでリアルメソッドを使う `mock`の問題はリアルメソッド、ここでは`ArrayList.add`が使えないことであり、それが使えれば`mock`で問題ない。`mock`関数は第2引数で設定を受け付けており、そこでリアルメソッドを使うように制御させる。 ```java ArrayList<String> mockedList = mock( ArrayList.class, withSettings() .useConstructor() .defaultAnswer(CALLS_REAL_METHODS) ); ``` これで、コンストラクタを通して、メソッドのデフォルト返却はリアルな実装を使うようになった。あとは、部分的に[[スタブ]]化すればよい。 ```java @Test void リアルメソッドを使ったmockのテスト() { ArrayList<String> mockedList = mock(ArrayList.class, withSettings().useConstructor().defaultAnswer(CALLS_REAL_METHODS)); // mockList.get(0) はリアルインスタンスの挙動なので IndexOutOfBoundsException になってしまう.. // > when(mockedList.get(0)).thenReturn("mocked value"); // 代わりにdoReturnを使う doReturn("mocked value").when(mockedList).get(0); // mockedList.get(0) はスタブ化したので、"mocked value"が返る assertEquals("mocked value", mockedList.get(0)); assertEquals(0, mockedList.size()); // リアルな値を入れる (spyとは異なりaddが動作する) mockedList.add("real value 0"); mockedList.add("real value 1"); // mockedList.get(0) はスタブ化したので、変わらず"mocked value"が返る assertEquals("mocked value", mockedList.get(0)); // mockedList.get(1) はスタブ化していないので、"real value 1"が返る assertEquals("real value 1", mockedList.get(1)); } ``` > [!caution] > この方法はレガシープログラムのために仕方なく使うことを想定されている。この方法は危険という意見もある(詳細は追っていない)。 > > 新しいプログラムに対してテストを書くのなら、極力リアルメソッドを使わず`mock`化することが推奨されている。 ## 参考 - [java \- Failed to mock ArrayList with option, CALLS\_REAL\_METHODS \- Stack Overflow](https://stackoverflow.com/questions/37329966/failed-to-mock-arraylist-with-option-calls-real-methods)