Edited at

JMockit はじめの一歩

More than 3 years have passed since last update.


はじめに

 レガシーコードの static, final, private, new をモックにしたいとき、次のような選択肢が考えられます。

(A) Mockito + 自作のラッパークラス or リファクタリング

(B) Mockito + PowerMockito

(C) JMockit

(A)~(C)のどれにするのかは、テスト対象との兼ね合いになるかと思います。

以前、『Java テストツールのトレンド 2014/1~2016/5』という記事を書きましたが、本記事では、この中で一番情報が少ないJMockitでモックを作る方法を紹介します。


セットアップ

EclipseでJMockitを使えるようにセットアップしてみます。



  1. ダウンロード

    今回はjmockit-1.27.zipをダウンロードしました。

    http://jmockit.org/index.html

     



  2. クラスパス

    jmockit.jarをEclipseの「JUnit 4」ライブラリーよりも前に設定します。


    JMockit_JUnit.png

    クラスパスの指定順を逆にするとIllegalStateExceptionが発生します。

    java.lang.IllegalStateException: JMockit wasn't properly initialized; please ensure that jmockit precedes junit in the runtime classpath, or use @RunWith(JMockit.class)
    
    at sample.FooTest$1.<init>(FooTest.java:19)
    at sample.FooTest.testGetString(FooTest.java:19)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

    設定方法は、クラスパスの指定順ではなく、JMockitを使っているテストクラスに@RunWith(JMockit.class)のアノテーションをつける方法もあります。


     




  3. サンプル


    サンプルコードで動作確認してみます。finalクラスFooの"Foo"の文字列を応答するfinalメソッドgetString()に"Mock"と応答させてみます。


    プロダクションコード


    Foo.java

    package sample;

    final class Foo {
    public final String getString() {
    return "Foo";
    }
    }


    テストコード


    FooTest_1.java

    package sample;

    import static org.hamcrest.CoreMatchers.*;
    import static org.junit.Assert.*;

    import org.junit.Test;

    import mockit.Expectations;
    import mockit.Mocked;

    //@RunWith(JMockit.class)
    public class FooTest_1 {

    @Mocked
    private Foo foo;

    @Test
    public void testGetString() {
    // Record
    new Expectations() {{
    foo.getString();
    result = "Mock";
    }};

    // Replay
    String actual = foo.getString();

    // Verify
    assertThat(actual, is("Mock"));
    }

    }


    テストクラスでは、@MockedアノテーションでFooのインスタンスをモックにし、テストメソッド内でモックの設定をします(new Expectations(){...})。JMockitの場合、finalクラスやfinalメソッドについて意識してなにかする必要はありません。

    これでJUnitの実行が成功すれば、JMockitのセットアップは完了です。

     




@Mocked



  1. @Mockedアノテーション

     @Mockedアノテーションを指定して生成したインスタンスは、モックのメソッドやコンストラクタの実装は空になり、利用するメソッドに対してnew Expectations() {...}で動作を設定します。

     


  2. テストメソッド毎にモックを生成する

     セットアップで紹介した方法のように、テストクラスのフィールドに対して@Mockedを定義すると、全てのテストメソッドが実行されるたびに@Mockedを指定したフィールドにモックのインスタンスが生成されます。1つのテストクラスに複数のテストメソッドがあり、モックを使うのがその一部のテストメソッドの場合は、モックを使わないテストに対してモックのインスタンスを生成するのは効率がよくありません。テストメソッド毎にモックを生成する場合は、メソッドに対してモックを指定します。テスト結果は同じになります。


    FooTest_2.java

    package sample;

    import static org.hamcrest.CoreMatchers.*;
    import static org.junit.Assert.*;

    import org.junit.Test;

    import mockit.Expectations;
    import mockit.Mocked;

    public class FooTest_2 {

    @Test
    public void testGetString(@Mocked final Foo foo) {
    // Record
    new Expectations() {{
    foo.getString();
    result = "Mock";
    }};

    // Replay
    String actual = foo.getString();

    // Verify
    assertThat(actual, is("Mock"));
    }

    }





  3. 同じクラスのモックを複数作る

    同じクラスのモックを複数作りそれぞれ異なる動作を設定したい場合は、次のようにします。テストメソッドには複数の@Mockedパラメータを指定することができ、それぞれのモックインスタンスに対して異なる動作を設定することができます。


    FooTest_3.java

    package sample;

    import static org.hamcrest.CoreMatchers.*;
    import static org.junit.Assert.*;

    import org.junit.Test;

    import mockit.Expectations;
    import mockit.Mocked;

    public class FooTest_3 {

    @Test
    public void testGetString(@Mocked final Foo foo1, @Mocked final Foo foo2) {
    // Record
    new Expectations() {{
    foo1.getString(); result = "Mock1";
    foo2.getString(); result = "Mock2";
    }};

    // Replay
    String actual1 = foo1.getString();
    String actual2 = foo2.getString();

    // Verify
    assertThat(actual1, is("Mock1"));
    assertThat(actual2, is("Mock2"));
    }

    }





