Sie sind auf Seite 1von 52

Database Testing

Data Base Management System


(DBMS)

Stores and maintains information about a world.

Can be queried for providing a view of the world.

It can be updated for reflecting changes in the


world.
Different types of DBMS

Relational Database Management System


(RDBMS)

Main type of database management system in use

Object Database Management System (ODBMS)

Relational Database

RDMS database is a collection of tables

Each row of a table is a record (tuple)


Each column element of a record is an attribute
(field)
Fields can be of many different types

Schema: defines the structure of tables


Exemple: Table Author
ID

FamilyName

GivenName

DOB

Address

Johnson

James

1960/10/20

125 King Ed

Doob

Larry

1972/05/10

656 Somerset

Relational Database

Primary key is a field (or possibly multiple fields used


together) that uniquely identifies each record in the
table from every other record in the table.
Data in different tables are related through common
fields

The primary key of one of the tables is almost always


involved in the relationship.
The field in the other table on the other end of that
relationship is called the foreign key.

ISBN

AuthorID

765698754 1

Title

Year

Testing made easy

1999

Structured Query Language (SQL)

Language for accessing and manipulating database


systems

Based on the relational tuple calculus

ANSI and ISO (ISO 9075) standards

However, different conforming implementations

only need to support the same major keywords in a


similar manner (such as SELECT, UPDATE, DELETE,
INSERT, WHERE, and others)
DBMS often implements additional (proprietary)
extensions

Object-Relational Mapping

Data is manipulated as Objects in an Object-Oriented


program

but stored in a RDMS

A mapping between Objects and data in Tables is


needed
Application
objects

DB

Persistence: moving objects to/from permanent storage.

Object-Relational Mapping
Client
String name
Date birthdate
Account account

Common Mapping

A Class corresponds to 1 or
more Tables

Class instances correspond to


Rows in Tables

Field values correspond to cells

JDBC

Java Database Connectivity


(JDBC) API is a standard
relational database access
interface
Clients
Table

Accounts
Table

Allows a consistent access across


RDMSs

Databases Application Structure

In production, the database is typically resident on


a separate host from applications that use it.

For a client application with a direct connection,


two hosts are involved.
For a web application, three hosts are involved:
client, web server, database host.

Databases Application Structure

A well designed database related application


should have its database access code cleanly
separated from its business logic code.
Separation of concerns allows to easily change
the persistence strategy without changing the rest
of the application.
Also simplifies unit testing.
Application
Logic

Database
Access

DB

Example

Suppose that we have customers with given names, family names, and an id number.

Java object:
public class Customer
{
private int id;
private String givenName;
private String familyName;
// get and set methods for each of these.
}

Data base table CUSTOMER has three columns:


CUSTOMER_ID the primary key, a non-null integer column that is automatically
incremented as table rows are added
GIVEN_NAME customers given name, a string
FAMILY_NAME customers family name, a string

Database Table Definition

Example with references

Databases and objects use fundamentally different methods for


establishing associations.
Java object:
public class Order
{
private int id;
private Customer myCustomer;
private String details;
// etc.
}

Data base table ORDER has three columns:


ORDER_ID the primary key, a non-null integer column that is
automatically incremented as table rows are added
CUSTOMER_ID contains a value from the CUSTOMER_ID column in the
CUSTOMER table
DETAILS information about the order

Create a table with a relationship

Object-Relational Mapping
(ORM)
Application
objects

ORM
Manager

SQL

DB

Objects use references (pointers) to establish associations.

Database tables use foreign keys.

The objective of an object-relational mapping component is to do the


translation between objects and database tables

Two major tasks:


Object attributes mapped to/from table columns
Object references mapped to/from foreign keys

Class to be tested

Test classes that implement an interface CustomerTable, which


moves data to and from the database.

public interface CustomerTable


{
static final String TABLE_NAME = "CUSTOMER";
static final String ID_COL = "CUSTOMER_ID";
static final String GIVEN_NAME_COL = "GIVEN_NAME";
static final String FAMILY_NAME_COL = "FAMILY_NAME";
Map<Integer, Customer> getAllCustomers( );
Customer getCustomer( int id );
boolean updateCustomer( Customer customer );
boolean insertCustomer( Customer customer );
boolean deleteCustomer( Customer customer );
void close( );
}

