Testing a persistent domain model

Some developers treat testing in the same way I treat flossing: It's a good idea but they either do it with great reluctance or not at all.

Note: This excerpt is taken from Chapter 4 of POJOs in Action, from Manning Publications.

Every six months, Anne-Marie, who is my dental hygienist, gives me the same lecture on the importance of flossing. And each time, I half-heartedly promise that I will make more of an effort - but I never keep that promise. Some developers treat testing in the same way I treat flossing: It's a good idea but they either do it with great reluctance or not at all.

Nevertheless, testing is a key part of the software development process, and just as flossing prevents dental decay, testing prevents software decay. The persistent layer, like most other application components, is not immune to decay and so requires testing. You need to write tests that verify that the domain model is mapped correctly to the database and that the queries used by the repositories work as expected. There are two main challenges when testing a persistent domain model. The first challenge is writing tests that detect the ORM-specific bugs. These bugs are often caused by inconsistencies between the domain model, the ORM document, and the database schema. For example, one common mistake is to forget to define the mapping for a newly added field, which can cause subtle bugs. Database constraints are another common problem that prevents the application from creating, updating, or deleting persistent objects. It's essential to have tests for the persistent domain model that catch these and other issues.

The second challenge is effectively testing the persistent domain model while minimizing the amount of time it takes for the tests to run. The test suite for the O/R mapping of a large domain model can take a long time to execute. Not only are there a large number of tests but also a test that accesses the database can take much longer to run than a simple object test. Although some database testing is unavoidable, it's important to find ways to do testing without it.

In this section you will learn about the different kinds of ORM bugs and how to write tests to detect them. I describe which aspects of the O/R mapping must be tested against the database and which other aspects can be tested without a database in order to minimize test execution time. You will see example tests that use the strategies described here in chapters 5 and 6.

4.5.1 Object/relational testing strategies

A variety of bugs can lurk in the O/R mapping, including the following:

  • Missing mapping for a field
  • References to nonexistent tables or columns
  • Database constraints that prevent objects from being inserted, updated, or deleted
  • Queries that are invalid or that return the wrong result
  • Incorrect repository implementation

Many bugs are caused by the domain model, ORM documents, and the database schema getting out of sync. For example, it is easy to change the domain model by adding a new field or renaming an existing one and then forgetting to add or update the O/R mapping for that field, which specifies how it is stored in the database. Some ORM frameworks will generate an error message if the O/R mapping for a field is undefined, but others (including Hibernate) will silently allow a field to be nonpersistent, which can cause subtle and hard-to-find bugs. It is also quite easy to forget to update the database schema when defining the mapping for a field.

Some bugs are easily caught, such as those detected by the ORM framework at startup. For instance, Hibernate complains about missing fields, properties, or constructors when the application opens a SessionFactory. Other kinds of bugs require a particular code path to be executed. An incorrect mapping for a collection field can remain undetected, for example, until the application tries to access the collection. Similarly, bugs in queries are often not detected until they are executed. In order to catch these kinds of bugs, we must thoroughly test the application.

One way to test a persistence layer is to write tests that run against the database. For example, we can write tests that create and update persistent objects and call repository methods. Yet one problem with this kind of testing is that the tests take a while to execute even when using an in-memory database such as HSQLDB. Another problem is that they can fail to detect some bugs, such as a missing mapping for a field. And writing them can be a lot of work.

A more effective and faster approach is to use several kinds of tests that test each part of the persistence layer separately. Some kinds of tests run against the database and others run without the database. The tests that run against the database are:

  • Test that create, update, and delete persistent objects
  • Tests for the queries that are used for the repositories
  • Tests that verify that the database schema matches the object/relational mapping

There are also tests that don't use the database:

  • Mock object tests for the repositories
  • Tests that verify the O/R mapping by testing the XML mapping documents

Next we'll look at these different kinds of tests, beginning with those that run against the database.

4.5.2 Testing against the database

Tests that run against the database are an essential part of testing the persistent domain model even though they take a relatively long time to execute. There are two kinds of database-level tests. The first kind verifies that persistent objects can be created, updated, and deleted. The second kind verifies the queries that are used by the repositories. Let's look at each approach.