@Injectable



  1. @Injectableについて

     @Injectableを指定すると、指定したクラスのコンストラクタが呼ばれフィールドに挿入されます。

     @Injectableを指定して生成したインスタンスは基本的には実体です。動作を変更したいメソッドやコンストラクタに対してnew Expectations() {...}を使って設定してあげます。

     @InjectableとExpectations()の組み合わせは、MockitoのSpyと似た動作になります。

     


  2. @Mocked@Injectableの違い

     @Mockedを指定すると、そのクラスのコンストラクタやメソッドは中身が空のモックになってしまいます。一方、@Injectableを指定すると、コンストラクタやメソッドは実体がともなったものになります。

     


  3. サンプルプログラム

     以下、@Mockedを指定すると中身が空のモックが生成され、@Injectedを指定すると実体を伴ったモックが生成される様子をコードで確かめてみます。

    プロダクションコード


    Man.java

    package sample;

    class Man {
    String name;
    int age;

    Man() {
    name = "Taro";
    age = 23;
    }

    Man(String p_name, int p_age) {
    name = p_name;
    age = p_age;
    }

    String getName() {
    return name;
    }

    int getAge() {
    return age;
    }
    }


    テストコード


    ManTest.java

    package sample;

    import static org.hamcrest.CoreMatchers.*;
    import static org.junit.Assert.*;

    import org.junit.Test;

    import mockit.Expectations;
    import mockit.Injectable;
    import mockit.Mocked;

    public class ManTest {

    /**
    * 引数なしのリアルコンストラクタ
    */

    @Test
    public void testMan_Real() {
    // Arrange

    // Act
    Man man = new Man();

    // Assert
    assertThat(man.getName(), is("Taro"));
    assertThat(man.getAge(), is(23));
    }

    /**
    * 検証1 @Mockedでモック化
    * ※ コンストラクタはモック化される
    */

    @Test
    public void testMan_Mocked_NoSet1(@Mocked final Man anyMan) {
    // Record

    // Replay
    Man actMan = new Man();

    // Verify
    assertThat(actMan.name, nullValue());
    assertThat(actMan.age, is(0));
    }

    /**
    * 検証2 @Mockedでモック化
    * ※ メソッドはモック化される
    */

    @Test
    public void testMan_Mocked_NoSet2(@Mocked final Man anyMan) {
    // Record

    // Replay
    Man actMan = new Man();
    actMan.name = "Jiro";
    actMan.age = 18;

    // Verify
    assertThat(actMan.getName(), nullValue());
    assertThat(actMan.getAge(), is(0));
    }

    /**
    * コンストラクタを@Mockedでモック化
    * ※ 各メソッドの動作を設定する必要がある
    */

    @Test
    public void testMan_Mocked_Set(@Mocked final Man anyMan) {
    // Record
    new Expectations() {{
    Man mock = new Man();
    mock.getName(); result = "Jiro";
    mock.getAge(); result = 18;
    }};

    // Replay
    Man actMan = new Man();

    // Verify
    assertThat(actMan.getName(), is("Jiro"));
    assertThat(actMan.getAge(), is(18));
    }

    /**
    * コンストラクタを@Injectableで部分モック化
    * ※ コンストラクタは実体が呼び出される
    */

    @Test
    public void testMan_Injected(@Injectable final Man anyMan) {
    // Record

    // Replay
    Man actMan = new Man();

    // Verify
    assertThat(actMan.getName(), is("Taro"));
    assertThat(actMan.getAge(), is(23));
    }

    /**
    * 引数ありのコンストラクタを部分モック化する
    * ※ Expectationsで動作を設定した部分だけがモック化される
    */

    @Test
    public void testManStringInt(@Injectable final Man spyMan) {
    // Record
    new Expectations() {{
    spyMan.getAge(); returns(15, 10);
    }};

    // Replay
    Man actMan = new Man("Jiro", 18);

    // Verify
    assertThat(actMan.getName(), is("Jiro"));
    assertThat(actMan.getAge(), is(18));
    assertThat(spyMan.getAge(), is(15));
    assertThat(spyMan.getAge(), is(10));
    }

    }