Database Connections

The connection to a database via a programming language is


via a connection protocol API (application programming
interface):

ODBC: open database connectivity


JDBC: specific version for Java
Since databases are not compatible, there are normally specific
drivers loaded in for each type of database (Oracle, MySQL,
Derby, DB/2, etc.)
Specify a URL such as
jdbc:mysql://ste5007.site.uottawa.ca:3306/db to
connect, and then provide a userid and password.

After logging in, SQL commands are issued to insert, view,


update, or delete data.
Results from the database are loaded into a O/JDBC object for
use.

JDBC Database Connections


Table1

Schema1

Java
Program

Table2
JDBC

Driver

DB
Table3

Schema2
Table4

JDBC handles the Java interface, while the driver is customized for
the particular database to be used.

Aside from the connection protocol specifics, SQL has many


variations among database vendors.

Primary JDBC classes

Java classes used for JDBC


DriverManager: used to load the class which is the driver for
a specific database.
Connection: the database connection interface.
Statement: An SQL statement to be executed on the
connection.
ResultSet: The set of results that are returned by a query.
If supported by the driver, the result set can be updated and
saved back to the data base as necessary.

The DataSource interface can be used for any object that


can be used as a database connection.

Setting up the JDBC connection

Step 1: Load the appropriate driver class.


Normally, the driver is provided by the database vendor, and
is specific to a particular database and version.

The class needs to be on the class path.

For MySQL:

Driver class is in the mysql-connector-java.jar file, and this


archive must be on the class path.
Class name is com.mysql.jdbc.ClientDriver
To load the driver in Java, include the following statements:
String driverClassName = "com.mysql.jdbc.ClientDriver";
Class.forName( driverClassName );

Setting up the JDBC connection

Step 2: Setup the URL for the database:

format:
jdbc:db_vendor://host:port/location

For MySQL, running as a client on the same


computer using the default MySQL port, the first part
is:
jdbc:mysql://localhost:3306/database

Setting up the JDBC connection

Step 3: Connect to the database.

Ask the DriverManager for a connection.


java.sql.Connection connection;
connection =
DriverManager.getConnection( url,
username, password );

Setting up the JDBC connection

Step 4: Create a statement from the connection.

The parameters set properties of ResultSet objects


produced by executing the statement.
java.sql.Statement stmt;
stmt = connection.createStatement(
ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE,
ResultSet.HOLD_CURSORS_OVER_COMMIT );

Using the JDBC connection

Step 5: Create an SQL statement to be


executed.

The return value is a ResultSet object


containing rows of the database table that are
retrieved from the database.
String sqlCommand = "SELECT * FROM
CUSTOMER";
java.sql.ResultSet results;
results = stmt.executeQuery( sqlCommand );

JDBC data source


public class JdbcDataSource implements DataSource
{
private String url;
private String userid = DBProperties.getString( "DB.USERID" );
private String pwd = DBProperties.getString( "DB.PASSWORD" );
public JdbcDataSource( ) throws ClassNotFoundException
{
Class.forName( DBProperties.getString( "DB.DRIVER_CLASS" ) );
String vendor = DBProperties.getString( "DB.VENDOR" );
String host = DBProperties.getString( "DB.HOST" );
String port = DBProperties.getString( "DB.PORT" );
String location = DBProperties.getString( "DB.DBNAME" );
url = "jdbc:" + vendor + "://" + host + ":" + port
+ "/" + location ;
}
public Connection getConnection( String username, String password )
throws SQLException
{
return DriverManager.getConnection( url, username, password );
}
}

Loading a ResultSet
public class CustomerTableResultSet implements CustomerTable
{
private JdbcDataSource datasource;
private Connection connection;
private ResultSet results;
private String sqlCommand;
public CustomerTableRowSet( )
throws ClassNotFoundException, SQLException
{
datasource = new JdbcDataSource( );
connection = datasource.getConnection( );
Statement stmt = connection.createStatement( );
sqlCommand = "SELECT * FROM " + TABLE_NAME;
results = stmt.executeQuery( sqlCommand );
}
}

