- Introspecting my wandering mind - https://rajandesai.com/blog -

How do you unit test your Data Access Layer?

Mock is not an option, I believe.

When I unit test my Data Access Layer (DAL), I really want to hit the database to ensure that I am executing correct SQL Queries against correct database schema. I have been using embedded (in-memory) databases like HSQL, H2 and Derby to unit test my Data Access Layer code.

Thanks to Spring 3 [1], embedding databases in my unit tests is extremely straightforward. I just have to include the following lines in my application context:

	<jdbc:embedded-database id="dataSource">
    	<jdbc:script location="classpath:schema.sql"/>
    	<jdbc:script location="classpath:test-data.sql"/>
	</jdbc:embedded-database>

There are couple of problems with this approach though:

I can use Hibernate to generate and populate database specific schema automatically. But what if I am not using Hibernate (like in my recent project)? Ideally, I would like to solve these issues by using the same database that I have in my Production environment – MySQL. A Java utility called MySQL Connector/MXJ can be used to address these problems.

What is MySQL Connector/MXJ?

explained here [2]) or use the com.mysql.management.MysqldResource class to programmatically control the MySQL server instance (explained here [3]). MySQL Connector/MXJ launches appropriate binary by detecting the platform it is running on. This post describes how I use Connector/MXJ component to unit test my DAL.

Where do I begin?

I use Maven-2 to manage component dependencies in my projects. Unfortunately, MySQL Connector/MXJ jars are not available in any central Maven-2 repositories. I manually installed required jar files in my local maven repository (~/.m2) as explained below:

  1. Download MySQL Connector/MXJ (link [4]). I am currently using Connector/MXJ version 5.0.12. According to the documentation [5], 5.0.12 version on Connector/MXJ supports the 5.5.9 version of MySQL.
  2. Unzip downloaded file in a temporary directory. I got this after exploding mysql-connector-mxj-gpl-5-0-12.zip:
    Content of downloaded Connector/MXJ archive
  3. Install necessary Connector/MXJ jars in Maven. I had to manually install the following two files in my local maven repository:
    • mysql-connector-mxj-gpl-5-0-12.jar
    • mysql-connector-mxj-gpl-5-0-12-db-files.jar – This jar contains platform specific binaries.

    Run mvn install:install-file to install these file locally.

    			mvn install:install-file -Dfile=mysql-connector-mxj-gpl-5-0-12.jar -DgroupId=com.mysql -DartifactId=connector.mxj -Dversion=5.0.12 -Dpackaging=jar -DgeneratePom=true -DupdateReleaseInfo=true
    		
    			mvn install:install-file -Dfile=mysql-connector-mxj-gpl-5-0-12-db-files.jar -DgroupId=com.mysql -DartifactId=connector.mxj.dbfiles -Dversion=5.0.12 -Dpackaging=jar -DgeneratePom=true -DupdateReleaseInfo=true
    		

Now I am ready to embed MySQL in my project.

Configure pom.xml

First I need to add Connector/MXJ dependencies to the pom.xml file.

	<dependency>
    	<groupId>com.mysql</groupId>
    	<artifactId>connector.mxj</artifactId>
    	<version>5.0.12</version>
	</dependency>
	<dependency>
    	<groupId>com.mysql</groupId>
    	<artifactId>connector.mxj.dbfiles</artifactId>
    	<version>5.0.12</version>
	</dependency>

I also need to include MySQL Connector/J JDBC driver in my pom.xml file.

	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
		<version>5.1.13</version>
		<type>jar</type>
	</dependency>

Implement a Manager to manage Embedded MySQL Database

EmbeddedMysqlManager uses com.mysql.management.MysqldResource to embed MySQL database in my unit tests. The MysqldResource provides methods to control the lifecycle of the MySQL database.

The following code snippet explains how MySQL instance can be started programatically. new MysqldResource(databaseDir) constructs a MysqldResource instance by setting the base database directory where MySQL files are deployed. The standard output and errors are directed to System.out and System.err.

mysqldResource.start("embedded-mysqld-thread-" + System.currentTimeMillis(), database_options) call deploys and starts the MySQL database. The first parameter specifies the name of the thread that runs the MySQL executable. The second parameter specifies a map of command line arguments passed to the MySQL executable.

mysqldResource.isRunning() returns true if the MySQL is running.

File databaseDir = new File(new File(baseDatabaseDir), databaseName);

mysqldResource = new MysqldResource(databaseDir);

Map<String, String> database_options = new HashMap<String, String>();
database_options.put(MysqldResourceI.PORT, Integer.toString(port));
database_options.put(MysqldResourceI.INITIALIZE_USER, "true");
database_options.put(MysqldResourceI.INITIALIZE_USER_NAME, username);
database_options.put(MysqldResourceI.INITIALIZE_PASSWORD, password);