@Mock



  1. interfaceの一部のメソッドだけ実体化

    多数のメソッドをもつinterfaceの一部のメソッドだけ実体化したインスタンスを利用したい場合は、new MockUp<T>(){...}が便利です。MockUpの内部では、実体化したいメソッドに対して@Mockアノテーションを指定します。

プロダクションコード


Boo.java

package sample;

interface Boo {
/**
* テスト対象のメソッド
*
* @return privateメソッドが応答した文字列を応答する
*/

String methodToTest();

/**
* テスト対象外のメソッド1
*/

String mothodToNotTest1();

/**
* テスト対象外のメソッド2
*/

String mothodToNotTest2();

/**
* テスト対象外のメソッド3
*/

String mothodToNotTest3();
}


テストコード


BooTest.java

package sample;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.Test;

import mockit.Mock;
import mockit.MockUp;

public class BooTest {

@Test
public void testMethodToTest_MockUp() {
// Record
Boo boo = new MockUp<Boo>() {
@Mock public String methodToTest() {
return "Hoge";
}
}.getMockInstance();

// Replay
String act = boo.methodToTest();

// Verify
assertThat(act, is("Hoge"));
}

}



@Capturing

基本編

 実装クラスを特定しないで基底クラスのモックを作りたい場合は@Capturingアノテーションを使います。異なる実装クラスの同一名のメソッドに対して、まとめてモックの動作を設定することができます。

クラス図

 Business.png

プロダクションコード


Service.java

package sample;

interface Service {
int doSomething();
}



ServiceImpl.java

package sample;

class ServiceImpl implements sample.Service {
@Override
public int doSomething() {
return 1;
}
}



Business.java

package sample;

class Business {
private final Service service1 = new ServiceImpl();
private final Service service2 = new Service() {
public int doSomething() {
return 2;
}
};

int businessOperation() {
return service1.doSomething() + service2.doSomething();
}
}


テストコード


BusinessTest.java

package sample;

import static org.junit.Assert.*;
import org.junit.Test;
import mockit.Capturing;
import mockit.Expectations;

public class BusinessTest {
@Capturing Service anyService;

@Test
public void mockingImplementationClassesFromAGivenBaseType() {
new Expectations() {{
anyService.doSomething();
returns(3, 4);
}};

int result = new Business().businessOperation();

assertEquals(7, result);
}
}


テストを実行すると、ServiceImplのdoSomething()が3を応答し、ServiceのdoSomething()が4を応答し、businessOperation()の応答が7になります。

応用編

 @CapturingにmaxInstancesを指定することで、これから作成されるインスタンスに対する動作をmaxInstances個目を境に変更することができます。

 java.nio.Bufferを使ったサンプルです。


DifferentBehaviorTest.java

package sample;

import static org.junit.Assert.*;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.IntBuffer;

import org.junit.Test;

import mockit.Capturing;
import mockit.Expectations;