Using a JDBC Connection


Two approaches can be used for the JDBC connection.
1. Send SQL commands directly to the data base. In this
case, the ResultSet is used only to transfer data to
Java objects. SQL commands would also be used for
database updates.
The full range of SQL capabilities and variants for a
particular DB are available for use.
2. After an initial SQL query that obtains a result set, use
the ResultSet commands to update values.
Java code is decoupled from a particular vendors
database, but available functions are more limited.

Update, using SQL command


public boolean updateCustomer( Customer customer )
{
try
{
Statement stmt = connection.createStatement( );
StringBuffer sqlCommand = new StringBuffer( "UPDATE " );
sqlCommand.append( TABLE_NAME );
sqlCommand.append( " SET " );
sqlCommand.append( FIRST_NAME_COL );
sqlCommand.append( "=" );
sqlCommand.append( encodeSQLString( customer.getFirstName( ) ) );
sqlCommand.append( ", " );
sqlCommand.append( LAST_NAME_COL );
sqlCommand.append( "=" );
sqlCommand.append( encodeSQLString( customer.getLastName( ) ) );
sqlCommand.append( " WHERE " );
sqlCommand.append( ID_COL );
sqlCommand.append( "=" );
sqlCommand.append( customer.getId( ) );
stmt.executeUpdate( sqlCommand.toString( ) );
}
catch ( SQLException e ) { return false; }
return true;
}

Update, using ResultSet


public boolean updateCustomer( Customer customer )
{
boolean ok = false;
ok = this.setCursorAtCustomer( customer ); // not a JDBC method
if ( ok )
{
try
{
results.updateString( FIRST_NAME_COL, customer.getFirstName( ) );
results.updateString( LAST_NAME_COL, customer.getLastName( ) );
results.updateRow( );
}
catch ( SQLException e ) { return false; }
}
return ok;
}

Scopes of Database Testing

Scopes of Database Testing

Logic unit tests

Goal of these tests is to unit-test the business logic


code in isolation of the database access code (in
the database access layer).
Strategy is to mock the database access layer code
using a mock-objects approach.

Scopes of Database Testing

Database access unit tests

The database access code uses a persistence API


(e.g JDBC API) to access the database.
Goal of this type of test is to validate the correct use
of persistence API.
Strategy mock the persistence API so that tests can
run without being connected to a database.

example using Mockrunner

Scopes of Database Testing

Database integration unit tests

These types of tests check the persistence API as well as the


database functionalities: connectivity, queries, stored
procedures, triggers, constraints and referential integrity.
Strategy is to have the system under test really connected to
a database.

Can use an external database or an embedded in-memory


database (such as Derby)

DbUnit provides a way to preload the database with test


data.

Database test purposes

In a typical application, one should avoid testing (unless


you are a product vendor of such a component):
The data base management system (i.e. assume it
does its job)
The access protocol
Third party object persistence components
One should test:
Configuration of parameters for connections (to be
sure they work)
If object persistence code is custom-built, it should
be tested.
Objective: test your code, not the platform.

Questions for Testing

How do we connect to the database?


Does the client send the correct commands
to the database?

Are the expected results returned?

How do we set up the database...

... before any testing?

... for specific tests?

Potential test purposes

Test creation of domain objects from a ResultSet .


Verify the correct SQL commands are generated when
accessing or updating data.
Test the data base schema

Tables / columns exist

Primary key columns correct

Foreign key constraints correct

Triggers are correct

Stored procedures are correct

Access privileges are correct


JDBC resources are cleaned up

Database access unit tests with


MockRunner

MockRunner is a framework built on Junit for testing J2EE applications in


general
Provides mock support for testing the most commons J2EE component
types

Servlet APIs,

JDBC,

JMS,

EJB,

Struts Actions

Simulates the application's runtime environment

Allows to create unit tests that run out-of-container and independently


