11

別のプロジェクトが作成したコードの事後検証の単体テストを作成しているときに、コントローラーにバインドされているバリデーターをinitBinder?でモックする方法に関するこの問題に遭遇しました。

通常、入力が有効であることを確認し、バリデーターでいくつかの追加の呼び出しを行うことを検討しますが、この場合、バリデーター クラスはいくつかのデータ ソースを介してチェックを実行することと結合されており、テストが非常に面倒になります。カップリングは、使用されているいくつかの古い共通ライブラリにさかのぼり、それらすべてを修正する現在の作業の範囲外です。

最初は、PowerMock を使用してバリデーターの外部依存関係をモックアウトし、静的メソッドをモックしようとしましたが、最終的には、クラスの作成時にデータ ソースを必要とするクラスに遭遇し、それを回避する方法が見つかりませんでした。

次に、通常のmockitoツールを使用してバリデーターをモックアウトしようとしましたが、それもうまくいきませんでした. 次に、呼び出しでバリデーターを設定しようとしましたが、それはバリデーターの注釈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)
4

2 に答える 2

10

newSpring アプリケーションでビジネス オブジェクトを作成することは避けてください。常にアプリケーション コンテキストから取得する必要があります。これにより、テストで簡単にモックできます。

ユースケースでは、バリデーターを 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" });
    }

    [...]
}

そうすれば、テストにモックを簡単に挿入できます。

于 2015-04-13T07:10:02.583 に答える
1

この状況に対処する唯一の方法は、アプリケーション コードを変更せずに PowerMock を使用することです。

JVM を計測し、静的メソッドだけでなく、newオペレーターを呼び出すときにもモックを作成できます。

次の例を見てください。

https://code.google.com/p/powermock/wiki/MockConstructor

Mockito を使用する場合は、PowerMock の代わりに PowerMockito を使用する必要があります。

https://code.google.com/p/powermock/wiki/MockitoUsage13

セクションを読む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 を計測せずにテストできるようにしてください。

于 2015-04-17T08:29:46.233 に答える