public class DifferentBehaviorTest {
@Test
public void differentBehaviorTest(
@Capturing(maxInstances = 1) final Buffer firstNewBuffer,
@Capturing final Buffer remainingNewBuffers) {

// Record
new Expectations() {{
firstNewBuffer.position(); result = 10;
remainingNewBuffers.position(); result = 20;
}};

// Replay
ByteBuffer buffer1 = ByteBuffer.allocate(100);
IntBuffer buffer2 = IntBuffer.wrap(new int[] { 1, 2, 3 });
CharBuffer buffer3 = CharBuffer.wrap(" ");

// Verify
assertEquals(10, buffer1.position());
assertEquals(20, buffer2.position());
assertEquals(20, buffer3.position());
}
}


 maxInstances = 1を指定したインスタンスfirstNewBufferの応答は「10」。それ以降のインスタンスは「20」を応答するようにExpectationsで指定しています。

 よって、最初のインスタンス、ByteBuffer buffer1 = ByteBuffer.allocate(100);position()は「10」を応答し、2個目以降のインスタンス、IntBuffer buffer2 = IntBuffer.wrap(new int[] { 1, 2, 3 });CharBuffer buffer3 = CharBuffer.wrap(" ");position()は「20」を応答します。

備考

 @Mockedアノテーションの指定が同一スコープにあると、@Mockedアノテーションの指定が優先され、すべての実装クラスはモック化されます。maxInstancesの指定は考慮されません。


@Tested

 @Testedアノテーションと@Injectableアノテーションを組み合わせて使うと、テスト開始時に@Testedアノテーションで指定したインスタンスが生成され、そのインスタンスに@Injectableアノテーションで指定したモックが差し込まれます。


TestedSample.java

public class SomeTest

{
@Tested CodeUnderTest tested;
@Injectable Dependency dep1;
@Injectable AnotherDependency dep2;
@Injectable int someIntegralProperty = 123;

@Test
public void someTestMethod(@Injectable("true") boolean flag, @Injectable("Mary") String name)
{
// Record expectations on mocked types, if needed.

tested.exerciseCodeUnderTest();

// Verify expectations on mocked types, if required.
}
}


この例では、テスト対象クラスCodeUnderTestの各フィールドに対して、そのフィールドの型に応じて以下のインスタンスが差し込まれます。

ただし、CodeUnderTestのインスタンスを作成する過程でフィールドになんらかのオブジェクトがセットされた場合は、モックは差し込まれません。


モック
備考

Dependency
new Dependency()
nullではない場合のみnewしてインジェクション

AnotherDependency
new AnotherDependency()
nullではない場合のみnewしてインジェクション

int
123

boolean
true

String
"Mary"
nullではない場合のみ"Mary"をインジェクション

ListやCollectionに対してもインジェクションが可能です。

TargetClassのnamesやcollaboratorsにインジェクションしてみます。


DITest.java

package sample;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import org.junit.Test;

import mockit.Injectable;
import mockit.Tested;

public final class DITest {

static class Collaborator {
final int value;

Collaborator() {
value = 0;
}

Collaborator(int value) {
this.value = value;
}
}

static final class TargetClass {
final List<String> names;
Collection<Collaborator> collaborators;

TargetClass(List<String> names) {
this.names = names;
}
}

@Tested
TargetClass target;
@Injectable
final List<String> nameList = Arrays.asList("One", "Two");
@Injectable
final Collection<Collaborator> collaboratorList = Arrays.asList(new Collaborator(1), new Collaborator(2));

@Test
public void testMethod() {
// Record

// Replay

// Verify
assertThat(nameList, is(target.names));
assertThat(collaboratorList, is(target.collaborators));
}

}



Deencapsulation