mysqldResource.start("embedded-mysqld-thread-" + System.currentTimeMillis(), 
					 database_options);

if (!mysqldResource.isRunning()) {
    throw new RuntimeException("MySQL did not start.");
}

The complete implementation of EmbeddedMysqlManager is shown below:

 
package com.rajandesai.example.testing.util;

import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.test.jdbc.SimpleJdbcTestUtils;

import com.mysql.management.MysqldResource;
import com.mysql.management.MysqldResourceI;

/**
 * Manages the life cycle of the embedded MySQL database. 
 * This manager uses the MySQL Connector/MXJ and Mysql Connector/J JDBC driver to manage the
 * embedded MySQL DB instance.
 * 
 * @see http://dev.mysql.com/doc/refman/5.1/en/connector-mxj.html for more details
 */
public class EmbeddedMysqlManager {
	protected Logger logger = LoggerFactory.getLogger(this.getClass());
	
	private MysqldResource mysqldResource;
	private String baseDatabaseDir = System.getProperty("java.io.tmpdir");
	private String databaseName = "test_db_" + System.nanoTime();
	private int port = 13306;
	private String username = "root";
	private String password = "password";
	private List<String> sqlScripts = new ArrayList<String>();
	private DriverManagerDataSource datasource;
	
	//-------------------------------------------------------------------------
	/**
	 * Starts the mysql database
	 * @return
	 */
    public void startDatabase() {
        if (logger.isDebugEnabled()) {
        	logger.debug("=============== Starting Embedded MySQL using these parameters ===============");
        	logger.debug("baseDatabaseDir : " + baseDatabaseDir);
        	logger.debug("databaseName : " + databaseName);
        	logger.debug("host : localhost (hardcoded)");
        	logger.debug("port : " + port);
        	logger.debug("username : " + username);
        	logger.debug("password : " + password);
        	logger.debug("=============================================================================");
        }
        
        File databaseDir = new File(new File(baseDatabaseDir), databaseName);
        
        mysqldResource = new MysqldResource(databaseDir);

        Map<String, String> database_options = new HashMap<String, String>();
        database_options.put(MysqldResourceI.PORT, Integer.toString(port));
        database_options.put(MysqldResourceI.INITIALIZE_USER, "true");
        database_options.put(MysqldResourceI.INITIALIZE_USER_NAME, username);
        database_options.put(MysqldResourceI.INITIALIZE_PASSWORD, password);

        mysqldResource.start("embedded-mysqld-thread-" + System.currentTimeMillis(), 
        					 database_options);

        if (!mysqldResource.isRunning()) {
            throw new RuntimeException("MySQL did not start.");
        }

        logger.info("MySQL started successfully @ " + System.currentTimeMillis());
        
        try {
	        if (!sqlScripts.isEmpty()) {
	        	SimpleJdbcTemplate simpleJdbcTemp = new SimpleJdbcTemplate(getDatasource());
	        	logger.info("Executing scripts...");
	        
		        for (String script : sqlScripts) {
		        	logger.info("Executing script [" + script + "]");
		        	SimpleJdbcTestUtils.executeSqlScript(simpleJdbcTemp, new ClassPathResource(script), false);
		        }
	        } else {
	        	logger.info("No scripts to load...");
	        }
        } catch (Exception e) {
        	logger.error(e.getMessage(), e);
        	if ((mysqldResource != null) && (mysqldResource.isRunning())) {
        		shutdownDatabase();
        	}
        }
    }

	public void shutdownDatabase() {
		mysqldResource.shutdown();
		if (mysqldResource.isRunning() == false) {
	        logger.info(">>>>>>>>>> DELETING MYSQL BASE DIR [" + mysqldResource.getBaseDir() + "] <<<<<<<<<<");
	        try {
				FileUtils.forceDelete(mysqldResource.getBaseDir());
			} catch (IOException e) {
				logger.error(e.getMessage(), e);
			}
		}
	}

	/**
	 * Gets a {@link DataSource} for the embedded DB managed by this manager
	 * @return
	 */
	public DataSource getDatasource() {
		if (!mysqldResource.isRunning()) {
			logger.error("MySQL instance not found... Terminating");
			throw new RuntimeException("Cannot get Datasource, MySQL instance not started.");
		}
		
		if (datasource == null) {
			datasource = new DriverManagerDataSource();
			datasource.setDriverClassName("com.mysql.jdbc.Driver");
	        String url = "jdbc:mysql://localhost:" + port + "/" + databaseName + 
	        			 "?" + "createDatabaseIfNotExist=true&sessionVariables=FOREIGN_KEY_CHECKS=0";
	
	        datasource.setUrl(url);
	        datasource.setUsername(username);
	        datasource.setPassword(password);
		}        
        return datasource;
	}
	