of deployment descriptors or other external artifacts.

MockRunner Example
public class CustomerTableRowSetMockTest extends
BasicJDBCTestCaseAdapter {

/**
* Prepares an empty result set to be returned by
* the next SELECT statement
*
*/

private void prepareEmptyResultSet() {


StatementResultSetHandler
StatementHandler = getJDBCMockObjectFactory().
getMockConnection().getStatementResultSetHandler();
MockResultSet result = statementHandler.createResultSet();
statementHandler.prepareGlobalResultSet(result);
}

MockRunner Example
@Test
public void testTableRowSet() {
prepareEmptyResultSet(); // we're not expecting any result
CustomerTable table = new CustomerTableRowSet();
table.close();
// check that select was issued
verifySQLStatementExecuted("SELECT * FROM CUSTOMER");
// check that no commit was issued
verifyNotCommitted();
// check that result set was closed
verifyAllResultSetsClosed();
}

MockRunner Example
private void prepareSingleResultSet() {
StatementResultSetHandler statementHandler =
getJDBCMockObjectFactory().
getMockConnection().
getStatementResultSetHandler();
MockResultSet result =
statementHandler.createResultSet();
// setup table
result.addColumn("CUSTOMER_ID");
result.addColumn("GIVEN_NAME");
result.addColumn("FAMILY_NAME");
// add data elements
result.addRow(new Object[] {"1", "Jane", "Deans"});
statementHandler.prepareGlobalResultSet(result);
}

MockRunner Example
@Test
public void testCustomerTableRowSet() throws
ClassNotFoundException,
SQLException {
prepareSingleResultSet();
CustomerTable table = new CustomerTableRowSet();
Customer cust = table.getCustomer(1);
table.close();
// check that right customer was created
String expGiven = "Jane";
String expFamily = "Deans";
assertEquals(expGiven,cust.getGivenName());
assertEquals(expFamily,cust.getFamilyName());
}

Database Integration Testing


Necessary to check interaction with real database
before putting application to production
Issues
Ensuring that test data doesn't mix-up with real
application data
Setting up test data (may involve a large amount
of realistic data)

How many databases?


Recommended to have four separate databases:
1. The production database. Never used for testing.
2. The development database, where each developer /
tester has their own area. Limited amounts of data, and
tables are unstable. Most testing should use this one.
3. A shared database with realistic amounts of data, to
ensure no performance problems occur, and that there
are no surprises at full scale.
4. A deployment database which can be set up to swap in
with the production database when ready to go. This is
the one where pre-deployment tests should be run.

At least the first two of these are necessary, but all


four are preferable, especially for large projects.

Database Integration Testing


Setup: Have the database loaded with
specific data before the test starts.
Test: do something interesting with the
database
After a test: check that the database has
been updated as per the test purpose.
Not necessary for read only tests.

Database setup strategies

Use direct access provided by data base management


system:
MySQL example:
Run an SQL script from the query browser.
Use an IDE or Ant
The Eclipse data tools platform provides a facility to
connect to a data base, browse tables, and load data.
Ant has an sql task that will connect to a database and
run SQL commands.
Use dbUnit
dbUnit can load a database using an XML file.

Use Java + JDBC and execute SQL statements

Use database data generator

Setup from Database Tools

SQL script to delete rows from table, and then load two
rows into a MySQL database:

Ant script to load data


<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." name="Database Demo"
default="insertData">
<property name="sql.dir" location="${basedir}/data" />
<property file="mysql.properties" />
<target name="insertData" description="Insert table data">
<sql
driver="${db.driver}"
url="${db.url}"
userid="${db.user}"
password="${db.pwd}"
autocommit="yes"
onerror="continue"
caching="yes"
>
<transaction src="${sql.dir}/loadData.sql" />
</sql>
</target>
</project>

dbUnit

dbUnit is an open source tool that can help


with database testing.
http://dbunit.sourceforge.net

Functions:

Use an XML file to store data to be loaded into a


database table before running a test.
Use an XML file as an expected table after a test.

The expected table can omit columns such as autogenerated primary keys, and the actual table can be
filtered based on the file.

Flat XML data set

Each row of the table is specified by one tag, with


the name of the table, and then values for the
columns.

<?xml version="1.0" encoding="UTF-8"?>


<dataset>
<CUSTOMER CUSTOMER_ID="1" GIVEN_NAME="Jane" FAMILY_NAME="Deans" />
<CUSTOMER CUSTOMER_ID="2" GIVEN_NAME=John" FAMILY_NAME="Doe" />
</dataset>

Using dbUnit for test setup


@Before
public void setUp( ) throws Exception
{
// Set up the connection for dbUnit
IDatabaseConnection connection = new DatabaseConnection(
new JdbcDataSource().getConnection() );
// Locate data set file, and read it
IDataSet dataSet = new FlatXmlDataSet(
this.getClass( ).getResource( "/customer/data.xml" ) );
// Load the data into the database, after erasing all data
DatabaseOperation.CLEAN_INSERT.execute(connection,dataSet);
connection.close( );
}

Using dbUnit to verify data


@Test
public void testDeleteCustomer( ) throws Exception
{
// Database set up and loaded in @Before method. Delete a customer.
Map<Integer, Customer> map = table.getAllCustomers( );
Customer deleteMe = map.get( 1 );
table.deleteCustomer( deleteMe );
// Load data set with expected table values
IDataSet expectedDataSet = new FlatXmlDataSet(
this.getClass( ).getResource( "/customer/afterDelete.xml" ) );
ITable expectedTable = expectedDataSet.getTable(CustomerTable.TABLE_NAME);
// Get actual database table values after deletion
IDataSet databaseDataSet = new DatabaseConnection(
dataSource.getConnection( ) ).createDataSet( );
ITable actualTable = databaseDataSet.getTable(CustomerTable.TABLE_NAME);
// Compare expected and actual data sets. dbUnit assertEquals() used.
Assertion.assertEquals( expectedTable, actualTable );
}

Data generation

Tools to generate large amount of random data

Use to populate a test database with realistic data

Data generation
CREATE TABLE CUSTOMER (
`id` mediumint(8) unsigned NOT NULL auto_increment,
`CUSTOMER_ID` MEDIUMINT default NULL,
`GIVEN_NAME` varchar(255) default NULL,
`FAMILY_NAME` varchar(255) default NULL,
PRIMARY KEY (`id`)
) TYPE=MyISAM AUTO_INCREMENT=1;

INSERT INTO `CUSTOMER` (`CUSTOMER_ID`,`GIVEN_NAME`,`FAMILY_NAME`) VALUES ('1','Kelsey','Chase');


INSERT INTO `CUSTOMER` (`CUSTOMER_ID`,`GIVEN_NAME`,`FAMILY_NAME`) VALUES ('2','Ryder','Knight');
INSERT INTO `CUSTOMER` (`CUSTOMER_ID`,`GIVEN_NAME`,`FAMILY_NAME`) VALUES ('3','Aidan','Navarro');
INSERT INTO `CUSTOMER` (`CUSTOMER_ID`,`GIVEN_NAME`,`FAMILY_NAME`) VALUES ('4','Dean','Dickson');
INSERT INTO `CUSTOMER` (`CUSTOMER_ID`,`GIVEN_NAME`,`FAMILY_NAME`) VALUES ('5','Ava','Moses');
INSERT INTO `CUSTOMER` (`CUSTOMER_ID`,`GIVEN_NAME`,`FAMILY_NAME`) VALUES ('6','Jarrod','Slater');
INSERT INTO `CUSTOMER` (`CUSTOMER_ID`,`GIVEN_NAME`,`FAMILY_NAME`) VALUES ('7','Harper','Rivers');
INSERT INTO `CUSTOMER` (`CUSTOMER_ID`,`GIVEN_NAME`,`FAMILY_NAME`) VALUES ('8','Charity','Morrow');
INSERT INTO `CUSTOMER` (`CUSTOMER_ID`,`GIVEN_NAME`,`FAMILY_NAME`) VALUES ('9','Branden','Moses');
........