Skip to main content

Speeding up Spring integration tests

The biggest problem with unit testing using Spring testing support* is the time it takes to initialize the Spring framework context. Every new test case adds precious seconds to overall build time. After a while it will take minutes or even hours to fully build the application, while most of this time is consumed by Spring itself. But we'll start from the basics.

In order to make JUnit aware of Spring framework test support, simply add these annotations on test case class:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@Transactional
public class MainControllerTest {
    //...
}

While @Transactional is not necessary, it will greatly simplify testing when database is involved (details here). In IntelliJ IDEA 10 (I just took this brand new version for a test drive) these annotations will raise the following error to occur:



And suggested solution:



You now have two options: either create the file named the same as your test case with -context.xml suffix (and in the same package) as suggested or use different file and specify its name explicitly using locations attribute to @ContextConfiguration. Following convention over configuration for now I recommend you to follow the Spring naming convention. When using Maven, your class under test should reside in src/main/java, test case in src/test/java and Spring configuration file in src/test/resources (but see IDEA-61829):

pom.xml
src
|-- main
|    -- java
|       `-- com
|           `-- blogspot
|               `-- nurkiewicz
|                   `-- spring
|                       `-- test
|                           `-- web
|                               `-- MainController.java
`-- test
    |-- java
    |   `-- com
    |       `-- blogspot
    |           `-- nurkiewicz
    |               `-- spring
    |                   `-- test
    |                       `-- web
    |                           `-- MainControllerTest.java
    `-- resources
         -- com
            `-- blogspot
                `-- nurkiewicz
                    `-- spring
                        `-- test
                            `-- web
                                `-- MainControllerTest-context.xml

In case you'll get lost, IDEA provides magnificent Packages view in Project explorer:



As you can see files in different physical directories are all located in the same logical directory corresponding to the package. This is especially useful when working with Wicket web framework, where each page class must have equivalent HTML file, preferably in src/main/resources.

Coming back to Spring. When running the test case, Spring runner will automatically open the *-context.xml file and initialize the application context described in this file. The context will typically contain class under test bean definition along with its direct dependencies. Now you can inject every bean from the context directly to your test case class:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class MainControllerTest {

 @Resource
 private MainController mainController;

 @Test
 public void smokeTest() throws Exception {
  //mainController...
 }

}


The important thing to remember is that spring context will be initialized prior the first test method is executed and (unless you use @DirtiesContext annotation) will be reused (rather than recreated) for every subsequent test method in this test case. This is a way of decreasing the test execution time. Although it is a myth that Spring context initialization takes so much time, but some of your own beans might increase this time significantly. For instance Hibernate/JPA persistence providers or embedded ActiveMQ server are huge facilities taking several seconds to boot up. This is the major drawback of Spring tests, making many developers reluctant to them.

What we recently discovered is that Spring out of the box supports reusing once initialized context even in different test case classes across your artifact. This means that in best case scenario you pay the price of context startup only once and use the same context across all your tests, making startup time less relevant and insignificant compared to the overall build time.

In order to benefit from this feature, you must forget everything I said about convention over configuration. Now every test case has its own context configuration file, treated as independent application context. But if you reuse the same file in every test case, Spring will figure out that every test case points to the same file and simply reuse the context as well, for example:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:test-context.xml")
@Transactional
public class MainControllerTest {
    //...
}

//...

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:test-context.xml")
@Transactional
public class BarRepositoryTest {
    //...
}

//...

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:test-context.xml")
@Transactional
public class BarServiceTest {
    //...
}

//...

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:test-context.xml")
@Transactional
public class FooRepositoryTest {
    //...
}

//...

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:test-context.xml")
@Transactional
public class FooServiceTest {
    //...
}


By the way if you are disgusted by the annotations repetition, inheritance comes to the rescue.

There are few consequences of single vs. specialized context for every test case. First of all, the single context must be suitable for each and every test case, which means it must contain all beans being tested (effectively: almost whole application). This means that even though the complete build will be much faster, running a separate test case will cost you much more time. But there is a workaround for that as well. In your complete test context simply declare:

<beans default-lazy-init="true">

This will cause loading only these beans, that are necessary in this particular test. And when running a full test suite, all beans will be lazily initialized one after another. In one context per test case approach each test context has only carefully chosen, fine grained beans. In single context you must have all the beans declared, but thanks to lazy loading not all of them will be created when not needed.

To sum things up. In order to get the most of your Spring integration testing, take your production application context, mocking only necessary dependencies like database or JMS. Thanks to that you will avoid repeating the bean definitions in production and test XML context files. Once having one, master test context, point to it in every test case to make efficient use of Spring context caching. Happy testing!

* Even bigger problem is that such tests shouldn't be considered as unit tests at all, as they test system as a whole rather than separate class (unit). That is why you should consider Spring-powered tests as integration tests and treat them as complementary to unit tests rather than their substitution.


Comments

  1. Hello,
    nice post. Maybe you now how to speed hibernate startup time in integration tests

    ReplyDelete
  2. Well, if you manage to start Hibernate only once for a whole test suite (which I explained how can it be achieved), boot time shouldn't be such a big issue anymore.

    As for the Hibernate itself, maybe you could try to disable automatic schema creation (I guess you're using in-memory database) and run previously generated DDL script manually before SessionFactory startup? Just a guess.

    ReplyDelete
  3. Yes I'm using in memory databse and creating sessionFactory per suite.
    But I haven 1.563 tables and hibernate starts about 40 second :-(

    ReplyDelete
  4. 1.5K database tables, impressive :-). You should probably address this question to Hibernate community, my only suggestion is to avoid database schema generation overhead, but it's hard to say what eats most of the time during boot in your case.

    ReplyDelete
  5. Common test context XML configuration has two main drawbacks:
    a) configuration scalability. Maintaining single configuration context becomes a horror.
    b) violation of the tests isolation. Tests should be as isolated from each other as possible. Execution of one test should not affect nor be related to the execution of the other tests.

    I know that shared Spring context REALLY speeds up the tests and actually I've been using it for a while (even in current projects). However I start to think that it's better to created separated minimal test contexts. For the larger projects maintainability of tests are far more important for me than speed of test.

    If your tests take too long to execute, it's often a sign that you need to split the developed module into the smaller, more grained modules.

    I definitely start to prefer maintainability of my tests over it's overall execution speed.

    ReplyDelete
  6. Two gists from GitHub not visible at page..

    ReplyDelete
  7. Anonymous: thank you, it's fixed now!

    ReplyDelete
  8. Thank you very much Tomek.
    I was struggling with this problem as one of my test is dirtying up the context, which is used by other tests. I didn't know about this spring feature until I saw your post.

    -Kiran

    ReplyDelete
  9. Hi,

    We use eclipse indigo, maven 3 and spring 3.1. When I run the tests from the IDE spring context is not re-initialized for every unittest. But when we run from command line.

    e.g. # mvn test

    we do see spring context being initialized multiple times.

    Is there a solution for this as well? Are you able to confirm this in your scenario?

    Thanks
    PixalSoft

    ReplyDelete
    Replies
    1. Please ignore this. This has nothing to do with Spring. Apparently, maven-surefire plugin added support for junit 4.7+ so that several tests run in parallel, each in their own JVM. If this is disabled then the spring context is not initialized again.

      Delete
  10. Hi. How can this be done when using AnnotationConfigContextLoader based context initialization ?

    @ContextConfiguration(classes=TestConfig.class,loader = AnnotationConfigContextLoader.class)

    ReplyDelete

Post a Comment