LoginSignup
62
65

More than 5 years have passed since last update.

JMockit はじめの一歩

Last updated at Posted at 2016-09-19

はじめに

 レガシーコードの 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テストの実装を効率的におこないましょう。

62
65
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
62
65