別のプロジェクトが作成したコードに事後ユニットテストを記述しているときに、initBinder
でコントローラーにバインドされているバリデーターをモックする方法のこの問題に遭遇しましたか?
通常、入力が有効であることを確認し、バリデーターでいくつかの追加の呼び出しを行うことを検討しますが、この場合、バリデータークラスは、いくつかのデータソースを介したチェックの実行と組み合わされ、すべてをテストするのが非常に面倒になります。カップリングは、使用されているいくつかの古い一般的なライブラリにまでさかのぼり、それらすべてを修正するための現在の作業の範囲外です。
最初は、PowerMockと静的メソッドのモックを使用してバリデーターの外部依存関係をモックアウトしようとしましたが、クラスの作成時にデータソースを必要とするクラスに遭遇し、その回避方法が見つかりませんでした。
次に、通常のモックツールを使用してバリデーターをモックアウトしようとしましたが、それも機能しませんでした。次に、mockMvc
呼び出しでバリデーターを設定しようとしましたが、それは@Mock
バリデーターの注釈。最後に この質問 に遭遇しました。しかし、コントローラー自体にフィールドvalidator
がないため、これも失敗します。では、どうすればこれを修正して機能させることができますか?
バリデーター:
public class TerminationValidator implements Validator {
// JSR-303 Bean Validator utility which converts ConstraintViolations to Spring's BindingResult
private CustomValidatorBean validator = new CustomValidatorBean();
private Class<? extends Default> level;
public TerminationValidator(Class<? extends Default> level) {
this.level = level;
validator.afterPropertiesSet();
}
public boolean supports(Class<?> clazz) {
return Termination.class.equals(clazz);
}
@Override
public void validate(Object model, Errors errors) {
BindingResult result = (BindingResult) errors;
// Check domain object against JSR-303 validation constraints
validator.validate(result.getTarget(), result, this.level);
[...]
}
[...]
}
コントローラー:
public class TerminationController extends AbstractController {
@InitBinder("termination")
public void initBinder(WebDataBinder binder, HttpServletRequest request) {
binder.setValidator(new TerminationValidator(Default.class));
binder.setAllowedFields(new String[] { "termId[**]", "terminationDate",
"accountSelection", "iban", "bic" });
}
[...]
}
テストクラス:
@RunWith(MockitoJUnitRunner.class)
public class StandaloneTerminationTests extends BaseControllerTest {
@Mock
private TerminationValidator terminationValidator = new TerminationValidator(Default.class);
@InjectMocks
private TerminationController controller;
private MockMvc mockMvc;
@Override
@Before
public void setUp() throws Exception {
initMocks(this);
mockMvc = standaloneSetup(controller)
.setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver())
.setValidator(terminationValidator)
.build();
ReflectionTestUtils.setField(controller, "validator", terminationValidator);
when(terminationValidator.supports(any(Class.class))).thenReturn(true);
doNothing().when(terminationValidator).validate(any(), any(Errors.class));
}
[...]
}
例外:
Java.lang.IllegalArgumentException: Could not find field [validator] of type [null] on target [my.application.web.controller.TerminationController@560508be]
at org.springframework.test.util.ReflectionTestUtils.setField(ReflectionTestUtils.Java:111)
at org.springframework.test.util.ReflectionTestUtils.setField(ReflectionTestUtils.Java:84)
at my.application.web.controller.termination.StandaloneTerminationTests.setUp(StandaloneTerminationTests.Java:70)
at Sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at Sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.Java:39)
at Sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.Java:25)
at Java.lang.reflect.Method.invoke(Method.Java:597)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.Java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.Java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.Java:44)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.Java:24)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.Java:271)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.Java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.Java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.Java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.Java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.Java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.Java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.Java:229)
at org.junit.runners.ParentRunner.run(ParentRunner.Java:309)
at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl.run(JUnit45AndHigherRunnerImpl.Java:37)
at org.mockito.runners.MockitoJUnitRunner.run(MockitoJUnitRunner.Java:62)
at org.Eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.Java:50)
at org.Eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.Java:38)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.Java:467)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.Java:683)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.Java:390)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.Java:197)
Springアプリケーションでnew
を使用してビジネスオブジェクトを作成することは避けてください。常にアプリケーションコンテキストから取得する必要があります。これにより、テストでのモックが容易になります。
ユースケースでは、バリデーターをBean(たとえば、defaultTerminationValidator
)として作成し、コントローラーに挿入するだけです。
public class TerminationController extends AbstractController {
private TerminationValidator terminationValidator;
@Autowired
public setDefaultTerminationValidator(TerminationValidator validator) {
this.terminationValidator = validator;
}
@InitBinder("termination")
public void initBinder(WebDataBinder binder, HttpServletRequest request) {
binder.setValidator(terminationValidator);
binder.setAllowedFields(new String[] { "termId[**]", "terminationDate",
"accountSelection", "iban", "bic" });
}
[...]
}
そうすれば、テストにモックを注入するだけで済みます。
さて、PowerMockを使用して、アプリケーションコードを変更せずに、この状況に対処するために私が知っている唯一の方法です。
JVMをインストルメント化でき、静的メソッドだけでなく、new
演算子を呼び出すときにもモックを作成します。
この例を見てください:
https://code.google.com/p/powermock/wiki/MockConstructor
Mockitoを使用する場合は、PowerMockの代わりにPowerMockitoを使用する必要があります。
https://code.google.com/p/powermock/wiki/MockitoUsage1
セクションを読むHow to mock construction of new objects
例えば:
私のカスタムコントローラー
public class MyController {
public String doSomeStuff(String parameter) {
getValidator().validate(parameter);
// Perform other operations
return "nextView";
}
public CoolValidator getValidator() {
//Bad design, it's better to inject the validator or a factory that provides it
return new CoolValidator();
}
}
私のカスタムバリデーター
public class CoolValidator {
public void validate(String input) throws InvalidParameterException {
//Do some validation. This code will be mocked by PowerMock!!
}
}
PowerMockitoを使用したカスタムテスト
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.powermock.api.mockito.PowerMockito.*;
@RunWith(PowerMockRunner.class)
@PrepareForTest(MyController.class)
public class MyControllerTest {
@Test(expected=InvalidParameterException.class)
public void test() throws Exception {
whenNew(CoolValidator.class).withAnyArguments()
.thenThrow(new InvalidParameterException("error message"));
MyController controller = new MyController();
controller.doSomeStuff("test"); // this method does a "new CoolValidator()" inside
}
}
Mavenの依存関係
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
私のテストでわかるように、私はバリデーターの動作をモックしているので、コントローラーがそれを呼び出すと例外がスローされます。
ただし、PowerMockの使用は通常、設計が悪いことを示します。通常、レガシーアプリケーションをテストする必要がある場合に使用する必要があります。
アプリケーションを変更できる場合は、JVMをインストルメント化せずにテストできるように、コードを変更することをお勧めします。