How do I inject mocks into a Spring class marked a

2019-08-10 21:28发布

问题:

I'm using SPring 3.1.1.RELEASE and JUnit 4.8.1. In my test class, I want to mock a private field and discovered the beauty of "ReflectionTestUtils" ...

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:test-context.xml" })
public class OrderServiceTest extends AbstractTransactionalJUnit4SpringContextTests
    …
        @Autowired
    private OrderService m_orderSvc;

    @Test
    public void testGetPDOrders() throws QuickBaseException, Exception {
        …
            ReflectionTestUtils.setField(m_orderSvc, "m_orderDao", mockOrderDao);

Below is the class and field I'm trying to mock ...

@Service("orderService")
@Transactional
public class OrderServiceImpl implements OrderService {

    …
    @Autowired
    private OrderDAO m_orderDao;

Disappointingly, I get the below error. I've read that this is because my class is marked as "@Transactional" but I can't find an adequate work-around and it seems like a waste to write setter methods solely to accommodate JUnit. Does anyone have another suggestion about how I can inject my mock object into a private field?

java.lang.IllegalArgumentException: Could not find field [m_orderDao] on target [org.mainco.subco.myclient.service.OrderServiceImpl@282f0e07]
    at org.springframework.util.Assert.notNull(Assert.java:112)
    at org.springframework.test.util.ReflectionTestUtils.setField(ReflectionTestUtils.java:107)
    at org.springframework.test.util.ReflectionTestUtils.setField(ReflectionTestUtils.java:84)
    at org.mainco.subco.myclient.service.OrderServiceTest.testGetPDOrders(OrderServiceTest.java:130)
    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:44)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:49)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
    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)

回答1:

I'm not sure what mocking library you're using, but there is a helpful integration between Spring and Mockito aptly and clumsily named "Springockito" that can make tactical insertions of mocks in a greater Spring context pretty easy.

The idea is that you change your test application context to map that bean to a mock, rather than try to wire the mock to your parent bean at runtime.

So really you just end up with:

text-context.xml

<beans xmlns="http://www.springframework.org/schema/beans"
      ...
      xmlns:mockito="http://www.mockito.org/spring/mockito"
      xsi:schemaLocation="... http://www.mockito.org/spring/mockito https://bitbucket.org/kubek2k/springockito/raw/tip/springockito/src/main/resources/spring/mockito.xsd">

    <mockito:mock id="m_orderDao" class="my.package.OrderDao"/>

    <!--... other config ...-->
 </beans>

Then you can just autowire the mock itself into your test if you need to interact with it, the same way you are doing for your service as it stands now.

If you're not using Mockito, you can still use the approach above, but instead use your mocking library's method for creating mocks as the factory for the bean.



回答2:

As of 2014 the easiest solution is to use @InjectMocks annotation that is part of Mockito. This works with any class including the ones marked with @Transactional.

Here is an example :

public class TestTestController {

    @Mock
    private TestService testService;

    @InjectMocks
    private TestController testController;

    @Before
    public void initMocks() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testMocks() throws Exception {
        String mockedReturnValue = "this is a mocked reply";
        when(testService.getMessage()).thenReturn(mockedReturnValue);

        assertEquals(mockedReturnValue, testController.callTestService());

    }
}

and the related classes

public class TestController {

    @Autowired
    private TestService testService;

    public String callTestService() {
        return testService.getMessage();
    }

}

public class TestService {

    public static final String THIS_IS_A_TEST = "this is a getMessage";

    public String getMessage() {
        return THIS_IS_A_TEST;
    }

}