Smoke test for JPA entities

JPA Object Relational Mapping (ORM) is the subject of passionate relationships (no pun intended): either love or hate. Some developers love it for the great amount of work that they don't have to manually perform, others hate it for the great amount of work that they needed to do in order to make it work... if it worked at all. Personally I have a very good relationship with the two most popular implementations, Hibernate and EclipseLink (I've mostly used the former) and a number of customers of mine are relatively happy with them. The “trick” is to acknowledge that there are three major scenarios:

  1. Projects that are not database-centric, so the design is done first at the OO level, and the database schema is created later, as a consequence. Here JPA fits well (even though the evolution process must be clearly defined), but in my experience these cases are pretty rare.
  2. Projects that are more or less heavily database-centric, with a history of discipline; that is, the database schemata have been properly designed and maintained (e.g. there are primary keys for all tables, all the relationships are properly declared, etc...). In this case there are very good chances that JPA and its related tools (such as the code generators that reverse engineer the schema) work pretty well.
  3. Projects that are more or less heavily database-centric, without a history of discipline, so the database schemata are missing a number of good practices. In these cases, JPA might be clearly unusable (e.g. there are no primary keys in some tables), or it can be early guessed that it would create relevant problems so a different solution can be searched for.

Until recently I've been involved in these kind of scenarios (mostly #2), so either I advised about using JPA (and it worked) or I immediately discouraged its use. But then I ran into a sort of intermediate case, in which it was quite difficult to tell the right story from the beginning. The database schema was pretty large, a mixed case, with older parts and newer parts; the project was going to be managed in an iterative and incremental way, and the schema wasn't supposed to be entirely used; thus more and more JPA entities where thrown into the code in a number of iterations and it wasn't clear from the beginning if the troubled ones would have been used or not.

Everything went fine at the beginning, but at a certain point a serious issue was met with a couple of related tables that triggered a problem in Hibernate. The problem could be initially detected just during the boot of Hibernate, so it was apparently a quick thing, but one of the first work-arounds happened to trigger a secondary bug, a known one but not fixed yet, involved in the generation of SQL. In other words, Hibernate didn't raise the problem during the boot, but only when the involved entities were manipulated.

Sure, we found the workaround for it too, in a few hours, but at that point some uncertainty had hit the the project leader: what if a different problem with the same unpredictability characteristics happened again? Would have always been possible to solve it? What about timing, I mean, the search of a new workaround could have severely impacted the time-frame of the project, that was a relatively tight one. In the project JPA had been pretty useful and in the end it worked fine, so we didn't feel it was the case to reject it in a possible excess of caution.

In the end I developed a quick and simple test, relying on the capability of TestNG to easily run parametrised tests, that was useful for early discovering whether new entities were triggering a problem. It would have allowed us to eventually develop some contingency plan for them. I called it the “smoke test” for JPA since it exercises the capability of performing a simple query on each entity in the project.

                    package it.tidalwave.util.jpa;

                    import java.io.IOException;
                    import java.util.ArrayList;
                    import java.util.List;
                    import javax.annotation.Nonnull;
                    import javax.persistence.Entity;
                    import javax.persistence.EntityManager;
                    import javax.persistence.EntityManagerFactory;
                    import org.springframework.context.support.ClassPathXmlApplicationContext;
                    import org.testng.annotations.AfterClass;
                    import org.testng.annotations.DataProvider;
                    import org.testng.annotations.Test;

                    public class JpaEntitiesSmokeTest 
                      {
                        private static final Logger log = LoggerFactory.getLogger(JpaEntitiesSmokeTest.class);

                        private ClassPathXmlApplicationContext context;

                        private EntityManagerFactory emf;

                        private EntityManager em;

                        @BeforeClass
                        public void setup() 
                          throws Exception 
                          {
                            log.info("Booting ORM for JPA smoke test...");
                            context = new ClassPathXmlApplicationContext(…); // your Spring configuration
                            emf = context.getBean(EntityManagerFactory.class);
                            em = emf.createEntityManager();
                            log.info("Going to test {} JPA entities...", entityNameProvider().length);
                          }

                        ... // shut down EntityManagerFactory and Spring after the test

                        @Test(dataProvider = "entityNameProvider")
                        public void must_generate_valid_SQL_for_entity (final @Nonnull Class<?> entityClass) 
                          {
                            final String sql = String.format("SELECT e FROM %s e", entityClass.getSimpleName());
                            log.info("Executing: {}", sql);

                            try 
                              {
                                em.getTransaction().begin();
                                em.createQuery(sql, entityClass).setMaxResults(5).getResultList();
                              }
                            catch (Exception e)
                              {
                                throw new RuntimeException(String.format("%s: %s", entityClass, e.getMessage()), e);
                              }
                            finally 
                              {
                                try
                                  {
                                    em.getTransaction().commit();
                                  } 
                                catch (Exception e)
                                  {
                                    log.warn("Committing:", e);
                                  }
                              }
                          }

                        @DataProvider
                        private static Object[][] entityNameProvider() 
                          {
                            final ClassScanner classScanner = new ClassScanner().withAnnotationFilter(Entity.class);
                            final List<Object[]> entities = new ArrayList<>();

                            for (final Class<?> clazz : classScanner.findClasses()) 
                              {
                                entities.add(new Object[]{clazz});
                              }

                            return entities.toArray(new Object[0][0]);
                          }
                      }
                    

I'm not giving the exact configuration of Spring since it might be different in each case, and in the end the test only needs to access the EntityManagerFactory. I've also omitted the clean up part, that in some cases could be necessary to avoid an OutOfMemoryError in a test suite with many tests using JPA.

The simple idea behind it is to scan the classpath in search for entities, using the ClassScanner facility that I illustrated in a previous post, and perform a quick SELECT e FROM Entity e for each entity. In the real world exercising further statements (such as INSERT or DELETE) might be useful. There's no expected result to assert: the smoke test passes if no error is raised. In case of a very large database, the setMaxResults(5) limit is useful to have the execution very fast. Hundreds of entities can be tested in a matter of seconds. It helped us in spotting a couple of more problems, that in the end turned out to be of the same kind of the initial one. 

Comments are managed by Disqus, which makes use of a few cookies. Please read their cookie policy for more details. If you agree to that policy, please click on the button below to accept it. If you don't, you can still enjoy this site without using Disqus cookies, but you won't be able to see and post comments. Thanks.