2010-06-30

Integration testing your Spring 3 & JPA 2.0 application

In my previous posts, I explained the following:

Now, one thing that is missing in this picture is...


Testing DAO classes


There is a presentation by Rod Johnson of Spring on testing with Spring that explains you what unit testing and what integration testing is, what mock objects are and some of the best practices. The presentation itself is from 2007 and while the general ideas haven't changed, the implementation techniques have.

Therefore in this article I will show you how to integration test your DAO classes. I am going to use a JPA project for this, but the same test code will work for Hibernate as well.


Integration Testing principles


Integration testing is another level of tests; while unit tests test pieces of code in separation, integration tests test the bigger picture. Unit tests don't go to data sources, they are fast and simple. Integration tests hit the data sources and test multiple modules together.

So, you should have a database set up, in a well known state. This should not be the production database! But it should be identical (or as close as possible).

On this database you perform the tests. This is better than mock objects, because although you can simulate certain behaviors with mock objects, it's nothing compared to using a real database, with its triggers, views, stored procedures etc.


How does it, uhm, how does it work?


Integration testing that involves a data source, whether you use Spring or, say, DBUnit, often works according to the same schema.

You mess around with the database, add, update, remove. Then all the changes are reverted.

With Spring, they are rolled back.


Integration testing with Spring 3


Prior to Spring 3 the recommended way of testing JPA was to use AbstractJpaTests - but not anymore; now it's deprecated. The official documentation suggests you use (extend) AbstractJUnit38SpringContextTests. You would rather use AbstractJUnit4SpringContextTests if you use JUnit 4. But both strategies require extending and Java allows for inheritance only (which is good), so I will show you an alternative way of doing it.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "file:src/test/resources/applicationContext-test.xml" })
@TransactionConfiguration(transactionManager = "myTransactionManager", defaultRollback = true)
@Transactional
public class DogsDaoImplTest {
// ...

Alright! Let's explain these annotations, one by one.

  • @RunWith(SpringJUnit4ClassRunner.class) - this means that JUnit will enable the functionality of the Spring TextContext Framework. That makes it possible for you to use Spring goodies (annotations like @Autowired etc.) in the tests
  • @ContextConfiguration - tells Spring where the beans files are. Note the format. This way it works within Eclipse, within Ant, within Maven, from Hudson etc.
  • @TransactionConfiguration - here we configure the transaction. We specify the manager to use and if rollback should be the default thing to do when a transaction ends (a bit explicit - the default is true anyhow)
  • @Transactional - all methods must run in transactions so that their effects can be rolled back

NOTE: Some configurations also use this annotation:
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class })
But the code works without it... I don't see why it should be used. Perhaps someone can clarify on that.

And now a test method.

@Autowired
    private DogsDao dogsDaoImpl;

    @Test
    public void testPersistDog() {
        long dogsBefore = dogsDaoImpl.retrieveNumberOfDogs();

        Dog dog = new Dog();
        dog.setName("Fluffy");
        dogsDaoImpl.persistDog(dog);
        long dogsAfter = dogsDaoImpl.retrieveNumberOfDogs();

        assertEquals(dogsBefore + 1, dogsAfter);
    }


Dependencies


In the previous posts I screwed up dependencies - yes. I imported spring-dao, version 2.0.8 - WRONG. Here's the correct dependency set for the whole project:

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
        http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>me.m1key</groupId>
    <artifactId>springtx</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.0.2</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate</artifactId>
            <version>3.5.3-Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>3.5.3-Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-annotations</artifactId>
            <version>3.5.3-Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-commons-annotations</artifactId>
            <version>3.3.0.ga</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.javax.persistence</groupId>
            <artifactId>hibernate-jpa-2.0-api</artifactId>
            <version>1.0.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>3.5.3-Final</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>1.8.0.7</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>3.0.3.RELEASE</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>3.0.3.RELEASE</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>3.0.3.RELEASE</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.5.8</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.5.8</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>3.0.3.RELEASE</version>
            <type>jar</type>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.8.1</version>
            <type>jar</type>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>3.0.3.RELEASE</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>3.0.3.RELEASE</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
    </dependencies>
    <repositories>
        <repository>
            <id>r.j.o-groups-public</id>
            <url>https://repository.jboss.org/nexus/content/groups/public/</url>
        </repository>
    </repositories>
</project>

Previously I was getting this:
java.lang.NoSuchMethodError:
org.springframework.transaction.interceptor.
TransactionAttribute.getQualifier()Ljava/lang/String;
Now it's fixed.


Other possibilities


Apart from JUnit traditional annotations, such as @Before and @BeforeClass, you can also use:
  • @BeforeTransaction (org.springframework.test.context.transaction.BeforeTransaction)
  • @AfterTransaction (org.springframework.test.context.transaction.AfterTransaction)