Testing the persistent objects

One goal of testing the persistent domain model is to verify that persistent objects can be saved in the database. A simple approach is to write a test that creates a graph of objects and saves it in the database. The test doesn't attempt to verify that the database tables contain the correct values and instead fails only if an exception is thrown by the ORM framework. This kind of test is a relatively easy way to find basic ORM bugs, including missing mappings for a class and missing database columns. It also verifies that the database constraints allow new objects to be inserted into the database. However, even though this kind of test is a good way to start, it does not detect other common ORM bugs, such as constraint violations that occur when objects are updated, added, or deleted.

We can catch those types of bugs by writing more elaborate tests that update and delete persistent objects. As well as saving an object in the database, a test loads the object, updates it, and saves it back. A test can also delete the object. For example, a test for PendingOrder could consist of the following steps:

  1. Create a PendingOrder and save it.
  2. Load it, update the delivery information, and save it.
  3. Load it, update the restaurant, and save it.
  4. Load it, update the quantities, and save it.
  5. Load it, update the quantities, and save it (again to test deleting line items).
  6. Load it, update the payment information, and save it.
  7. Delete the PendingOrder.

This testing approach verifies that the database can store all states of an object and detects problems with database constraints when creating or destroying associations between objects. Each step of the test consists of a database transaction that uses a new persistence framework connection to access the database. Using a new transaction and connection each time ensures that objects are really persisted in the database and loaded again. It also makes sure that deferred constraints, which are not checked until commit time, are satisfied. The downside of this approach is that it changes the database, which requires each test to initialize the database to a known state.

We could also enhance the tests to verify that an object's fields are mapped correctly by validating the contents of the database tables. After inserting the object graph into the database, the test verifies that the database contains the expected rows and column values. A test can verify the contents of the database by using to JDBC to retrieve the data. Alternatively, it could use DbUnit [DbUnit], which is a JUnit extension, to compare the database tables against an XML file that contains the expected values. However, although this approach is more thorough it is extremely tedious to develop and maintain these kinds of tests. In addition, the tests don't detect a missing mapping for a newly added field or property. Consequently, a much better way to test that classes and field/properties are mapped correctly is, as I describe later, to test the ORM document directly.

Tests that insert, update, and delete persistent objects are extremely useful, but they can be challenging to write. One reason is because some objects have lots of states that need to be tested. Another reason for the complexity is the amount of setup often required. Tests may have to create other persistent objects that are referenced by the object being tested. For example, in order to persist a PendingOrder and its line items, the test has to initialize the database with Restaurant and MenuItems. In addition, an object's public interface doesn't usually allow its fields to be set directly and so a test must call a sequence of business methods with the correct arguments, which can involve even more setup code. As a result, it can be challenging to write good persistence tests.

The other drawback with this approach is that executing the tests can be slow because of the number of times the database is accessed. Each persistent class can have multiple tests that each consists of multiple steps. Each step makes multiple calls to the ORM framework, which executes multiple SQL statements. Consequently, these tests usually take too long to be part of the unit tests suite and instead should be part of the functional tests.

Even though these persistent object tests can be difficult to write and can take a significant amount of time to execute, they are an important part of the test suite for a domain model. If necessary you can always start off by writing tests that just save objects in the database and over time add tests that update and delete objects.

Testing queries

We need to write database-level tests for some of the queries that are used by the repositories. One basic way to test the queries is to execute each query once and ignore the result. This quick and easy approach can catch lots of basic errors and is often all you need to do for simple queries.

For more complex queries, it is usually important to detect bugs in the logic of the query such as using < instead of <=. To catch these kinds of bugs, we need to write tests that populate the database with test data, execute the query, and verify that it returns the expected objects. Unfortunately, these kinds of tests are time consuming to both write and execute.

There are a couple of ways a test can execute a query. One option is to execute the query directly using the Spring and Hibernate APIs. The other option is to execute the query indirectly by invoking the repository. Which of these options is better depends on various factors, including the complexity of the repository. If the repository is fairly simple, then it can be easier to test the query by calling the repository because it is straightforward to execute the query with a particular set of arguments. If the repository is more complex, then testing the queries directly can be easier.