Deencapsulationクラスは、アクセス権のないフィールド、メソッド、コンストラクタにアクセスするためのユーティリティクラスです。



  • static <T> T getField(Class<?> classWithStaticField, Class<T> fieldType)

    クラスの型とフィールドの型を指定して、アクセス権のないstaticフィールドの値を取得します。

    引数に指定したフィールドの型は、引数に指定したクラスに1つしかない想定です。


  • static <T> T getField(Class<?> classWithStaticField, String fieldName)

    クラスの型とフィールド名を指定して、アクセス権のないstaticフィールドの値を取得します。


  • static <T> T getField(Object objectWithField, Class<T> fieldType)

    オブジェクトとフィールドの型を指定して、アクセス権のないフィールドの値を取得します。

    引数に指定したフィールドの型は、引数に指定したオブジェクトの型に1つしかない想定です。


  • static <T> T getField(Object objectWithField, String fieldName)

    オブジェクトとフィールド名を指定して、アクセス権のないフィールドの値を取得します。



  • static <T> T invoke(Class<?> classWithStaticMethod, String methodName, Class<?>[] parameterTypes, Object... methodArgs)

    アクセス権のないstaticメソッドを呼び出します。メソッド引数の型を指定します。


  • static <T> T invoke(Class<?> classWithStaticMethod, String methodName, Object... nonNullArgs)

    アクセス権のないstaticメソッドを呼び出します。


  • static <T> T invoke(Object objectWithMethod, String methodName, Class<?>[] parameterTypes, Object... methodArgs)

    アクセス権のないインスタンスメソッドを呼び出します。メソッド引数の型を指定します。


  • static <T> T invoke(Object objectWithMethod, String methodName, Object... nonNullArgs)

    アクセス権のないインスタンスメソッドを呼び出します。


  • static <T> T newInnerInstance(Class<? extends T> innerClassToInstantiate, Object outerClassInstance, Object... nonNullArgs)

    インナークラスのインスタンスを生成します。インナークラスの型はクラスで指定します。


  • static <T> T newInnerInstance(String innerClassSimpleName, Object outerClassInstance, Object... nonNullArgs)

    インナークラスのインスタンスを生成します。インナークラスの型は文字列で指定します。


  • static <T> T newInstance(Class<? extends T> classToInstantiate, Class<?>[] parameterTypes, Object... initArgs)

    クラスを指定し、コンストラクタを呼んでクラスのインスタンスを生成します。コンストラクタの引数の型を指定します。


  • static <T> T newInstance(Class<? extends T> classToInstantiate, Object... nonNullArgs)

    クラスを指定し、コンストラクタを呼んでクラスのインスタンスを生成します。コンストラクタの引数の型はnonNullArgsから推定します。


  • static <T> T newInstance(String className, Class<?>[] parameterTypes, Object... initArgs)

    アクセス権のないクラスのコンストラクタを呼んでインスタンスを生成します。クラス名を文字列で指定します。コンストラクタの引数の型を指定します。


  • static <T> T newInstance(String className, Object... nonNullArgs)

    アクセス権のないクラスのコンストラクタを呼んでインスタンスを生成します。クラス名を文字列で指定します。コンストラクタの引数の型はnonNullArgsから推定します。


  • public static T newUninitializedInstance(Class<? extends T> classToInstantiate)
    コンストラクタを呼ばずにインスタンスを生成します。abstractクラスやinterfaceの場合、abstractメソッドは空のインスタンスを生成します。


  • public static void setField(Class<?> classWithStaticField, Object fieldValue)

    アクセス権のないstaticフィールドに値をセットします。fieldValueの型でセットするフィールドを探します。


  • public static void setField(Class<?> classWithStaticField, String fieldName, Object fieldValue)

    アクセス権のないstaticフィールドに値をセットします。


  • public static void setField(Object objectWithField, Object fieldValue)

    アクセス権のないフィールドに値をセットします。fieldValueの型でセットするフィールドを探します。


  • public static void setField(Object objectWithField, String fieldName, Object fieldValue)

    アクセス権のないフィールドに値をセットします。

staticフィールドにsetFieldで値をセットできないケースが存在します。

例えば、以下です。


  • private static final int

  • private static final String

  • public static final int

  • public static final String

これは、これらのstaticフィールドは、コンパイル時にバイトファイルに値が展開されてしまい、JMockitがクラスローダー経由でバイトコードをロードした時点で、staticフィールドがなくなってしまっているためです。