Summary


In this article I showed you how to integration test DAO classes with Spring 3.

Download source code for this article

8 comments:

  1. So your tests save the data and read it back in the same transation? I'm afraid you will miss some corner cases, especially when using stuff like JPA or Hibernate, which does quite a lot of "magic" behind the scenes (never got an exception when data you wanted to persist/update was being flushed on commit performed at the end of web request?)

    We've been through this in projects I participated to, and never found any way to do really good integration tests other than writing & reading test data in separate transations (just like it will happen in real environment) and cleaning the database state somehow after the test case (yeah, really annoying, ugly and slower than just rolling back the transation but much more accurate).

    ReplyDelete
  2. Cześć Marcin, thanks for your comment!

    True - that's how it works. Each test in one, separate transaction. You're absolutely right, that misses quite a bit - transaction separation for one thing.

    But integration tests are just another layer of tests, so you might argue whether you should go that deep into details here, or perhaps on another level (with functional tests for example).

    ReplyDelete
  3. Add flush + clear after data preparation and there will be no problem at all

    dogsDaoImpl.persistDog(dog);

    em.flush();
    em.clear();

    long dogsAfter = ...
    assertEquals(dogsBefore + 1, dogsAfter);

    ReplyDelete
  4. @Michał
    Yeah, functional/systems tests might catch such issues. I'm not sure though where the responsibility to test such cases belongs.
    On one hand I'd like to believe that a green bar in integration test means that I'm using Hibernate/JPA/whatever correctly and won't get any nasty surprise (that's why you write integration tests above unit tests after all - mock objects are just not enough to feel safe with your code). On the other hand - as you say - maybe higher-level tests should verify broader concerns like transactional behaviour. Still not sure about it...

    I must admit that in my team we spend more time discussing integration testing than any other aspect of programming (yeah, we are clear tests freaks, at least some of us). Most of the time we write them a bit higher level than you presented in this blog entry (that's in fact what made me write a comment), with separate transactions for reads and writes, but I think I might try doing it your way, especially that I'll be starting a brand new, fresh project really soon and will have some time to experiment.

    @cVoronin:
    Yeah, that could help, but I don't think i would do that in my tests, because I find it rather ugly - that's just a hack to simulate real environment. If I was sure that I want to test transactional behaviour of my dao I would rather make run the code in different transactions and deal with cleanup after the test

    ReplyDelete
  5. @cVoronin, thanks for your input. That would require a slight change in my code, as em (EntityManager) is not exposed.

    @marcin
    Sorry for the cliche: "Program testing can be used to show the presence of bugs, but never to show their absence!" -- E. Dijkstra.
    There's just no way you can get 100% confidence.

    If you try this method in your future project(s), please inform us whether it worked and what your experiences are - I would be delighted to know.
    PS. Perhaps DBUnit would be a better idea than this method (in your context)?

    ReplyDelete
  6. @Michał
    I didn't really meant that I want to be 100% sure my code works. I just wanted to say that IMHO you write integration tests to know that your code that uses 3rd party libraries & external components (like databases) behaves correctly - that is as you expected. Of course I'm not able to cover all cases - not only because the cost would greatly exceed the benefit but also because I'm sure I can't even imaging some of them.

    As for DBUnit, I tried it in the past but didn't really like it, because with it I have to split my tests into Java code & XML datasets. I think it reduces the value of tests as examples/documentation as part of the test (initial data) is hidden somewhere outside the test class. You then have to switch back and forth between (at least) two files. Though automatic cleanup is a really nice thing to have.

    And of course, will surely give you some feedback when I get the change to use your tips in practice.

    ReplyDelete

  7. تتميز شركة أرمور بتقديم عدد متنوع من الخدمات المنزلية مثل تعقيم المنازل وتنظيف المكيفات
    شركة تعقيم منازل
    يجب على أفراد الأسرة اتباع الإجراءات الوقائيّة في العمل والمنزل، بما في ذلك الحرص على تنظيف اليدين إذا كانت الأيدي مُتّسخة، وبعد استخدام الحمام، وقبل الأكل أو تحضير الطعام، وبعد السّعال أو العطاس أو التمخط من الأنف، وبعد ملامسة الحيوانات
    شركة تنظيف مكيفات

    يؤدي انسداد المكثفات في الوحدة الداخلية لمكيف الهواء إلى تسرب المياه بسبب تجمع الطحالب أو الفطريات عليها، وتتسبب هذه المشكلة في رجوع المياه إلى داخل الأنبوب، وقد تتسرب المياه بسبب وجود عطل في مضخة التكثيف، وعندها يتطلب الإصلاح استبدالها بمضخة تكثيف أخرى.

    ReplyDelete