To be able to test a query independently of the repository that executes it, the query must be stored separately from the repository. The easiest way to accomplish this is to use named queries that are defined in the mapping document. Both Hibernate and JDO 2.0 let you define queries in the XML mapping document and provide an API for executing them by name. In addition to keeping the queries separate from the repositories, it is a lot more manageable to define multiline queries in an XML document than it is to do so in Java code by concatenating multiple strings. Alternatively, if you are using an ORM framework that doesn't support named queries, such as a JDO 1.x implementation, then you should store the queries in a properties file. Once you have done this, the queries can be tested separately.

Verifying that the schema matches the mapping

Unless the schema is generated from the O/R mapping, it is possible for the mapping and the schema to get out of sync. It is quite easy, for example, to forget to add a new column to a table after defining the mapping for a field. Consequently, we must write tests that verify that the database schema matches the O/R mapping.

One way to test the database schema is to extract the table and column names from the mapping document and use the JDBC metadata APIs to verify that every table and column exists in the database schema. This kind of test executes fairly quickly because it makes relatively few calls to the database. However, the one drawback is that you have to write a lot of code to implement this kind of test.

A much easier option that you can use some with ORM frameworks such as Hibernate is the ORM framework's schema generation feature. Some ORM frameworks provide an API to generate a SQL script that adds the missing tables and columns to the database schema. It is extremely easy to write a test that generates the script and fails if it contains SQL commands to add tables or columns.

Using an in-memory database

A great way to speed up database-level tests is to use an in-memory SQL database such as HSQLDB [HSQLDB]. An in-memory database runs in the application's JVM and is a lot faster than a regular database because there is no network traffic or disk access. Because the ORM framework insulates application code from many aspects of the database, some aspects of using an in-memory database are very straightforward. To configure the ORM framework to use the in-memory database, you typically have to specify the appropriate JDBC driver and other settings. Once you have done this, the ORM framework will automatically generate the correct SQL statements.

One challenge when using an in-memory database is ensuring that its schema is identical to the production database's schema. This isn't a problem if the ORM framework generates the database schema. However, if the production database schema is maintained separately, then its definition might not be compatible with the in-memory database. It could, for example, use vendor-specific data types and other features. In order to use an in-memory database, you will need to use a different schema definition or generate its schema from the ORM. In either case, there is no guarantee that the in-memory database has the same schema as the production database. As a result, an in-memory database is only useful for certain kinds of tests. You could, for example, use an in-memory database to test the queries.

Another issue with using an in-memory database is that although it is faster than a regular database, the tests can still be much slower than simple object tests. This is because calling the ORM framework and accessing the database simply involves a lot of overhead. In addition, initializing the database to the correct state at the start of a test and verifying its state at the end can make the tests more complicated. Consequently, in order to minimize test execution time and complexity it is important to test as much as possible without the database.

4.5.3 Testing without the database

Testing against the database is certainly important, but a lot of testing can be done without the database. We can verify that the O/R mapping correctly maps classes and fields to tables and columns without even opening a database connection. We can also test the repositories using mock objects. Let's take a closer look.

Verifying the mapping document

JDO and Hibernate define the O/R mapping using an XML document. We can write tests that verify that the mapping document correctly specifies the mapping from classes, fields, and relationships to tables, columns, and foreign keys. For example, it is quite easy to write a test that verifies that a class is mapped to a particular table and that all of its fields are mapped to columns of that table. This kind of test is extremely useful since it fails whenever you forget to map a newly defined field. With a little bit more effort we could also write a test that verifies that each field is mapped to the correct column.

One straightforward way to implement this kind of test is to use XmlUnit [XmlUnit], which is a JUnit extension for testing XML documents. A test for the ORM can use XmlUnit to make assertions about the contents of the document. For example, a test can verify that the PendingOrder class is mapped to the PENDING_ORDER table using the following code:

class PendingOrderMappingTests extends XMLTestCase {

public void testMapping() throws Exception {
 Document mappingDocument = ...;
 assertXpathEvaluatesTo("PENDING_ORDER",
  "hibernate-mapping/class[@name='PendingOrder']/@table",
  mappingDocument);
  ...
 }
}