any引数



  1. 任意の引数にマッチさせる

     テストコードのnew Expectations() {{...}}; 内でモックメソッドを定義するさいに、任意の引数にマッチさせたい場合は、mockit.InvocationsクラスのanyXXXを指定します。

    anyXXX
    Type

    anyBoolean
    Boolean

    anyByte
    Byte

    anyChar
    Char

    anyDouble
    Double

    anyFloat
    Float

    anyInt
    Int

    anyShort
    Short

    anyString
    String

    any
    Object

    サンプル


    HogeTest.java

    // Record
    
    new Expectations() {{
    mock.getName(anyString); result = "Hoge"
    }};

    getName()の引数に"A"を指定しても、"B"を指定しても、getName()は"Hoge"を応答します。




staticメソッド


  1. staticメソッドのモック化

     @Mockedアノテーションでクラス名を指定し、そのクラスのメソッドに対して、Expectations()内でクラス名.メソッド名(); result = XXX;をセットすると、staticメソッドをモック化することができます。

     


  2. サンプルプログラム

    プロダクションコード



Bar.java

package sample;

class Bar {
/**
* staticメソッド
*/

static String getName(String p_name) {
return null;
}
}


テストコード


BarTest.java

package sample;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.Test;

import mockit.Expectations;
import mockit.Mocked;

public class BarTest {

/**
* staticメソッドをモック化する
*/

@Test
public void testGetName(@Mocked final Bar mock) {
// Record
new Expectations() {{
Bar.getName("Taro"); result = "Taro";
Bar.getName("Jiro"); result = "Jiro";
Bar.getName(anyString); result = "None";
}};

// Replay
String act1 = Bar.getName("Taro");
String act2 = Bar.getName("Jiro");
String act3 = Bar.getName("Hanako");

// Verify
assertThat(act1, is("Taro"));
assertThat(act2, is("Jiro"));
assertThat(act3, is("None"));
}

}



privateメソッド

privateメソッドのモック化がうまくいかない

 最近のJMockit(version 1.27で検証)はprivateメソッドをモック化する方法が不明です。

 例えば、StackOverflow(リンク)の回答を実行してみます。


TestAClass.java

import org.junit.Test;

import static mockit.Deencapsulation.*;
import mockit.*;

public class TestAClass
{
public static class ClassToTest
{
public void methodToTest()
{
boolean returnValue = methodToMock(0);
System.out.println("methodToMock returned " + returnValue);
}

private boolean methodToMock(int value) { return true; }
}

@Tested ClassToTest classToTestInstance;

@Test
public void partiallyMockTestedClass() {
new Expectations(classToTestInstance) {{
invoke(classToTestInstance, "methodToMock", anyInt);
result = false;
times = 2;
}};

classToTestInstance.methodToTest();
classToTestInstance.methodToTest();
classToTestInstance.methodToTest();
}
}


結果は、IllegalArgumentException。

java.lang.IllegalArgumentException: Invalid invocation from expectation block