	//-------------------------------------------------------------------------
	/**
	 * @return the baseDatabaseDir
	 */
	public final String getBaseDatabaseDir() {
		return baseDatabaseDir;
	}

	/**
	 * @param baseDatabaseDir the baseDatabaseDir to set
	 */
	public final void setBaseDatabaseDir(String baseDatabaseDir) {
		this.baseDatabaseDir = baseDatabaseDir;
	}

	/**
	 * @return the databaseName
	 */
	public final String getDatabaseName() {
		return databaseName;
	}

	/**
	 * @param databaseName the databaseName to set
	 */
	public final void setDatabaseName(String databaseName) {
		this.databaseName = databaseName;
	}

	/**
	 * @return the port
	 */
	public final int getPort() {
		return port;
	}

	/**
	 * @param port the port to set
	 */
	public final void setPort(int port) {
		this.port = port;
	}

	/**
	 * @return the username
	 */
	public final String getUsername() {
		return username;
	}

	/**
	 * @param username the username to set
	 */
	public final void setUsername(String username) {
		this.username = username;
	}

	/**
	 * @return the password
	 */
	public final String getPassword() {
		return password;
	}

	/**
	 * @param password the password to set
	 */
	public final void setPassword(String password) {
		this.password = password;
	}

	/**
	 * @return the sqlScripts
	 */
	public final List<String> getSqlScripts() {
		return sqlScripts;
	}

	/**
	 * @param sqlScripts the sqlScripts to set
	 */
	public final void setSqlScripts(List<String> sqlScripts) {
		this.sqlScripts = sqlScripts;
	}		
}

Wire everything together using Spring

Define EmbeddedMysqlManager bean in the Spring application context as shown below:

<!-- ====================================================================== -->
<!-- Test DB setup using Embedded MySQL							-->
<!-- Note :  DO NOT include "-" in the DB name. 							-->
<!-- For example, test-db is INVALID, use test_db instead 					-->
<!-- ====================================================================== -->
<bean id="embeddedMysqlManager" 
	  class="com.rajandesai.example.testing.util.EmbeddedMysqlManager"
	  p:baseDatabaseDir="#{systemProperties['java.io.tmpdir']}"		
	  p:databaseName="test_db"  	
	  p:username="root"
	  p:password="password"
	  p:port="13306"
	  init-method="startDatabase"
	  destroy-method="shutdownDatabase">
	<property name="sqlScripts">
		<list>
			<value>schema.sql</value>
			<value>data.sql</value>
		</list>
	</property>
</bean>	

This setup ensures that the test database (test_db) is initialized correctly as the Spring Framework loads the application context. The schema.sql and data.sql scripts are executed as part of the manager initialization process. As names suggest, the schema.sql script creates the database schema and the data.sql script inserts
sample data in the database.

Test the setup

Finally, the following unit test illustrates how everything works together. This simple test validates the count of employees from the embedded database.

package com.rajandesai.example.testing.dal;

import static org.junit.Assert.assertTrue;
import junit.framework.TestCase;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:applicationContext-test.xml"})
public class AppTest extends TestCase{
	@Autowired
	private SimpleJdbcTemplate jdbcTemplate;

	@Test
	public void testApp() {
        int rowCount = jdbcTemplate.queryForInt("select count(*) from employees");
        assertTrue(rowCount == 10);
    }
}

Running the above unit test from command prompt produces this output:

