Unit testing stored procedures with Junit
Anyone who has used an automated unit testing framework such as Junit
knows just how life-changing an automated test suite can be.
Once you've experienced validating that recent changes have not
broken old code, or discovering subtle bugs via junit that would
otherwise have remained undetected with the press of a button,
you naturally want to have this capability in all your programming
environments.
Guisseppe Maxia has
written a few stored procedure snippets to assist with automated unit
testing of MySQL routines. Unfortunately, the MySQL stored
procedure language itself does not have the necessary abilities to
fully implement the sort of unit testing we would like. In
particular, the inability for a stored procedure to capture the result
sets generated by another stored procedure prevents a stored procedure
from fully unit testing another.
So I decided that - for me - Junit offered the best solution. I
created an extension to the Junit class that contains some assertions
useful when unit testing stored procedure and extended my PlainSql JDBC
wrapper classes to allow a stored procedure to return a single object
that contains all its result sets and the values of OUT or INOUT
parameters.
If you're not familiar with Java and/or, you might feel that this
solution is not for you. However, the amount of java programming
you need to do is very minimal and GUI environments such as Eclipse
make it very easy to set up.
Writing a stored procedure test case
The procedure is pretty similar to a standard Junit test case, except that instead of extending junit.framwork.TestCase, you extend com.syncsoft.plainsql.SpTestCase. You need a global PSconnection object (from my JDBC wrappers), so declare that as a private class variable:
import java.sql.Connection;
import java.sql.DriverManager;
import com.syncsoft.plainsql.PSconnection;
import com.syncsoft.plainsql.ProcedureResult;
import com.syncsoft.plainsql.RowSet;
import com.syncsoft.plainsql.SpTestCase;
public class SakilaTest extends SpTestCase {
PSconnection conn;
You will almost certainly want to create a setUp() method to create your connection and probably a tearDown()
method that can rollback any changes you've made. Most database
testing requires that the data be consistent between runs so you'll
often want to perform tests that always undo any changes they might
make:
protected void setUp() throws Exception {
super.setUp();
Class.forName("com.mysql.jdbc.Driver").newInstance();
Connection myConnection = DriverManager
.getConnection("jdbc:mysql://localhost:3306/sakila?user=root&password=secret");
conn = new PSconnection(myConnection);
}
protected void tearDown() throws Exception {
super.tearDown();
conn.rollback();
}
Now you can write a test. Consider the stored procedure here (customer_report.sql) that runs a report against the Sakila sample database .
The procedure takes 3 arguments - 2 of them OUT parameters - and
returns 2 result sets. We want to make sure that when it's run it
returns the correct number of result sets with the correct column names
and the expected data. For each unit test, we simply create a
method starting with the string "test":
public void testCustomerReport() throws Exception {
We start off by creating some variables representing the values of the
parameters to the procedure. We don't have to worry too much
about OUT variables here, since the values of out variables will end up
in the ProcedureResult object.
int activeRentals = 0;
int totalRentals = 0;
int customerId = 431; // A "known" customer id
We execute the stored procedure and create a ProcedureResults object called results, by using the executeProc method of the PSconnection class. We supply the input and output variables together with the name of the stored procedure:
ProcedureResult results = conn.executeProc("customer_report", 431,
activeRentals, totalRentals);
Everything retunred by the stored procedure call, including values of
OUT or INOUT parameter and all of the result sets are contained within
the results object. Various assertions allow us to
fail the test if the number of result sets are not as expected, if
column names are not found, or if expected data values do not exist in
the results. We can also check the values of the OUT parameters:
assertRowSetCount("Should be two result sets", results, 2);
assertColumnCount("First rowset should have 3 columns", results, 1, 3);
assertHasColumn("First rowset should contain column firstname",
results, 1, "first_name");
assertRowCount("First rowset should have exactly one row", results, 1,
1);
assertHasColumnValue("First result firstname should equal Joel",
results, 1, "first_name", "JOEL");
assertColumnCount("Second result set should have 4 columns", results,
2, 4);
assertHasColumn("Second result should include column 'title'", results,
2, "title");
assertParameterValue("2nd parameter should return 1", results, 2, 1);
assertParameterValue("3rd parameter should return 23", results, 3, 23);
}
Some of these checks - the number and names of columns for instance -
will be independent of the state of the database, while others will be
dependent of values in database tables. Generally, it's
wise to somehow reset your database before preforming unit tests that
rely on data state.
Unit testing the database state
Another procedure (return_film) updates a
rental record when a video is returned. To write a test for this,
we probably want to directly check the contents of the database before
and after the procedure is executed. This test does that, again
using the PlainSql classes:
public void testReturnVideo() throws Exception {
int CustomerId = 431; // A "known" customer id
int RentalId = 13587; // A "known" rental id
String sql = "SELECT * FROM rental " + " WHERE customer_id=?"
+ " AND rental_id=?" + " AND
return_date IS NULL";
// Should be a single film out on loan prior to update
RowSet r1 = conn.fetchAll(sql, CustomerId, RentalId);
conn.getLogger().info(r1.toString());
assertRowCount("Should be a single row before video return", r1, 1);
// Return the film
conn.executeProc("return_film", CustomerId, RentalId);
// Now there should be no film out on loan
RowSet r2 = conn.fetchAll(sql, CustomerId, RentalId);
assertRowCount("Should be no rows after video return", r2, 0);
}
Junit has a built-in IDE for running tests and observing the
results, but I always use Junit in Eclipse. In Eclipse, you
get a interface like the one below showing you which tests passed and
which succeeded and a stack trace sho2ing where the failures occured.
Note the message from the assertion failure indicating that after
returning our film there are still rows being returned for that
customer/rental with a null RETURN_DATE. There must be something
wrong either with the data or the procedure (in this case I simply
modified the assertion to force it to fail):

The full text for the Salika testunit example is here.
Automated testing for databases
At the MySQL users conference, Scott Ambler
challanged us to adopt more agile techniques for data modeling and
database maintenance. In particular, he asked how many had
automated regression tests for the database - very few of us did.
While it may seem awkward to use the Java Junit framework to
construct tests for MySQL database and or stored proceudres, it
is far better to use whatever tools are available rather than to
avoid automated database testing altogether. Junit certainly has
the neccessary tools to perform automated testing of stored procedures
and many of these routines could be used to test the database in
general. I'm definitely going to make it my habit to contsturct
unit tests for new stored procedures from now on.
My friend and colleague Steve Feuerstein
has developed a fairly sophisticated unit testing framework for Oracle
stored procedures - you can find out more about that here.
References
There's a good article on getting started with Junit here.
The JavaDoc for my PlainSql classes.
JavaDoc for the SpUnitTest class (lists all of the exceptions you can call).
A zip containing the PlainSql classes, the particular Connector/J jar and other dependencies
Requirements
This particular implementation has some pretty strict version requirements:
JDK 1.5
Junit. I'm using 3.8.1
MySQL Connector/J version 3.1.2+. I needed the ParameterMetaData
class to be working, which meant that I had to pick up a recent 3.1
snapshot
My PlainSql classes and supporting files.
Everything you need other than the JDK and Junit is in this zip file.