JUnit

JUnitのPowerMockを利用してstaticメソッドのMock化と呼出確認をしてみた

これまでは、Mockitoを利用してインスタンスメソッドをMock化する方法について記載していたが、今回はstaticメソッドのMock化と呼出確認をしてみたので、そのサンプルプログラムを共有する。

staticメソッドのMock化は、Mockitoでは実行できないので、PowerMockというライブラリを利用する。

前提条件

下記記事の実装が完了していること。

JunitのMockitoを利用してモック化したオブジェクトの引数と呼出回数を取得してみたJUnitのテストを行う際、Mock化したメソッドの戻り値がvoid型で、Mock化したメソッドが呼ばれたかどうかわからない場合がある。...

サンプルプログラムの内容

作成したサンプルプログラムの構成は以下の通り。
サンプルプログラムの構成
なお、上記の赤枠のうち、「build.gradle」が変更したプログラムで、他は今回新規で作成したプログラムとなる。

build.gradleの内容は以下の通り。テストクラスでlombokを利用できる設定・PowerMockが利用できるための設定を追加している。

plugins {
	id 'org.springframework.boot' version '2.1.7.RELEASE'
	id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	compileOnly 'org.projectlombok:lombok:1.18.10'
	annotationProcessor 'org.projectlombok:lombok:1.18.10'
	compile files('lib/ojdbc6.jar')
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.1'
	//lombokがテストクラスで利用できるようにするための設定
	testCompileOnly 'org.projectlombok:lombok:1.18.10'
	testAnnotationProcessor 'org.projectlombok:lombok:1.18.10'
	//PowerMockが利用できるための設定
	testCompile 'org.powermock:powermock-module-junit4:2.0.0-RC.4'
	testCompile 'org.powermock:powermock-api-mockito2:2.0.0-RC.4'
}
「HD Video Converter Factory Pro」は動画の形式変換や編集・録画等を行える便利ツールだった動画の形式変換や編集・録画等を行える便利ツールの一つに、「HD Video Converter Factory Pro」があります。ここ...

さらに、「DemoServiceImplTest3.java」の内容は以下の通り。

package com.example.demo;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;

import java.time.LocalDate;
import java.util.Map;

import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.validation.BindingResult;

import static org.junit.Assert.assertEquals;

//staticメソッドをMock化するにはPowerMockを利用
//@PrepareForTestアノテーションで、staticメソッドを含むクラスを指定
@RunWith(PowerMockRunner.class)
@PrepareForTest({DateCheckUtil.class})
public class DemoServiceImplTest3 {

    /**
     * テスト対象のクラス
     * (今回はSpring Bootを利用しないため、Serviceではなく
     * ServiceImplを対象クラスに指定している)
     */
    @InjectMocks
    private DemoServiceImpl demoServiceImpl;

    private DemoForm demoForm;

    private BindingResult bindingResult;

    /**
     * 前処理(各テストケースを実行する前に行われる処理)
     */
    @Before
    public void init() {
        //@Mockアノテーションのモックオブジェクトを初期化
        //これを実行しないと@Mockアノテーション、@InjectMocksを付与した
        //Mockオブジェクトが利用できない
        MockitoAnnotations.initMocks(this);

        //DateCheckUtilクラスをMock化
        PowerMockito.mockStatic(DateCheckUtil.class);

        //テスト対象メソッドの引数を設定
        demoForm = makeDemoForm(Long.valueOf(1), "テスト プリン"
                , LocalDate.of(2012, 1, 15), SexEnum.MAN);
        bindingResult = BindingResultModel.getBindingResult();
    }

