Spring Roo provides a test
integration
command that simplifies the creation of integration tests for persistent entities. In this recipe, we'll look at how to create an integration test for an entity.
Exit the Roo shell and delete the contents of the C:
oo-cookbookch02-recipes
directory.
Execute the ch02_jsr303_fields.roo
script. It creates a flight-app
Roo project and sets up Hibernate as persistence provider using the persistence
setup
command. The script also creates a Flight
entity, which has FlightKey
as its composite primary key class, and adds fields to the Flight
and FlightKey
classes. If you are using a different database than MySQL or your connection settings are different from what is specified in the script, then modify the script accordingly.
Start the Roo shell from the C:
oo-cookbookch02-recipes
directory.
The following steps will show you how to create integration tests:
Flight
entity:roo> focus --class ~.domain.Flight
test
integration
command:~.domain.Flight> test integration Created ...FlightDataOnDemand.java Created ...FlightIntegrationTest.java Created ...FlightDataOnDemand_Roo_DataOnDemand.aj Created ...FlightIntegrationTest_Roo_IntegrationTest.aj Created ... FlightDataOnDemand_Roo_Configurable.aj Created ... Flight_Roo_Configurable.aj
The test
integration
command generates files in the src estjava
folder. The output from the test
integration
command shows that the following files are generated:
FlightDataOnDemand.java
: represents a 'data on demand' class which provides the necessary data for automated integration testing of the Flight
entity.FlightDataOnDemand_Roo_DataOnDemand.aj
: this AspectJ ITD file defines methods that are added to the FlightDataOnDemand
class during compilation. The methods defined in this AspectJ ITD file are responsible for dynamically creating 10 instances of the Flight
entity and storing them in the database—referred to as seed data. These Flight
instances are used while performing integration testing. The entity instances created by AspectJ ITD comply with the JSR 303 constraints that apply on persistent fields of the entity. By default, a transaction associated with a test method is rolled-back after the test method completes—the reason why you won't see seed data in database tables after the execution of integration tests. Refer to the Executing persistent entities tests recipe to see an example usage of Spring's @Roolback
annotation to specify that transactions associated with test methods must not be rolled-back.FlightIntegrationTest.java
: represents the JUnit integration test class for the Flight
entity.FlightIntegrationTest_Roo_IntegrationTest.aj
: AspectJ ITD responsible for defining integration testing methods for the Flight
entity.FlightDataOnDemand_Roo_Configurable.aj
and Flight_Roo_Configurable.aj
: AspectJ ITDs that add the @Configurable
annotation to FlightDataOnDemand
and Flight
classes, respectively.Let's now look at each of these files in detail.
The following listing shows the FlightDataOnDemand.java
class:
import org.springframework.roo.addon.dod.RooDataOnDemand;
import sample.roo.flightapp.domain.Flight;
@RooDataOnDemand(entity = Flight.class)
public class FlightDataOnDemand { }
The code listing shows the use of Roo's @RooDataOnDemand
annotation, which identifies the persistent entity for which the FlightDataOnDemand
class creates seed data for integration testing. The @RooDataOnDemand
annotation is responsible for the creation of the corresponding *_Roo_DataOnDemand.aj
AspectJ ITD file. The @RooDataOnDemand
accepts two attributes to customize the behavior of seed data generation:
entity
: identifies the persistent entity for which the seed data needs to be created.quantity
: the number of records to be created for the entity, default being 10. If you want to create more records for integration testing of the entity, then specify an appropriate value of this attribute.If you are using a performance testing tool like JMeter to test the performance of the JPA layer of your enterprise application, you can modify the FlightIntegrationTest
JUnit test class and use it as a JUnit Request Sampler (or you can put a wrapper around FlightIntegrationTest
and use Java Request Sampler) when creating a test plan in JMeter. This lets you quickly get started with testing the performance of your data access code. Based on the performance test requirements for a persistent entity, you can adjust the value of the quantity
attribute of the @RooDataOnDemand
annotation. For instance, if you want to test the performance of the data access layer when there are n number of records in the database, then specify the value of quantity
attribute as n.
The following listing shows the FlightDataOnDemand_Roo_DataOnDemand.aj
AspectJ ITD file, which defines methods, attributes, and annotations that are weaved into the FlightDataOnDemand.java
class at compile-time:
privileged aspect FlightDataOnDemand_Roo_DataOnDemand { declare @type: FlightDataOnDemand: @Component; private Random FlightDataOnDemand.rnd = new java.security.SecureRandom(); private List<Flight> FlightDataOnDemand.data; public Flight FlightDataOnDemand.getNewTransientFlight (int index) {...} public Flight FlightDataOnDemand.getSpecificFlight(int index) {...} public Flight FlightDataOnDemand.getRandomFlight() {...} public boolean FlightDataOnDemand.modifyFlight(Flight obj) {...} public void FlightDataOnDemand.init() {...} }
The code listing shows that AspectJ ITD does the following:
@Component
annotation to the FlightDataOnDemand
class, so that it is auto-registered with Spring's application context. This enables you to create custom integration tests in which you can autowire one or more *DataOnDemand
classes.java.security.SecureRandom
class, which is used for generating a random number.Flight
entities generated by the 'data on demand' class. These Flight
entity instances represent the seed data generated by the 'data on demand' class.getNewTransientFlight(int
index)
method for generating a unique Flight
instance based on the value of the index
argument. The getNewTransientFlight
method creates persistent entity instances which comply with the JSR 303 annotations specified for the entity's persistent fields. For instance, the Flight
entity specifies the @Size(min
=
3,
max
=
20)
JSR 303 annotation for the origin
and destination
fields (refer to the Flight.java
class); therefore, the getNewTransientFlight
method attempts (that is, it is not guaranteed, as we'll see soon) to ensure that the size of the origin
and destination
fields comply with the corresponding JSR 303 annotation.getSpecificFlight(int
index)
method, which returns the Flight
entity at the specified index
from the collection of seed data maintained by the 'data on demand' class.getRandomFlight()
method, which returns Flight
entity at a random index (obtained from the java.security.SecureRandom
instance) in the seed data collection maintained by the 'data on demand' class.modifyFlight(Flight
+obj)
method, which is supposed to modify the Flight
entity passed as argument and return the success or failure of modification. But it simply returns false
, that is, it never modifies the passed Flight
instance.init()
method, which is responsible for creating the seed data for integration testing of the Flight
entity. It creates Flight
entities in the database using the getNewTransientFlight(int
index)
method. The number of Flight
entities created in the database is determined by the value of the quantity
attribute of the @RooOnDemand
annotation.It is important to note that the init()
method is internally called by methods defined in the *_Roo_DataOnDemand.aj
to ensure that a fresh set of seed data is created in the database each time a test method is invoked.
JSR 303 annotations and seed data
The 'data on demand' classes generated by Spring Roo provide limited support for creating entity instances that comply with JSR 303 annotations. As of Spring Roo 1.1.3, it only supports @NotNull
, @Past
, and @Future
JSR 303 constraints, along with some support for maximum and minimum range annotations. If your project uses any other JSR 303 annotation, then it is recommended to create your own setter
method for entity fields in the *DataOnDemand.java
class.
The following listing shows the FlightIntegrationTest.java
class:
import org.junit.Test;
import org.springframework.roo.addon.test.RooIntegrationTest;
@RooIntegrationTest(entity = Flight.class )
public class FlightIntegrationTest {
@Test
public void testMarkerMethod() {
}
}
The code listing shows the presence of Roo's @RooIntegrationTest
annotation, which indicates that an integration test AspectJ ITD is to be created for the Flight
entity. The testMarkerMethod
is an example JUnit test method. The @RooIntegrationTest
annotation accepts an entity
attribute, which identifies the persistent entity for which the integration test is created—Flight
in case of the FlightIntegrationTest
class. Additionally, the @RooIntegrationTest
annotation defines attributes which let you control the integration test methods that are auto-generated by Roo in the corresponding *_Roo_IntegrationTest.aj
AspectJ ITD.
The following listing shows the FlightIntegrationTest_Roo_IntegrationTest.aj
AspectJ ITD file that was generated by Roo corresponding to the FlightIntegrationTest
class:
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context. junit4.SpringJUnit4ClassRunner; privileged aspect FlightIntegrationTest_Roo_IntegrationTest { declare @type: FlightIntegrationTest: @RunWith(SpringJUnit4ClassRunner.class); declare @type: FlightIntegrationTest: @ContextConfiguration(locations = "classpath:/META-INF/spring/applicationContext.xml"); declare @type: FlightIntegrationTest:@Transactional; @Autowired private FlightDataOnDemand FlightIntegrationTest.dod; @Test public void FlightIntegrationTest.testFindAllFlights() { ... } @Test public void FlightIntegrationTest.testPersist() { ... } ... }
The previous code shows that AspectJ ITD does the following:
@RunWith
annotation to the FlightIntegrationTest
class, instructing the use of Spring's SpringJUnit4ClassRunner
for running the JUnit tests.@ContextConfiguration
annotation to the FlightIntegrationTest
class, which specifies the location of Spring's application-context XML file to be used for executing the tests. By default, it is set to use the applicationContext.xml
file in the META-INFspring
folder. If you want to use a different application-context XML for running the tests, then specify the @ContextConfiguration
annotation in the FlightIntegrationTest
class.@Transactional
annotation to the FlightIntegrationTest
class, which means all the test methods defined in FlightIntegrationTest
(or weaved into it by AspectJ ITD) are transactional in nature.testFindAllFlights
, testPersist
, and so on. The test methods make use of FlightDataOnDemand
(a 'data on demand' class) instance for creating Flight
instances (the seed data) for testing the Flight
entity and for retrieving a random Flight
instance from the database.In some scenarios you may want to customize the seed data created by *DataOnDemand.java
class and to control the integration test methods that are auto-generated by Spring Roo.
If you're using JSR 303 annotations that are not supported by Spring Roo, you'll need to create custom setter methods (defined in *_Roo_DataOnDemand.aj
) for setting persistent entity field values. The following listing shows some of the setter methods (setNumOfSeats
and setOrigin
) auto-generated by Roo for the Flight
entity; the following is the FlightDataOnDemand_Roo_DataOnDemand.aj
method:
public Flight FlightDataOnDemand.getNewTransientFlight (int index) { sample.roo.flightapp.domain.Flight obj = new sample.roo.flightapp.domain.Flight(); setEmbeddedId(obj, index); setNumOfSeats(obj, index); setOrigin(obj, index); setDestination(obj, index); ... return obj; } private void FlightDataOnDemand.setEmbeddedId(Flight obj, int index) { java.lang.String flightId = "flightId_" + index; ... obj.setId(embeddedIdClass); } private void FlightDataOnDemand.setNumOfSeats(Flight obj, int index) { java.lang.Integer numOfSeats = new Integer(index); obj.setNumOfSeats(numOfSeats); } private void FlightDataOnDemand.setOrigin(Flight obj, int index) { java.lang.String origin = "origin_" + index; if (origin.length() > 20) { origin = origin.substring(0, 20); } obj.setOrigin(origin); } ...
In the code listing, there are a couple of things to notice about the Flight
entity that is created:
@NotNull
JSR 303 annotation is taken care of while creating the Flight
entity, but the @DecimalMax
and @DecimalMin
JSR 303 annotations on the numOfSeats
field are completely ignored (refer setNumOfSeats
method in the code listing).@Size
annotation on origin
and destination
fields (refer setOrigin
method in the code listing) is partially supported as the method only checks if the maximum length of the value assigned to destination
(or origin
) field is 20
. It doesn't check for the minimum length as 3
.setEmbeddedId
method in the previous listing).To address the issue with JSR 303 support in the auto-generated Flight
entity instance by FlightDataOnDemand_Roo_DataOnDemand.aj
, we can write custom setNumOfSeats
, setOrigin
, and setDestination
methods in the FlightDataOnDemand.java
class, which does the following:
origin
and destination
fields are within the limits defined by @Size
@DecimalMin
and @DecimalMax
JSR 303 annotationsIt is important to note that if you create setNumOfSeats
, setOrigin
, and setDestination
methods in the FlightDataOnDemand.java
class, then Roo removes these methods from the corresponding FlightDataOnDemand_Roo_DataOnDemand.aj
AspectJ ITD. This requires that the signature of methods in the FlightDataOnDemand.java
class is the same as the signature of methods defined in the corresponding FlightDataOnDemand_Roo_DataOnDemand.aj
AspectJ ITD file. Similarly, you can customize any method of FlightDataOnDemand_Roo_DataOnDemand.aj
by writing them in the FlightDataOnDemand.java
class.
The following figure shows how Roo removes setNumOfSeats
, setOrigin
, and setDestination
methods from the FlightDataOnDemand_Roo_DataOnDemand.aj
file:
The figure shows that Spring Roo Shell monitors the FlightDataOnDemand.java class (because it is annotated with Roo's @RooDataOnDemand
annotation). When any change is made to the FlightDataOnDemand.java class, Roo triggers add-on(s) responsible for managing the AspectJ ITD file(s) corresponding to the FlightDataOnDemand.java
class. In the case of FlightDataOnDemand
, Roo triggers Dod add-on ('data on demand') to update FlightDataOnDemand_Roo_DataOnDemand.aj AspectJ ITD, so that the AspectJ ITD is in sync with the FlightDataOnDemand.java
class. When you add setNumOfSeats
, setOrigin
, and setDestination
methods in the FlightDataOnDemand.java class, Dod add-on checks if the methods already exist there. If they exist, Dod add-on removes those methods from the FlightDataOnDemand_Roo_DataOnDemand.aj AspectJ ITD.
It is important to note that modifying a Java class that is annotated with Roo annotations may result in multiple AspectJ ITDs getting affected. For instance, if you remove the modifiedBy
field from the Flight.java
class, Roo will update the Flight_Roo_JavaBean.aj
, Flight_Roo_ToString.aj
, and FlightDataOnDemand_Roo_DataOnDemand.aj
AspectJ ITD files to reflect the removal of the modifiedBy
field.
We mentioned earlier that the @RooIntegrationTest
annotation in *IntegrationTest.java
defines attributes which let you control the integration test methods that are auto-generated by Roo in the *_Roo_IntegrationTest.aj
AspectJ ITD file. The @RooIntegrationTest
annotation defines the following attributes to control the auto-generation of integration test methods: count
, find
, findAll
, findAllMaximum
, findEntries
, flush
, merge
, persist
, and remove
. If the value of any of these attributes is specified as false
, then the corresponding test method is removed from the *_Roo_IntegrationTest.aj
AspectJ ITD file. For instance, the testFindAllFlights
method searches for all Flight
instances in the database, which may not be desirable for performance reasons. To instruct Spring Roo to remove the auto-generated testFindAllFlights
method from the FlightIntegrationTest_Roo_IntegrationTest.aj
file, all you need to do is to specify the value of the findAll
attribute value as false
in the @RooIntegrationTest
annotation, as shown here:
@RooIntegrationTest(entity = Flight.class, findAll=false)
public class FlightIntegrationTest {
...
}
In this recipe we saw how to create integration tests using the test
integration
command. You can also use the testAutomatically
argument of the entity
command to instruct Roo to create integration tests at the time of entity creation, as shown here:
roo> entity --class ~.domain.Flight --testAutomatically --identifierType ~.domain.FlightKey --table FLIGHT_TBL
As with the 'data on demand' class, you can provide a custom implementation for an integration test method in your *IntegrationTest.java
class. For instance, if you want to modify the testPersist
Roo-generated test method with a customized testPersist
method, create a testPersist
method in the FlightIntegrationTest.java
file. Adding the testPersist
method to *IntegrationTest.java
results in the removal of the testPersist
method from the corresponding *_Roo_IntegrationTest.aj
file by Roo. Similarly, you can customize any other test method defined in the *_Roo_IntegrationTest.aj
file.
3.142.114.19