I want to start by asking a simple question.
"How do you unit test your JPA classes?"Now, before you answer look carefully at the question. The key words are unit test. Not test but unit test.
Conversation
In my experience, after asking this question, the conversation goes something like this.
"How do you unit test your JPA domain objects?"
"We've developed this shared project which starts an in-memory Derby database (See my previous blog article about how to do this for integration testing) and it automatically runs scripts in your project to build the database and insert data for the tests."
"Where is this shared project?"
"It's in Nexus. We pull it in as a Maven dependency."
"I see it in the POM. You know this dependency doesn't have <scope>test</scope>"
"Huh?"
"Never mind. Where's the source code for the project?"
"The person who made it doesn't work here anymore so we don't know where the source code is. But we haven't had to change it."
"Where's the documentation?"
"Umm...we just copy stuff from existing projects and change the DDLs and queries"
"Why are you starting Derby for a unit test? Unit tests must be easy to maintain and fast to run. You can't be starting any frameworks like JPA or relying on external resources like a database. They make the unit tests complicated and slow running."
"Well, the classes use JPA, you need a database to test them."
"No, you don't. You don't need to start a database. JPA relies heavily on annotations. All you need to do is make sure all the classes, fields, and getters are annotated correctly. So just unit test the annotations and the values of their properties."
"But that won't tell you if it works with the database."
"So? You are supposed to be writing simple and fast unit tests! Not integration tests! For a unit test all you need to know is if the JPA classes are annotated properly. If they're annotated properly they'll work."
"But what if the databases changes?"
"Good question, but not for a unit test. For a unit test all you need to know is that what was working before is still working properly. For frameworks like JPA that depend on annotations to work properly, your unit tests need to make sure the annotations haven't been messed around with."
"But how do you know if the annotations are right? You have to run against a database to get them right."
"Well, what if you weren't using JPA, what if you were writing the SQL manually? Would you right a unit test to connect to the a database and keep messing around with the SQL in your code until you got it right? Of course not. That would be insane! Instead, what you would do is use a tool like SQL Developer, connect to the database, and work on the query until it runs correctly. Then, after you've gotten the query correct, you'd copy and paste the query into your code. You know the query works - you just ran it in SQL Developer - so no need to connect to a database from your unit test at all. Your unit test only needs to assert that the code generates the query properly. If you are using JPA, it's fundamentally the same thing. The difference is with JPA you need to get the annotations correct. So, do the JPA work somewhere else, then, when you got it correct, copy & paste it into your project and unit test the annotations."
"But where do you do this work? Can SQL Developer help figure out JPA annotations?....Wait! I think Toad can. Do we have more licenses for that?"
"Ugh! No! You create a JPA research project which starts a JPA implementation so you can play around with the JPA annotations. In this research project, ideally you'd connect to the real project's development database, but you can actually connect to whatever database that has the data you need. Doing all this work in a research project is actually much better for the real project because you get rid of the in-memory database from the real project and you also get rid of trying to replicate your project's real database in Derby.
"Where do we get a research project like this?"
"Umm, you just create one; Right-click -> Create -> New project."
"You mean everyone has to create their own research project? Seems like a waste."
"Ugh!"If you have had a conversation similar to this, please let me know. I'd love to hear your stories.
Example
But with this all being said, how do you unit test the annotations of you JPA objects. Well it's not really that difficult. The Java reflection API give access to a classes annotations. So let's see what this might look like.
Suppose listing 1 is a Person object. This Person object is part of your domain model and is setup to be handled by JPA to persist data to the database.
Listing 1: Person and Phone Object Model
package org.thoth.jpa.UnitTesting; import java.util.ArrayList; import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.Table; /** * (JavaCodeGeeks, 2015) */ @Entity @Table(name = "T_PERSON") public class Person { private Long id; private String firstName; private String lastName; private List<Phone> phones = new ArrayList<>(); @Id @GeneratedValue() public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Column(name = "FIRST_NAME") public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } @Column(name = "LAST_NAME") public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @OneToMany(mappedBy = "person", fetch = FetchType.LAZY) public List<Phone> getPhones() { return phones; } }
The code in listing 1 is just an example, so it's very simple. In real applications, the domain objects and their relationships to other objects will get complex. But this is enough for demonstration purposes. Now, the next thing you want to do is unit test this object. Remember, the key words are unit test. You don't want to be starting any frameworks or databases. It's the annotations and their properties which make the Person object work properly, so that's what you want to unit test. Listing 2 shows what a unit test for the Person object may look like.
Listing 2: PersonTest Unit Test
package org.thoth.jpa.UnitTesting; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.Table; import org.junit.Assert; import org.junit.Test; /** * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class PersonTest { @Test public void typeAnnotations() { // assert AssertAnnotations.assertType( Person.class, Entity.class, Table.class); } @Test public void fieldAnnotations() { // assert AssertAnnotations.assertField(Person.class, "id"); AssertAnnotations.assertField(Person.class, "firstName"); AssertAnnotations.assertField(Person.class, "lastName"); AssertAnnotations.assertField(Person.class, "phones"); } @Test public void methodAnnotations() { // assert AssertAnnotations.assertMethod( Person.class, "getId", Id.class, GeneratedValue.class); AssertAnnotations.assertMethod( Person.class, "getFirstName", Column.class); AssertAnnotations.assertMethod( Person.class, "getLastName", Column.class); AssertAnnotations.assertMethod( Person.class, "getPhones", OneToMany.class); } @Test public void entity() { // setup Entity a = ReflectTool.getClassAnnotation(Person.class, Entity.class); // assert Assert.assertEquals("", a.name()); } @Test public void table() { // setup Table t = ReflectTool.getClassAnnotation(Person.class, Table.class); // assert Assert.assertEquals("T_PERSON", t.name()); } @Test public void id() { // setup GeneratedValue a = ReflectTool.getMethodAnnotation( Person.class, "getId", GeneratedValue.class); // assert Assert.assertEquals("", a.generator()); Assert.assertEquals(GenerationType.AUTO, a.strategy()); } @Test public void firstName() { // setup Column c = ReflectTool.getMethodAnnotation( Person.class, "getFirstName", Column.class); // assert Assert.assertEquals("FIRST_NAME", c.name()); } @Test public void lastName() { // setup Column c = ReflectTool.getMethodAnnotation( Person.class, "getLastName", Column.class); // assert Assert.assertEquals("LAST_NAME", c.name()); } @Test public void phones() { // setup OneToMany a = ReflectTool.getMethodAnnotation( Person.class, "getPhones", OneToMany.class); // assert Assert.assertEquals("person", a.mappedBy()); Assert.assertEquals(FetchType.LAZY, a.fetch()); } }
For this unit test, I created a couple simple helper classes: AssertAnnotations and ReflectTool since these can obviously be reused in other tests. AssertAnnotations and ReflectTool are shown in listing 3 and 4 respectively. But before moving on to these helper classes, let's look at PersonTest in more detail.
Line 19 is the #typeAnnotations method. This method asserts the annotations on the Person class itself. Line 21 calls the #assertType method and passes Person.class as the first parameter then after that the list of annotations expected on the class. It's important to note the #assertType method will check that the annotations passed to it are the only annotations on the class. In this case, Person.class must only have the Entity and Table annotations. If someone adds an annotation or removes an annotation, #assertType will throw an AssertionError.
Line 27 is the #fieldAnnotations method. This method asserts the annotations on fields of the Person class. Lines 29-32 call the #assertField method. The first parameter is Person.class. The second parameter is the name of the field. But then after that something is missing; where is the list of annotations? Well in this case there are no annotations! None of the fields in this class are annotated. By passing no annotations to the #assertField method, it will check to make sure the field has no annotations. Of course if you JPA object uses annotations on the fields instead of the getter method, then you would put in the list of expected annotations. It's important to note the #assertField method will check that the annotations passed to it are the only annotations on the field. If someone adds an annotation or removes an annotation, #assertField will throw an AssertionError.
Line 37 is the #methodAnnotations method. This method asserts the annotations on the getter methods of the Person class. Lines 39-49 call the #assertMethod method. The first parameter is Person.class. The second parameter is the name of the getter method. The remaining parameters are the expected annotations. It's important to note the #assertMethod method will check that the annotations passed to it are the only annotations on the getter. If someone adds an annotation or removes an annotation, #assertMethod will throw an AssertionError. For example, on line 40, the "getId" method must only have the Id and GeneratedValue annotations and no others.
At this point PersonTest has asserted the annotations on the class, its fields, and its getter methods. But, annotations have values too. For example, line 17 of the Person class is @Table(name = "T_PERSON"). The name of the table is vitally important to the correct operation of this JPA object so the unit test must make sure to check it.
Line 64 is the #table method. It uses the ReflectTool on Line 68 to get the Table annotation from the Person class. Then line 71 asserts the name of the table is "T_PERSON".
The rest of the unit test method in PersonTest assert the values of the annotations in the Person class. Line 83 asserts the GeneratedValue annotation has no generator and Line 84 asserts the generation type. Lines 96 and 108 assert the names of the database table columns. Lines 120-121 assert the relationship type between the Person object and the Phone object.
After looking at PersonTest in more detail, let's look at help classes: AssertAnnotations and ReflectTool. I'm not going to say anything about these classes; they aren't all that complicated.
Listing 3: AssertAnnotations helper
package org.thoth.jpa.UnitTesting; import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.List; /** * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class AssertAnnotations { private static void assertAnnotations( List<Class> annotationClasses, List<Annotation> annotations) { // length if (annotationClasses.size() != annotations.size()) { throw new AssertionError( String.format("Expected %d annotations, but found %d" , annotationClasses.size(), annotations.size() )); } // exists annotationClasses.forEach( ac -> { long cnt = annotations.stream() .filter(a -> a.annotationType().isAssignableFrom(ac)) .count(); if (cnt == 0) { throw new AssertionError( String.format("No annotation of type %s found", ac.getName()) ); } } ); } public static void assertType(Class c, Class... annotationClasses) { assertAnnotations( Arrays.asList(annotationClasses) , Arrays.asList(c.getAnnotations()) ); } public static void assertField( Class c, String fieldName, Class... annotationClasses) { try { assertAnnotations( Arrays.asList(annotationClasses) , Arrays.asList(c.getDeclaredField(fieldName).getAnnotations()) ); } catch (NoSuchFieldException nsfe) { throw new AssertionError(nsfe); } } public static void assertMethod( Class c, String getterName, Class...annotationClasses) { try { assertAnnotations( Arrays.asList(annotationClasses) , Arrays.asList(c.getDeclaredMethod(getterName).getAnnotations()) ); } catch (NoSuchMethodException nsfe) { throw new AssertionError(nsfe); } } }
Listing 4: ReflectTool helper
package org.thoth.jpa.UnitTesting; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; /** * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class ReflectTool { public static <T extends Annotation> T getMethodAnnotation( Class<?> c, String methodName, Class<T> annotation) { try { Method m = c.getDeclaredMethod(methodName); return (T)m.getAnnotation(annotation); } catch (NoSuchMethodException nsme) { throw new RuntimeException(nsme); } } public static <T extends Annotation> T getFieldAnnotation( Class<?> c, String fieldName, Class<T> annotation) { try { Field f = c.getDeclaredField(fieldName); return (T)f.getAnnotation(annotation); } catch (NoSuchFieldException nsme) { throw new RuntimeException(nsme); } } public static <T extends Annotation> T getClassAnnotation( Class<?> c, Class<T> annotation) { return (T) c.getAnnotation(annotation); } }
That's it. I hope this is helpful.
References
https://www.javacodegeeks.com/2015/02/jpa-tutorial.html#relationships_onetomany
No, not useful. What you really need to do is fix your integration testing strategy. Specifically:
ReplyDelete1. Do not use an in-memory db; use a real db, already started, already with all tables, etc. created. In a real project, the DB schema is evolved through incremental SQL scripts, not by (re-)creation from annotated entity classes.
2. A development DB should be available and up-to-date at all times (also for manual testing of the UI, etc.), so there is no need to run DDL scripts before a test run.
3. Similarly, test data should be inserted into the DB via Java code that instantiates and persists JPA entities, not through SQL code. Each (integration) test should create the data it needs.
4. Each test should run in a DB transaction that gets rolled back.
I have written literally hundreds of such integration tests; doing it right now with a PostGreSQL db and Hibernate/JPA, actually. They are simple and fast.
If you have the resources to maintain a real database vs. creating an in-memory database, then that is certainly another valid way to do integration testing. However, you are still writing integration tests, not unit tests. If you "fix your integration testing strategy" by using a real database, that doesn't turn your test into a "unit test". So I got back to my original question, "How do you unit test your JPA domain objects?"
DeleteYou don't "unit test" your JPA domain objects, you "integration test" them, that's the only way to go. In general, integration tests are much, much better than unit tests. The real problem is that, often, development teams lack the tooling and/or the expertise to effectivelly perform integration testing.
DeleteWhy wouldn't you unit test your JPA objects? It's faster and easier and ensures the configuration - which makes JPA work in the first place - hasn't been adversely changed. Even if development teams do have the time and expertise to do integration testing, that should not mean they forgo unit testing.
DeleteHi Michael
ReplyDeleteThank you for this post. It is a clean and easy approach to testing JPA Entities. I will definitely use these techniques in the future.
Nico
Thank you and good luck.
Delete