    /**
     * DemoServiceImplクラスのcheckFormメソッド(DateCheckUtil.checkDateが正常時)の確認
     */
    @Test
    public void testCheckFormNormal() {
        //DateCheckUtilメソッドをMock化し、0が返却されるように設定
        PowerMockito.when(DateCheckUtil.checkDate(
                Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(0);

        //テスト対象メソッドの実行
        String returnVal = demoServiceImpl.checkForm(demoForm, bindingResult);

        //テスト対象メソッドを実行した結果、戻り値がconfirmであることを確認
        assertEquals("confirm", returnVal);

        //テスト対象メソッドを実行した結果、bindingResultに
        //エラーメッセージが設定されないことを確認
        Map<String, Object> mapObj = bindingResult.getModel();
        assertEquals(0, mapObj.size());
    }

    /**
     * DemoServiceImplクラスのcheckFormメソッド(DateCheckUtil.checkDateが異常時)の確認
     */
    @Test
    public void testCheckFormalDateError() {
        //DateCheckUtilメソッドをMock化し、4が返却されるように設定
        PowerMockito.when(DateCheckUtil.checkDate(
                Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(4);

        //テスト対象メソッドの実行
        String returnVal = demoServiceImpl.checkForm(demoForm, bindingResult);

        //テスト対象メソッドを実行した結果、戻り値がinputであることを確認
        assertEquals("input", returnVal);

        //テスト対象メソッドを実行した結果、resultに生年月日の日付が
        //不正な場合のエラーメッセージが設定されることを確認
        Map<String, Object> mapObj = bindingResult.getModel();
        BindingResultRejectValueModel resultModelYear
                = (BindingResultRejectValueModel) mapObj.get("birthYear");
        assertEquals("validation.date-invalidate", resultModelYear.getErrorCode());
        BindingResultRejectValueModel resultModelMonth
                = (BindingResultRejectValueModel) mapObj.get("birthMonth");
        assertEquals("validation.empty-msg", resultModelMonth.getErrorCode());
        BindingResultRejectValueModel resultModelDay
                = (BindingResultRejectValueModel) mapObj.get("birthDay");
        assertEquals("validation.empty-msg", resultModelDay.getErrorCode());
    }

    @Test
    public void testCheckFormDateCheckUtilArgs() {
        //DateCheckUtilメソッドをMock化し、0が返却されるように設定
        PowerMockito.when(DateCheckUtil.checkDate(
                Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(0);

        //テスト対象メソッドの実行
        String returnVal = demoServiceImpl.checkForm(demoForm, bindingResult);

        //DateCheckUtilが呼ばれたときの引数を取得するための設定
        ArgumentCaptor<String> strYearArg = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> strMonthArg = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> strDayArg = ArgumentCaptor.forClass(String.class);

        //DateCheckUtilが1回呼ばれたことを確認
        PowerMockito.verifyStatic(DateCheckUtil.class, Mockito.times(1));
        DateCheckUtil.checkDate(
                strYearArg.capture(), strMonthArg.capture(), strDayArg.capture());

        //DateCheckUtilの引数が2012年1月15日の年月日であることを確認
        assertEquals("2012", strYearArg.getValue());
        assertEquals("1", strMonthArg.getValue());
        assertEquals("15", strDayArg.getValue());

        //テスト対象メソッドを実行した結果、戻り値がconfirmであることを確認
        assertEquals("confirm", returnVal);

        //テスト対象メソッドを実行した結果、bindingResultに
        //エラーメッセージが設定されないことを確認
        Map<String, Object> mapObj = bindingResult.getModel();
        assertEquals(0, mapObj.size());
    }

    /**
     * Demoフォームオブジェクトを生成する
     *
     * @param id       ID
     * @param name     名前
     * @param birthDay 生年月日
     * @param sexEnum  性別Enum
     * @return Demoフォームオブジェクト
     */
    private DemoForm makeDemoForm(Long id, String name, LocalDate birthDay
                                , SexEnum sexEnum) {
        DemoForm demoForm = new DemoForm();
        if (id != null) {
            demoForm.setId(String.valueOf(id));
        }
        demoForm.setName(name);
        if (birthDay != null) {
            demoForm.setBirthYear(String.valueOf(birthDay.getYear()));
            demoForm.setBirthMonth(String.valueOf(birthDay.getMonthValue()));
            demoForm.setBirthDay(String.valueOf(birthDay.getDayOfMonth()));
        }
        if (sexEnum != null) {
            demoForm.setSex(sexEnum.getSex());
            demoForm.setSex_value(sexEnum.getSex_value());
        }
        return demoForm;
    }

}

PowerMockを利用できるために、クラスの先頭に「@RunWith(PowerMockRunner.class)」「@PrepareForTest({(staticメソッドをもつクラス).class})」というアノテーションを追加している。また、initメソッド内で「PowerMockito.mockStatic((staticメソッドをもつクラス).class)」を実行することで、指定したstaticメソッドがMock化している。

さらに、各テストメソッド内で「PowerMockito.when(staticメソッド(引数)).thenReturn(戻り値)」を実行することで、指定したstaticメソッドの戻り値を指定している。

また、「testCheckFormDateCheckUtilArgs」メソッド内で、staticメソッドの呼出確認とそのときの引数を取得している。テスト対象クラス実行後に、「PowerMockito.verifyStatic(staticメソッドをもつクラス.class, Mockito.times(呼出回数))」を実行し、その直後に呼出確認対象となるstaticメソッドを実行することで、staticメソッドの呼出回数確認が行える。さらに、Mockito利用時と同様に、そのメソッド呼出時の引数は、ArgumentCaptorクラスを利用することで取得できる。



その他、「BindingResultModel.java」「BindingResultRejectValueModel.java」の内容は以下の通り。

package com.example.demo;

import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;

import java.beans.PropertyEditor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BindingResultModel {

    public static BindingResult getBindingResult() {

        BindingResult result = new BindingResult() {
            Map<String, Object> modelMap = new HashMap<>();

            @Override
            public Object getTarget() {
                return null;
            }

            @Override
            public Map<String, Object> getModel() {
                return modelMap;
            }

            @Override
            public Object getRawFieldValue(String field) {
                return null;
            }

            @Override
            public PropertyEditor findEditor(String field, Class<?> valueType) {
                return null;
            }

            @Override
            public PropertyEditorRegistry getPropertyEditorRegistry() {
                return null;
            }

            @Override
            public String[] resolveMessageCodes(String errorCode) {
                return new String[0];
            }

            @Override
            public String[] resolveMessageCodes(String errorCode, String field) {
                return new String[0];
            }

            @Override
            public void addError(ObjectError error) {

            }

            @Override
            public String getObjectName() {
                return null;
            }

            @Override
            public void setNestedPath(String nestedPath) {

            }

            @Override
            public String getNestedPath() {
                return null;
            }

            @Override
            public void pushNestedPath(String subPath) {

            }

            @Override
            public void popNestedPath() throws IllegalStateException {

            }

            @Override
            public void reject(String errorCode) {

            }

            @Override
            public void reject(String errorCode, String defaultMessage) {

            }

            @Override
            public void reject(String errorCode, Object[] errorArgs
                             , String defaultMessage) {

            }

            @Override
            public void rejectValue(String field, String errorCode) {
               BindingResultRejectValueModel obj = new BindingResultRejectValueModel();
               obj.setErrorCode(errorCode);
               modelMap.put(field, obj);
            }

            @Override
            public void rejectValue(String field, String errorCode
                                  , String defaultMessage) {
               BindingResultRejectValueModel obj = new BindingResultRejectValueModel();
               obj.setErrorCode(errorCode);
               obj.setDefaultMessage(defaultMessage);
               modelMap.put(field, obj);
            }

            @Override
            public void rejectValue(String field, String errorCode
                                  , Object[] errorArgs, String defaultMessage) {
               BindingResultRejectValueModel obj = new BindingResultRejectValueModel();
               obj.setErrorCode(errorCode);
               obj.setErrorArgs(errorArgs);
               obj.setDefaultMessage(defaultMessage);
               modelMap.put(field, obj);
            }

            @Override
            public void addAllErrors(Errors errors) {

            }

            @Override
            public boolean hasErrors() {
                return false;
            }

            @Override
            public int getErrorCount() {
                return 0;
            }

            @Override
            public List<ObjectError> getAllErrors() {
                return null;
            }

            @Override
            public boolean hasGlobalErrors() {
                return false;
            }

            @Override
            public int getGlobalErrorCount() {
                return 0;
            }

            @Override
            public List<ObjectError> getGlobalErrors() {
                return null;
            }

            @Override
            public ObjectError getGlobalError() {
                return null;
            }

            @Override
            public boolean hasFieldErrors() {
                return false;
            }

            @Override
            public int getFieldErrorCount() {
                return 0;
            }

            @Override
            public List<FieldError> getFieldErrors() {
                return null;
            }

            @Override
            public FieldError getFieldError() {
                return null;
            }

            @Override
            public boolean hasFieldErrors(String field) {
                return false;
            }

            @Override
            public int getFieldErrorCount(String field) {
                return 0;
            }

            @Override
            public List<FieldError> getFieldErrors(String field) {
                return null;
            }

            @Override
            public FieldError getFieldError(String field) {
                return null;
            }

            @Override
            public Object getFieldValue(String field) {
                return null;
            }

            @Override
            public Class<?> getFieldType(String field) {
                return null;
            }
        };
        return result;
    }
}
package com.example.demo;

import lombok.Data;

@Data
public class BindingResultRejectValueModel {

    private String errorCode;

    private Object[] errorArgs;

    private String defaultMessage;
}

なお、「BindingResultModel.java」BindingResultオブジェクトの生成のために、「BindingResultRejectValueModel.java」はBindingResultオブジェクトの設定値を確認するために、それぞれ追加している。

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/junit-powermock-static/demo

「MiniTool Partition Wizard」はパーティション分割・統合・バックアップ・チェックを直感的に行える便利ツールだったハードディスクの記憶領域を論理的に分割し、分割された個々の領域のことを、パーティションといいます。 例えば、以下の図の場合、C/D...

今回作成した「DemoServiceImplTest3.java」の実行結果は以下の通り。ワーニングメッセージが出力されるが、テストメソッドが正常に実行できたことが確認できる。
JUnit実行結果

要点まとめ

  • PowerMockというライブラリを利用すると、staticメソッドをMock化することができる。
  • staticメソッドをMock化するには、クラスの先頭に「@RunWith(PowerMockRunner.class)」「@PrepareForTest({(staticメソッドをもつクラス).class})」というアノテーションを付与し、「PowerMockito.mockStatic((staticメソッドをもつクラス).class)」を実行すればよい。
  • 「PowerMockito.when(staticメソッド(引数)).thenReturn(戻り値)」を実行することで、指定したstaticメソッドの戻り値を指定することができる。
  • テスト対象クラス実行後に、「PowerMockito.verifyStatic(staticメソッドをもつクラス.class, Mockito.times(呼出回数))」を実行し、その直後に呼出確認対象となるstaticメソッドを実行することで、staticメソッドの呼出回数確認が行える。また、そのメソッド呼出時の引数は、ArgumentCaptorクラスを利用することで取得できる。