Spring beans redefinition in unit test environment

2019-01-16 04:07发布

we are using Spring for my application purposes, and Spring Testing framework for unit tests. We have a small problem though: the application code loads a Spring application context from a list of locations (xml files) in the classpath. But when we run our unit tests, we want some of the Spring beans to be mocks instead of full-fledged implementation classes. Moreover, for some unit tests we want some beans to become mocks, while for other unit tests we want other beans to become mocks, as we are testing different layers of the application.

All this means I want to redefine specific beans of the application context and refresh the context when desired. While doing this, I want to redefine only a small portion of the beans located in one (or several) original xml beans definition file. I cannot find an easy way to do it. It's always regarded that Spring is a unit testing friendly framework so I must be missing something here.

Do you have any ideas how to do it?

Thanks.

13条回答
SAY GOODBYE
2楼-- · 2019-01-16 04:27

One of the reasons spring is described as test-friendly is because it may be easy to just new or mock stuff in the unit test.

Alternately we have used the following setup with great success, and I think it is quite close to what you want, I would strongly recommend it:

For all beans that need different implementations in different contexts, switch to annotation based wiring. You can leave the others as-is.

Implement the following set of annotations

 <context:component-scan base-package="com.foobar">
     <context:include-filter type="annotation" expression="com.foobar.annotations.StubRepository"/>
     <context:include-filter type="annotation" expression="com.foobar.annotations.TestScopedComponent"/>
     <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
 </context:component-scan>

Then you annotate your live implementations with @Repository, your stub implementations with @StubRepository, any code that should be present in the unit-test fixture ONLY with @TestScopedComponent. You may run into needing a couple more annotations, but these are a great start.

If you have a lot of spring.xml, you will probably need to make a few new spring xml files that basically only contain the component-scan definitions. You'd normally just append these files to your regular @ContextConfiguration list. The reason for this is because you frequently end up with different configurations of the context-scans (trust me, you will make at least 1 more annotations if you're doing web-tests, which makes for 4 relevant combinations)

Then you basically use the

@ContextConfiguration(locations = { "classpath:/path/to/root-config.xml" })
@RunWith(SpringJUnit4ClassRunner.class)

Note that this setup does not allow you to have alternating combinations of stub/live data. We tried this, and I think that resulted in a mess I wouldn't recommend anyone ;) We either wire inn the full set of stubs or the full set of live services.

We mainly use auto-wired stub dependencies when testing gui near stuff where the dependencies are usually quite substantial. In cleaner areas of the code we use more regular unit-testing.

In our system we have the following xml-files for component-scan:

  • for regular web production
  • for starting web with stubs only
  • for integration tests (in junit)
  • for unit tests (in junit)
  • for selenium web tests (in junit)

This means we totally have 5 different system-wide configurations that we can start the application with. Since we only use annotations, spring is fast enough to autowire even those unit tests we want wired. I know this is untraditional, but it's really great.

Out integration tests run with full live setup, and once or twice I have decided to get really pragmatic and want to have a 5 live wirings and a single mock:

public class HybridTest {
   @Autowired
   MyTestSubject myTestSubject;


   @Test
   public void testWith5LiveServicesAndOneMock(){
     MyServiceLive service = myTestSubject.getMyService();
     try {
          MyService mock = EasyMock.create(...)
          myTestSubject.setMyService( mock);

           .. do funky test  with lots of live but one mock object

     } finally {
          myTestSubject.setMyService( service);
     }


   }
}

I know the test purists are going to be all over me for this. But sometimes it's just a very pragmatic solution that turns out to be very elegant when the alternative would be really really ugly. Again it's usually in those gui-near areas.

查看更多
等我变得足够好
3楼-- · 2019-01-16 04:27

Since the OP this has come along: Springockito

查看更多
女痞
4楼-- · 2019-01-16 04:28

See this tutorial with @InjectedMock annotation

It saved me a lot of time. You just use

@Mock
SomeClass mockedSomeClass

@InjectMock
ClassUsingSomeClass service

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

and all your problems are solved. Mockito will replace the spring dependency injection with a mock. I just used it myself and it works great.

查看更多
Anthone
5楼-- · 2019-01-16 04:29

i would propose a custom TestClass and some easy rules for the locations of the spring bean.xml

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
    "classpath*:spring/*.xml",
    "classpath*:spring/persistence/*.xml",
    "classpath*:spring/mock/*.xml"})
@Transactional
@TestExecutionListeners({
    DependencyInjectionTestExecutionListener.class,
    TransactionalTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class})
public abstract class AbstractHibernateTests implements ApplicationContextAware 
{

    /**
     * Logger for Subclasses.
     */
    protected final Logger LOG = LoggerFactory.getLogger(getClass());

    /**
     * The {@link ApplicationContext} that was injected into this test instance
     * via {@link #setApplicationContext(ApplicationContext)}.
     */
    protected ApplicationContext applicationContext;

    /**
     * Set the {@link ApplicationContext} to be used by this test instance,
     * provided via {@link ApplicationContextAware} semantics.
     */
    @Override
    public final void setApplicationContext(
            final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
}

if there are mock-bean.xml in the specified location, they will override all "real" bean.xml in the "normal" locations - your normal locations might differ

but... i would never mix mock and non-mock beans it's hard to trace problems, when the application grows older.

查看更多
爱情/是我丢掉的垃圾
6楼-- · 2019-01-16 04:30

There are some very complicated and powerful solutions listed here.

But there is a FAR, FAR simpler way to accomplish what Stas has asked, which doesn't involve modifying anything other than one line of code in the test method. It works for unit tests and Spring integration tests alike, for autowired dependencies, private and protected fields.

Here it is:

junitx.util.PrivateAccessor.setField(testSubject, "fieldName", mockObject);
查看更多
一纸荒年 Trace。
7楼-- · 2019-01-16 04:31

You can also write your unit tests to not require any lookups at all:

@ContextConfiguration(locations = { "classpath:/path/to/test-config.xml" })
@RunWith(SpringJUnit4ClassRunner.class)
public class MyBeanTest {

    @Autowired
    private MyBean myBean; // the component under test

    @Test
    public void testMyBean() {
        ...
    }
}

This gives an easy way to mix and match real config files with test config files.

For example, when using hibernate, I might have my sessionFactory bean in one config file (to be used in both the tests and the main app), and have by dataSource bean in another config file (one might use a DriverManagerDataSource to an in-memory db, the other might use a JNDI-lookup).

But, definitely take heed of @cletus's warning ;-)

查看更多
登录 后发表回答