[testing.dal]> mvn test
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building testing.dal
[INFO]    task-segment: [test]
[INFO] ------------------------------------------------------------------------
[INFO] [resources:resources {execution: default-resources}]
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory testing.dal/src/main/resources
[INFO] [compiler:compile {execution: default-compile}]
[INFO] Nothing to compile - all classes are up to date
[INFO] [resources:testResources {execution: default-testResources}]
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 4 resources
[INFO] [compiler:testCompile {execution: default-testCompile}]
[INFO] Nothing to compile - all classes are up to date
[INFO] [surefire:test {execution: default-test}]
[INFO] Surefire report directory: testing.dal/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.rajandesai.example.testing.dal.AppTest
DEBUG [main] EmbeddedMysqlManager.startDatabase(136) | =============== Starting Embedded MySQL using these parameters ===============
DEBUG [main] EmbeddedMysqlManager.startDatabase(137) | baseDatabaseDir : /var/folders/b8/_yd9n9kj56z30jx6xp2gqjj80000gp/T/
DEBUG [main] EmbeddedMysqlManager.startDatabase(138) | databaseName : test_db
DEBUG [main] EmbeddedMysqlManager.startDatabase(139) | host : localhost (hardcoded)
DEBUG [main] EmbeddedMysqlManager.startDatabase(140) | port : 13306
DEBUG [main] EmbeddedMysqlManager.startDatabase(141) | username : root
DEBUG [main] EmbeddedMysqlManager.startDatabase(142) | password : password
DEBUG [main] EmbeddedMysqlManager.startDatabase(143) | =============================================================================
[/private/var/folders/b8/_yd9n9kj56z30jx6xp2gqjj80000gp/T/test_db/bin/mysqld][--no-defaults][--port=13306][--socket=mysql.sock][--basedir=/private/var/folders/b8/_yd9n9kj56z30jx6xp2gqjj80000gp/T/test_db][--datadir=/private/var/folders/b8/_yd9n9kj56z30jx6xp2gqjj80000gp/T/test_db/data][--pid-file=/private/var/folders/b8/_yd9n9kj56z30jx6xp2gqjj80000gp/T/test_db/data/MysqldResource.pid]
[MysqldResource] launching mysqld (embedded-mysqld-thread-1318513795413)
111013  9:49:56 [Warning] Setting lower_case_table_names=2 because file system for /private/var/folders/b8/_yd9n9kj56z30jx6xp2gqjj80000gp/T/test_db/data/ is case insensitive
111013  9:49:56 [Note] Plugin 'FEDERATED' is disabled.
111013  9:49:56 InnoDB: The InnoDB memory heap is disabled
111013  9:49:56 InnoDB: Mutexes and rw_locks use InnoDB's own implementation
111013  9:49:56 InnoDB: Compressed tables use zlib 1.2.3
111013  9:49:56 InnoDB: Initializing buffer pool, size = 128.0M
111013  9:49:56 InnoDB: Completed initialization of buffer pool
InnoDB: The first specified data file ./ibdata1 did not exist:
InnoDB: a new database to be created!
111013  9:49:56  InnoDB: Setting file ./ibdata1 size to 10 MB
InnoDB: Database physically writes the file full: wait...
111013  9:49:57  InnoDB: Log file ./ib_logfile0 did not exist: new to be created
InnoDB: Setting log file ./ib_logfile0 size to 5 MB
InnoDB: Database physically writes the file full: wait...
111013  9:49:57  InnoDB: Log file ./ib_logfile1 did not exist: new to be created
InnoDB: Setting log file ./ib_logfile1 size to 5 MB
InnoDB: Database physically writes the file full: wait...
InnoDB: Doublewrite buffer not found: creating new
InnoDB: Doublewrite buffer created
InnoDB: 127 rollback segment(s) active.
InnoDB: Creating foreign key constraint system tables
InnoDB: Foreign key constraint system tables created
111013  9:49:57  InnoDB: Waiting for the background threads to start
111013  9:49:58 InnoDB: 1.1.5 started; log sequence number 0
111013  9:49:58 [Note] Event Scheduler: Loaded 0 events
111013  9:49:58 [Note] /private/var/folders/b8/_yd9n9kj56z30jx6xp2gqjj80000gp/T/test_db/bin/mysqld: ready for connections.
Version: '5.5.9'  socket: 'mysql.sock'  port: 13306  MySQL Community Server (GPL)
[MysqldResource] mysqld running as process: 2555
INFO [main] EmbeddedMysqlManager.startDatabase(163) | MySQL started successfully @ 1318513803579
INFO [main] EmbeddedMysqlManager.startDatabase(168) | Executing scripts...
INFO [main] EmbeddedMysqlManager.startDatabase(171) | Executing script [schema.sql]
INFO [main] EmbeddedMysqlManager.startDatabase(171) | Executing script [data.sql]
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 11.616 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[MysqldResource] stopping mysqld (process: 2555)
111013  9:50:06 [Note] /private/var/folders/b8/_yd9n9kj56z30jx6xp2gqjj80000gp/T/test_db/bin/mysqld: Normal shutdown

111013  9:50:06 [Note] Event Scheduler: Purging the queue. 0 events
111013  9:50:06  InnoDB: Starting shutdown...
111013  9:50:07  InnoDB: Shutdown completed; log sequence number 1740505
111013  9:50:07 [Note] /private/var/folders/b8/_yd9n9kj56z30jx6xp2gqjj80000gp/T/test_db/bin/mysqld: Shutdown complete

[MysqldResource] clearing options
[MysqldResource] shutdown complete
INFO [Thread-3] EmbeddedMysqlManager.shutdownDatabase(188) | >>>>>>>>>> DELETING MYSQL BASE DIR [/private/var/folders/b8/_yd9n9kj56z30jx6xp2gqjj80000gp/T/test_db] <<<<<<<<<<
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 17 seconds
[INFO] Finished at: Thu Oct 13 09:50:08 EDT 2011
[INFO] Final Memory: 10M/265M
[INFO] ------------------------------------------------------------------------

Conclusion

MySQL Connector/MXJ is a little known secret that really simplified my unit testing of Data Access Layer.

Source for this project can be downloaded here [6]. This zip can be directly imported in Eclipse IDE.