これまでは、Mockitoを利用してインスタンスメソッドをMock化する方法について記載していたが、今回はstaticメソッドのMock化と呼出確認をしてみたので、そのサンプルプログラムを共有する。
staticメソッドのMock化は、Mockitoでは実行できないので、PowerMockというライブラリを利用する。
前提条件
下記記事の実装が完了していること。
サンプルプログラムの内容
作成したサンプルプログラムの構成は以下の通り。
なお、上記の赤枠のうち、「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' }
さらに、「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
今回作成した「DemoServiceImplTest3.java」の実行結果は以下の通り。ワーニングメッセージが出力されるが、テストメソッドが正常に実行できたことが確認できる。
要点まとめ
- 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クラスを利用することで取得できる。