at sample.TestAClass$1.<init>(TestAClass.java:28)
at sample.TestAClass.partiallyMockTestedClass(TestAClass.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

次章『廃止された機能』にも記載しましたが、Expectationsクラス内でprivateメソッドをモック化する機能は廃止されました。


まあ、この回答は、実行できたとしても、以下の部分の調整がもう少し必要です。


  • プロダクションコードとテストコードが一体になっている

  • staticクラスのメソッドをモック化している

privateメソッドを呼び出すラッパーメソッド

モックとは関係ありませんが、関連項目としてJMockitにはprivateメソッドを呼び出すラッパーメソッドが用意されています。

このラッパーメソッドを使ってprivateメソッドのテストは下記のように書くことができます。

プロダクションコード


Baz.java

package sample;

class Baz {
private String methodToMock(String p_str) {
return "private";
}
}


テストコード


BazTest.java

package sample;

import static mockit.Deencapsulation.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.Test;

public class BazTest {

@Test
public void testMethodToTest_invoke_private_method() {
// Record
Baz baz = new Baz();

// Replay
String act = invoke(baz, "methodToMock", new String());

// Verify
assertThat(act, is("private"));
}

}


こちらは問題なく動作し、直接リフレクションを使うよりも、少しだけ簡単に書くことができます。


廃止された機能



  1. privateメソッド、privateコンストラクタ

     Expectationsクラス内でprivateメソッド、privateコンストラクタをモック化する機能がVersion 1.23で廃止されています。MockUp<T>クラスを使うことで代用することができます。Expectationsクラス内でprivateメソッドやprivateコンストラクタをinvoke()しているサンプルは情報が古く、最新環境では動作しません。

     



  2. NonStrictExpectationsクラス

     NonStrictExpectationsクラスは、Version 1.23で非推奨になり、Version 1.25で廃止されています。NonStrictExpectationsクラスを使っている場合、Expectationsクラスを使うか、MockUp<T>クラスを使うことで代用することが可能です。


JMockitを実行する


実行環境


  • JDK 1.6+


    • JREでは動作しません。JDKを使う必要があります。

    • Eclipseのデフォルト設定は、「JREシステム・ライブラリー」です。JDKに変更する必要があります。



  • JUnit 4.5+

  • JMockitのテストランナーをJUnitのテストランナーの前に指定します。


    1. classpathでjmockit.jarをjunit.jarよりも前に指定します。Eclipseでは、「Javaのビルドパス」 → 「順序およびエクスポート(O)」タブで設定します。

    2. テストクラスに@RunWith(JMockit.class)を指定します。



  • JUnit 5+とTestNG 6.2+では、classpathのどこにjmockit.jarを指定しても大丈夫です。

  • 以下の場合は、JVMの初期化パラメータに -javaagent:/<proper path>/jmockit.jar を指定します。


    • Oracle以外のJDK実装を使うとき

    • Eclipse/IntelliJ IDEAで実行/デバッグするとき

    • Ant/Mavenのスクリプトを実行するとき




Ant

build.xml内で<junit>タグを使う場合は、JVMインスタンスを分ける必要があります。


build.xml

<junit fork="yes" forkmode="once" dir="directoryContainingJars">

<classpath path="jmockit.jar"/>

<!-- Additional classpath entries, including the appropriate junit.jar -->

<batchtest>
<!-- filesets specifying the desired test classes -->
</batchtest>
</junit>



Maven

JUnitの<dependency>タグの後にJMockitの<dependency>タグを指定します。


pom.xml

<dependencies>

<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.x</version>
<scope>test</scope>
</dependency>
</dependencies>


おわりに

 本記事は「JMockit」の記事ですが、モックを多用したテストコードは、テストコード自体にバグが発生しますし、他人がJMockitを駆使して作成したテストはメンテナンスに骨が折れます。

 モックツールを使う前に、設計の段階で、テストコードのメンテナンス性についても考慮して設計するほうが先決でしょう。

 また、新規開発ならMockitoでテストできるように設計していくのがおすすめです。

 Mockito単体ではできないとされていることがありますが、これらはJavaの本来の機能で対応可能です。



  • private

     リフレクションを使って可視性を変更する方法がよく知られています。


  • final

     クラスローダーでクラスをロードする際にfinalを取り除く方法がよく知られています。(リンク


  • 引数つきのコンストラクタ

     プロダクションコードを「引数なしコンストラクタ+インスタンス初期化メソッド」にリファクタリングする方法がよく知られています。

 Mockitのほうが『JUnit実践入門』など、Mockitoを取り上げた本や、Webの解説記事も多く学習しやすいです。

 Pleiades Eclipseを利用されている方は、以下の手順でMockitoを目にする機会があると思います。

   Mockito-Eclipse.png

           Eclipseのライブラリーの追加画面


Pleiades Eclipse Neon(version 4.6.0)でMockitoのライブラリを追加する手順:

  パッケージ・エクスプローラ → プロジェクトを右クリック → ビルド・パス(B) → ライブラリーの追加(L)... → Mockito → 次へ(N)> → 完了(F)

 また、Mockito + PowerMockito では問題なく動作する、privateメソッドのモックを動作させる方法がJMockitでは不明です。

 とはいえ、JMockitを利用する必要がでてきた方は、JMockitの機能をうまく使ってUnitテストの実装を効率的におこないましょう。