The test case extends XMLTestCase, which is provided by XmlUnit. It calls assertXpathEvaluatesTo(), which is an XmlUnit method that throws an exception if the specified XPath expression does not evaluate to the expected value. The XPath expression used by this particular test retrieves the value of the table attribute of a <class> element that is a child of <hibernate-mapping> and has a name attribute whose value is PendingOrder. The test could also call to assertXpathEvaluatesTo() to verify that each field is mapped to the correct column. It is also valuable to use reflection to get the names of all of the fields and verify that each field is mapped.

You can use XmlUnit to test the O/R mapping for a variety of ORM frameworks. The one drawback is that writing the XPath expressions can be tricky. A better option, which can be used with some ORM frameworks, is to get the O/R mapping metadata from the ORM framework. Some ORM frameworks provide an API, which returns Java objects that describe the mapping. An ORM test can then make assertions about the objects. This approach does not require detailed knowledge about the structure of the mapping document but does require the ORM framework to expose the necessary APIs. I describe how to write ORM tests in more detail in chapters 5 and 6.

Mock object testing of repositories

We could test a repository using database-level tests. For example, one way to test a repository method that executes a query is to populate data with test objects, call the method, and verify that it returns the expected objects. The problem with this approach is that it tests several things simultaneously: the repository, any queries that it executes, and the O/R mapping. The test needs a lot of setup and executes slowly. A better approach, which reduces the number of test cases and database accesses, is to test the repository using mock objects and to test the queries against the database separately.

Consider, for example, the PendingOrder.createPendingOrder() method, which creates a PendingOrder in the database. One way to test this method is to write a test that calls it and then verifies that it inserted a row into the PENDING_ORDER table. However, if you have written tests for the object/relational mapping, then you can safely assume that HibernateTemplate.save() or JdoTemplate.makePersistent() will work as expected. The repository test does not need to verify that a PendingOrder will be inserted into the PENDING_ORDER table when the repository calls save() or makePersistent(). We can therefore simplify and speed up the repository test by using a mock object for HibernateTemplate or JdoTemplate and verifying that the repository calls save() or makePersistent() as expected. We can use a similar approach to test other repository methods.

4.5.4 Overview of ORMUnit

In order to make it easier to write tests for the O/R mapping and persistent objects, I've written a simple JUnit extension called ORMUnit. It provides several base classes that extend JUnitTestCase:

  • HibernateMappingTests: For testing a Hibernate object/relational mapping
  • JDOMappingTests: For testing a JDO object/relational mapping
  • HibernatePersistenceTests: For testing Hibernate objects and queries
  • JDOPersistenceTests: For testing JDO objects and queries

HibernateMappingTests JDOMappingTests

JDOPersistenceTests and HibernatePersistenceTests make it easier to write tests for persistent objects and queries. They take care of opening and closing the PersistenceManager and Session; create the HibernateTemplate and JdoTemplate; and provide methods for managing transactions. In chapters 5 and 6 you will see examples of tests that use these classes.

Automated testing is an important tool for ensuring that the application works correctly. It's something that we all need to do regularly (along with flossing). But when we are developing an application we also need to consider performance. Let's now look at how to optimize the performance of an application that uses JDO and Hibernate.

Excerpted from Manning's POJOs in Action.

and simplify the task of testing the object/ relational mapping. They provide methods for making assertions about the mapping and for verifying that it matches the database schema. For example, they make it easy to write a test that verifies that all of a class's fields are mapped to the database.

About the Author

Chris Richardson ([email protected]) is a developer and architect with over 20 years of experience. He is the author of POJOs in Action, which describes how to build enterprise Java applications with POJOs and lightweight frameworks. Chris runs a consulting company that specializes in helping companies build better software faster. He has been a technical leader at Insignia, BEA and elsewhere. Chris has a computer science degree from the University of Cambridge in England and lives in Oakland, CA. Website and blog: www.chrisrichardson.net.

Dig Deeper on Development tools for continuous software delivery

App Architecture
Software Quality
Cloud Computing
Security
SearchAWS
Close