Easy and declarative way to execute sql queries in JUnit tests.
Introduction
JUnit test structure follows to test case model:
PreConditions
The actions that set system in partucular state required for performing test case.Test case
The actions that change system state in order to compare system behavior with expected result.PostConditions
The actions that set system into origin state, namely state before PreConditions.
JUnit provides correspond annotations to test case model:
- PreConditions =
@BeforeEach
- Test case =
@Test
- PostConditions =
@AfterEach
This structure in Java code:
Imagine, we need to test some back-end application that connect to relation database management system (RDBMS) like Postgresql. And we need to insert some values in database before start execute testCase()
method:
Developer have to write some code in such case in order to implement execution of sql query against RDBMS. And we have to reuse this code in methods marked by@BeforeEach
and @AfterEach
annotations.
Such approach has following drawbacks:
- requires additional effort to implement execution of sql queries to database
- requires to test new sql query executor
- difficult to reuse code in other projects
And there is one more drawback that breaks all efforts to implement solution described above. Let’s see…
What is the problem?
Let’s add new testCase:
Methods setUp()
and setUp2()
would be executed for both test methods: testCase()
and testCase2()
.
Why?
So, it is design of JUnit. There is no information about which @BeforeEach
method correspond to which @Test
method. And JUnit executes all methods marked by @BeforeEach
before executing each
test method.
Point to notice:
JUnit provides TestInfo object. You can put this object as method arguments into setUp() and setUp2() and then use if statement in order to execute sql queries depends on test method name.
How to deal with it?
JUnit provides only one capability: enclose each testCase()
method by inner class:
Such approach solves issue with execution corresponding @BeforeEach
and @AfterEach
for particular test method. But it brings complexity into code especially to work with test class dependencies and also increase efforts and time to support code base.
Motivation
With JUnit out of box functionality we have a lot of issues to implement test cases with executing sql queries at PreConditions and PostConditions stages.
And there is handy solution for all this issues:
DbChange goals
- Provide rich API to code sql queries that are executed in tests written on JUnit 5.
- Simplify sql queries maintaining in code base.
- Provide library independent of various frameworks (Uses only standard Java library and Junit 5 compile dependency)
DbChange core concept
There are three annotations:
- DbChange
- DbChangeOnce
- SqlExecutorGetter
DbChange
Provide meta information about RDBMS changes before/after each test execution in class.
DbChangeOnce
Provide meta information about RDBMS changes before/after all tests execution in class.
SqlExecutorGetter
Set default sql executor for all tests in class. Value in this annotation should be the name of public method in test class that returns instance of DefaultSqlExecutor
.
Annotations position in code:
How to plug library into project
Gradle
- Open to edit
build.gradle.kts
(orbuild.gradle
for groovy) - Add
Dbchange
dependency to project (example uses Kotlin)
Maven
- Open to edit your project
pom.xml
- Add
Dbchange
dependency to dependecies section
How to use DbChange
- (mandatory) Put
@ExtendWith(DbChangeExtension.class)
on test class. - (mandatory) Create public method in test class that returns instance of
DefaultSqlExecutor
. - (optional) Put
@DbChangeOnce
on test class - (optional) Put
@SqlExecutorGetter
on test class - (optional) Put
@DbChange
on test method
Points to notice:
- If there are no annotations
@DbChangeOnce
or@DbChange
in test class then Dbchange library does nothing during test execution. - If
@SqlExecutorGetter
is not present on test class then it is mandatory to set valuesqlExecutorGetter
in each@DbChangeOnce
and@DbChange
annotations. - If you use
@DbChangeOnce
in test then you have to initialize instance of DataSource class at test class constructor or in static context (for example, using JUnit annotation@BeforeAll
)
Here is simple example of DbChange usage:
DbChange workflow
Dbchange has following workflow:
- Gather information from
@DbChange
or@DbChangeOnce
annotation. - Generate sql queries with named JDBC parameters.
- Send sql queries and its parameters to sql query executor.
- Sql query executor send sql queries and its parameters to RDBMS via JDBC driver.
DbChange has multiply sources of changes in RDBMS. And this sources are called sql queries suppliers.
Sql queries suppliers
DbChange executes sql queries that you provide in annotations @DbChange
or @DbChangeOnce
, namely in sql query supplier values.
There are following sql queries suppliers:
- statements
- sql query files
- changeset
- sql query getter
- using JUnit
@MethodSoure
(only for JUnit parameterized test)
Point to notice:
all sql queries suppliers (except
@MethodSource
) are supported by@DbChange
and@DbChangeOnce
annotations.
Let’s see each sql query supplier in details.
Statements
This annotation value provides capability to set inline sql query statements.
Pros:
- Easy to use
- Use sql query explicitly
- Declarative way to execute sql query
Cons:
- Difficult to reuse sql query in other tests
- Difficult to read Java code if there are a lot of sql statements
- Difficult to customize sql query with parameters
- Difficult to understand what values are exactly used in particular test
- Requires a lot of boring actions if needs to change all statements in all tests
Sql query files
This annotation value provides capability to set sql file name with sql statements. This sql query supplier is really useful to unite multiply sql statements into one file.
Pros:
- Easy to use
- It is more easier way to read Java code in comparision to statements value
Cons:
- Difficult to reuse sql query in other tests
- Difficult to customize sql query with parametes
- Difficult to understand what values are exactly used in particular test
- Requires a lot of boring actions if needs to change all statements in all tests
Changeset
This annotation provides capability to set array of class that implement com.github.darrmirr.dbchange.sql.query.SqlQuery
interface. Here is usage example:
How does it works?
Annotation value expect array of classes that that implement com.github.darrmirr.dbchange.sql.query.SqlQuery
interface. Here is interface definition:
It is quite simple interface that returns sql query as Java String object.
DbChange provides some predefined implementation of SqlQuery
interface:
- TemplateSqlQuery
- EmptyTemplateSqlQuery
- InsertSqlQuery
- SpecificTemplateSqlQuery
All this classes dedicated to provide sql query with JDBC query parameters. Such approach gives opportunity to reuse code and customize sql query.
Point to notice:
It is recommend to use InsertSqlQuery or SpecificTemplateSqlQuery.
It is not forbidden to use TemplateSqlQuery and EmptyTemplateSqlQuery but they are mostly used for internal needs.
InsertSqlQuery
This class extends TemplateSqlQuery
one and provides capability to create SQL query template depends on parameter names and table name.
Class InsertSqlQuery
is abstract one and you have to extend it in order to use:
DbChange generates sql query according to InsertEmploee7
class definition. Here is example of generated sql query:
Then DbChange follows to its workflow. It has described above.
SpecificTemplateSqlQuery
This class also extends TemplateSqlQuery
as InsertSqlQuery
one but it has different purpose. This class dedicated to reuse TemplateSqlQuery
and override sql query parameters that only needed for particular test. Here is example:
Method commonTemplateSqlQuery()
contains instance of TemplateSqlQuery
class. This object will be get as a base for sql query creation. It means that sql query template and parameters will be get from this object. But SpecificTemplateSqlQuery
brings to us opportunity to override or add new query parameters. And specificParameters()
method dedicated for this purpose. Let’s see on common template class:
There are 5 query parameters in InsertEmployeeCommon
class. But class InsertEmployee5
overrides only 3 of them via specificParameters()
method.
So, SpecificTemplateSqlQuery
class brings great opportunity to reuse code and simplify adding new one and changing already existed classes.
Let’s sum up pros and cons of using changeset sql query supplier.
Pros:
- Capability to reuse code for generating sql queries
- It is more easier to support code compare with text files and Java strings.
- Easy to develop and navigate through the code using IDE
- Capability to set only needed parameters for test using by
SpecificTemplateSqlQuery
- There is no need to hard code insert sql query string in code.
InsertSqlQuery
class generates sql query for you.
Cons:
- Requires to create separate class as file for each sql query
- Requires default constructor in query class
Sql query getter
Changeset sql query supplier has a lot of advantanges, but it also has some disadvantages. And sql query getter aimed to provide dependency injection capability for classes that implement SqlQuery
interface and get rid of creation of separate classes for each sql query.
There is interface SqlQueryGetter
in DbChange. Here is its definition:
This interface supply list of objects that implement SqlQuery
interface. So, let’s take a look how to use it SqlQueryGetter
:
TemplateSqlQuery
and InsertSqlQuery
classes implement pattern Builder. It gives us declaritive way to create instance of this classes without its creation expicitly as separate file. Also you can create static class or use anonymous class and then use any dependencies that you injected or created in test class. And finally, you can use plain Java strings for supplying sql query.
Pros:
- Includes all pros from changeset sql supplier
- There is no default sql constructor requirement
- There is no requirement to create separate class as file for sql query
Cons:
- Requires to create additional methods in test class
DbChange and parameterized Test
DbChange also provides capability to execute sql queries for parameterized test. It means you can define individual sql queries for each test during parameterized test execution.
DbChange supports only @MethodSource
as source of changes to database. Let’s see code snippet:
First, there is no @DbChange
annotation in parameterized test. You can put it on method and then the same sql queries from this annotation would be executed for each test during parameterized test execution.
Second, you must put List<DbChangeMeta> dbChangeMetas
in method arguments. It is mandatory due to JUnit internal logic.
What is DbChangeMeta?
DbChangeMeta is class in DbChange JUnit extension. Internally, DbChange covert all information from @DbChange
and @DbChangeOnce
into instances of DbChangeMeta
at first step of its workflow. It developed in such way in order to simplify DbChange code base. Usually, extension user works only with @DbChange
and @DbChangeOnce
annotation. But there is one exception from this rule. It is parameterized test.
DbChange expects that list of DbChangeMeta
will be provided at one of test method arguments. If such item is absent in method arguments then DbChange do nothing.
DbChangeMeta
has the same structure as @DbChange
and @DbChangeOnce
annotations. And all rules of using sql query suppliers are applyed for DbChangeMeta
too.
Chained sql queries
Let’s see on example:
It is not obvious from this code that InsertEmployee5.class
depends on InsertOccupation3.class
and InsertDepartment9.class
. And if we change order, for example put InsertEmployee5.class
at the top of changeSet list, then test execution finished with exception. It happens because there is no yet department and occupation in database that employee belongs to.
DbChange provides capability to chain such sql queries and execute it in proper order. There is interface dedicated for this:
Point to notice:
Such capability available only for changeset and sqlQueryGetter sql query suppliers.
ChainedSqlQuery
interface is quite simple. It has only one method next()
. Let’s have a look how to use it:
There are a lot of code lines at first glance. Let’s go through it.
InsertEmployee5Chained
class is top level one and it extends SpecificTemplateSqlQuery
class. This class reuses InsertDepartmentCommon
and overrides one parameter at insert department sql query.
It maybe looks strange that top level class has name InsertEmployee5
, but it contains information about department insertion. It is ok because employee could not be created without department according to example business domain. Therefore, we have to insert this values first because employee depends of it.
Also InsertEmployee5Chained
class implements ChainedSqlQuery
interface. And method next()
point to next SqlQuery that must be executed after current one. So, next sql query is InsertOccupation3
.
InsertOccupation3
class also implements ChainedSqlQuery
interface. And next sql query is InsertEmployee5
.
So, sql query chain consist of three points:
insert department -> insert occupation -> insert employee.
Point to notice
You can chain queries as many as you need. Chain size is restricted only by thread stack size of your Java Virtual Machine (JVM)
And finally, let’s change code in @DbChange
annotation:
As you can see, there is only one class in changeSet list instead of three ones in previous version of this test.
DbChange Execution Phase
You maybe notice value executionPhase
at @DbChange
or @DbChangeOnce
annotations in examples of this article.
Execution phase describes time when sql query should be executed during test execution. DbChange execution phases correspond to JUnit ones.
From my point of view, names of phase are self-explanatory. But please read JUnit official documentation if names are not clear for you.
SqlExecutorGetter
Usually, application has one DataSource. But sometimes application could have several DataSources. DbChange provides sqlExecutorGetter
value for this case. This value contains method name in test class that returns instance of com.github.darrmirr.dbchange.sql.executor.SqlExecutor
class.
Let’s see on example:
DbChange gets SqlExecutor from datasource2SqlExecutor
method for InsertBankList.class
and DeleteBankList.class
. DbChange uses SqlExecutor from defaultSqlExecutor
method if sqlExecutorGetter
value is empty in @DbChange
annotation. Such behavior is the same for @DbChangeOnce
annotation.
Point to notice
If
@SqlExecutorGetter
is not present on test class then it is mandatory to set valuesqlExecutorGetter
in each@DbChangeOnce
and@DbChange
annotations.
Conclusion
DbChange is JUnit 5 extension that provides capability in declarative way to set sql queries and execute it in PreConditions and PostConditions stages.
DbChange repository is available on Github.com.
See usage examples in com.github.darrmirr.dbchange.component.DbChangeUsageTest
class.