Download Unit Testing Tips and Tricks: Database Interaction

Document related concepts

Extensible Storage Engine wikipedia , lookup

Entity–attribute–value model wikipedia , lookup

Open Database Connectivity wikipedia , lookup

Microsoft SQL Server wikipedia , lookup

Database model wikipedia , lookup

Relational model wikipedia , lookup

SQL wikipedia , lookup

PL/SQL wikipedia , lookup

Null (SQL) wikipedia , lookup

Transcript
Unit Testing Tips and Tricks:
Database Interaction
Louis Thomas
Overview
•
•
•
•
•
Attitudes Toward Testing
What is a Unit Test
Common Unit Testing Patterns
Unit Testing Database Interactions
Acceptance Tests With Databases
Attitudes Toward Testing
Are You Test Infected?
• There are two kinds of people:
– People who don’t like writing tests.
• Can’t be done attitude.
– People who do like writing tests.
• “I think I can” attitude.
• Expect a learning curve. Be creative!
• Unit tests are good even if you don’t do
Test Driven Development!
What Is A Unit Test
There Are Many Kinds Of Tests
• Acceptance tests, user tests, integration
tests, functional test, unit tests; black box,
white box…
• All tests have merit if they can detect
bugs.
• Tests only have value if they are run!
Unit Tests
• From developer's point of view.
• Tests the smallest amount of a system that
is interesting.
– Often just one part of one class!
• Highly automated.
• Fast.
• No configuration or external
dependencies.
Common Unit Testing Patterns
Unit Test Rule Of Thumb
• If you are having trouble writing a unit test
or (for those of you who aren't test infected)
if it is "impossible" to write a test for your
system,
• You are trying to test to much. Test a
smaller chunk.
But How?
• Sometimes objects have complex
behaviors, extensive state, and tight
relationships. This makes tests difficult: set
up is difficult and time consuming, and
objects cannot be isolated.
• (But wait, that’s not right! Right?)
Loosening The Coupling
• Introduce interfaces between complex
objects.
• Create a mock object to stand in for the
complex object.
• Repeat as needed. (Be creative.)
Creating Interfaces
• If it's our object, just create an interface!
• If it's not our object,
– Create a mock that extends the object and
overrides all its methods (works sometimes).
– Create an interface anyway and create an
adapter for the foreign object.
Example: WallClock
Interface
public interface WallClock {
long getTime();
}
Wrapper for normal system service
public class DefaultWallClock implements WallClock {
public static final WallClock INSTANCE = new DefaultWallClock();
public long getTime() {
return System.currentTimeMillis();
}
}
singleton instance
for convenience
implementation
of interface
Mock Objects
• A mock object is a stand-in for a complex
system object.
• Start out as simple as possible (throw
exceptions on all methods).
• Add recording of incoming method calls.
Example: MockClientSession
useful base class
public class MockClientSession extends ReportingMockObject implements ClientSession {
public void flushOutgoingBuffer() {
recordActivity("fOB");
}
public void setInterval(int nUpdateIntervalMilliseconds) {
recordActivity("sI("+nUpdateIntervalMilliseconds+")");
}
implementation of interface:
- record interesting input param
public void notifyNewOutgoingData() {
recordActivity("nNOD");
}
implementation of interface:
- no return value
- no side effects
- just record that call occurred
public String getClientName() {
recordActivity("gCN");
return "mockClient";
}
}
mock implements one interesting interface
- simple naming convention
implementation of interface:
- simple behavior, hard-coded
return value sufficient
Example: ReportingMockObject
public class ReportingMockObject {
StringBuffer m_stringBuffer = new StringBuffer();
}
public String getActivityRecordAndReset() {
String sActivityRecord = m_stringBuffer.toString();
m_stringBuffer = new StringBuffer();
return sActivityRecord;
}
Get activity details and clear
StringBuffer.
public void recordActivity(String sMessage) {
m_stringBuffer.append(sMessage);
}
Accumulate activity details into
a StringBuffer.
Mock Objects, cont’d
• Add facility for sending back canned
responses.
– (ex, setNextReply, setFailOnNextRequest)
Example: MockWallClock
public class MockWallClock implements WallClock {
private List m_nextTimes=new LinkedList();
public void addNextTime(long nNextTime) {
m_nextTimes.add(new Long(nNextTime);
}
Accumulate canned responses
into List.
public void addNextTimes(long[] nextTimes) {
Require.neqNull(nextTimes, "nextTimes");
for (int nIndex=0; nIndex<nextTimes.length; nIndex++) {
addNextTime(nextTimes[nIndex]);
}
}
public long getTime() {
Assert.gtZero(m_nextTimes.size(), "m_nextTimes.size()");
return ((Long)m_nextTimes.remove(0)).longValue();
}
}
Return next response from List
on each call.
Mock Objects, cont’d
• Add whatever functionality you need.
• Often one mock object will support all tests
for a given object, but can create special
ones for certain tests.
• Often, one mock object will support tests
for many objects that interact with it.
Example: MockMultiTableSessionListener
public class MockMultiTableSessionListener extends ReportingMockObject implements
MultiTableSession.Listener
{
Thumbprinter allows
public interface Thumbprinter {
customization of how activity
String getThumbprint(MultiTableSession.Update update);
is recorded, per unit test.
String getThumbprint(SessionState sessionState);
}
private final Thumbprinter m_thumbprinter;
private boolean m_bLogSessionStateNotification = true;
Flag allows disabling
recording of uninteresting
activity, per unit test.
public MockMultiTableSessionListener(Thumbprinter thumbprinter) {
m_thumbprinter = thumbprinter;
}
public void setLogSessionStateNotification(boolean bLogSessionStateNotification) {
m_bLogSessionStateNotification = bLogSessionStateNotification;
}
public void sessionStateNotification(SessionState sessionState) {
if (true==m_bLogSessionStateNotification) {
recordActivity("sSN("+m_thumbprinter.getThumbprint(sessionState)+")");
}
}
}
Mock Object Frameworks
• If you don’t want to create mock objects by
hand, consider a framework that does it by
reflection:
– EasyMock (http://easymock.org)
– jMock (http://www.jmock.org/)
Object Mother
• Sometimes you will need a complex data
structure set up. Refactor mercilessly.
• Especially if you need canned data that is
ancillary to the test, it is often worth while
to factor creation out into a static method
in a utility class (object mother) so you can
use it as necessary thereafter.
Testing Accessor
• Problem: there are private methods you
would like to test, or private members you
would like to inspect for your test
• You could make them public, but they
really are private.
• Alternative: an inner class!
TestingAccessor
Example: TestingAccessor
//################################################################
Default behavior
// testing
can be
modified for testing.
private WallClock m_wallClock = DefaultWallClock.INSTANCE;
private IStepper m_getConStepper = DefaultStepper.INSTANCE;
private IStepper m_maintStepper = DefaultStepper.INSTANCE;
View and modify important
private variables.
public class TestingAccessor {
public void setWallClock(WallClock wallClock) { m_wallClock = wallClock; }
public void setGetConStepper(IStepper stepper) { m_getConStepper = stepper; }
public void setMaintStepper(IStepper stepper) { m_maintStepper = stepper; }
public void setNextOverdueConnectionCheck(long tsNextOverdueConnectionCheck)
{ m_tsNextOverdueConnectionCheck = tsNextOverdueConnectionCheck; }
public int getAllConnectionsSize() { return m_allConnections.size(); }
public int getUnusedConnectionsSize() { return m_unusedConnections.size(); }
public int getTotalConnections() { return m_nTotalConnections; }
public void cacheMaintenaceThread() { DBConnectionPool.this.cacheMaintenaceThread(); }
public void doNotifyAll() { synchronized (m_oStateLock) { m_oStateLock.notifyAll(); } }
}
public TestingAccessor getTestingAccessor() {
return new TestingAccessor();
}
Invoke private methods or
other custom behavior.
Easy to set up.
Unit Testing Database
Interactions
Testing Database Interactions
• All the database classes are interfaces
already! How convenient!
• Create mocks and away you go.
• Insert / update / delete
– (relatively) straight forward
Example: writeChangeStepsToDatabase
public static void writeChangeStepsToDatabase(Statement dbStatement,
InstrumentListChangeRequest instrumentListChangeRequest, int nParentId) throws SQLException
{
InstrumentListChangeRequest.ChangeStep[]
changeSteps = instrumentListChangeRequest.getChangeStepsSnapshot();
StringBuffer stringBuffer = new StringBuffer();
for (int nIndex = 0; nIndex<changeSteps.length; nIndex++) {
InstrumentListChangeRequest.ChangeStep changeStep = changeSteps[nIndex];
stringBuffer.setLength(0);
changeStep.persistTo(stringBuffer);
SQL string we want to test.
String sSql = "INSERT INTO "+TABLE_NAME
+" ("+FIELD_PARENT_ID
+", "+FIELD_STEP_NUM
+", "+FIELD_STEP_DETAIL
+") VALUES"
+" ("+FORMAT_PARENT_ID.format(nParentId)
+", "+FORMAT_STEP_NUM.format(nIndex)
Method call on Statement.
+", "+FORMAT_STEP_DETAIL.format(stringBuffer.toString())
+")";
// send it to the database
int nRows = dbStatement.executeUpdate(SqlFormatHelper.showSql(sSql));
if (0==nRows) {
throw new SQLException("Failed to write to database.");
}
}
}
Example: testWriteChangeStepsToDatabase
public class TestChangeStepSqlHelper extends BaseTradingSystemTest {
public void testWriteChangeStepsToDatabase() throws Exception {
CurrencySnapshot instrument = new CurrencySnapshot(new InstrumentSnapshotId("0.FOO"), new
InstrumentLineageId(1), null, ValidityPeriod.ALL_TIME, new CurrencyIsoSymbol("F"), "F", "Foo");
InstrumentListChangeRequest instrumentListChangeRequest1 = new InstrumentListChangeRequest();
instrumentListChangeRequest1.addNewInstrument(instrument);
InstrumentListChangeRequest instrumentListChangeRequest2 = new InstrumentListChangeRequest();
instrumentListChangeRequest2.addNewInstrument(instrument);
Set up MockStatement.
instrumentListChangeRequest2.addNewInstrument(instrument);
Invoke method to be tested,
MockStatement mockStatement = new MockStatement();
passing mock.
SimulatedDatabase.Table changeStepTable = new
Verify correct behavior seen.
SimulatedDatabase.Table(ChangeStepSqlHelper.ALL_FIELDS);
CaxManagementSimulatedDatabaseTables.setUpChangeStepTable(mockStatement, changeStepTable);
ChangeStepSqlHelper.writeChangeStepsToDatabase(mockStatement, instrumentListChangeRequest1, 1);
assertEquals(1, changeStepTable.getRowCount());
ChangeStepSqlHelper.writeChangeStepsToDatabase(mockStatement, instrumentListChangeRequest2, 2);
assertEquals(3, changeStepTable.getRowCount());
}
}
Example: MockStatement
public class MockStatement implements Statement {
Beforehand, Handlers registered
public interface UpdateHandler {
int handleUpdate(String sql) throws SQLException;
with SQL string they support.
}
private Map m_stringMatcherToUpdateHandlerMap=new TreeMap(new
SystemWebDirectory.StringMatchComparator());
public void registerUpdateHandler(String sql, UpdateHandler updateHandler) {
Object key;
if (sql.endsWith("*")) {
SQL string compared against
sql=sql.substring(0, sql.length()-1);
known updates to find handler.
key=new SystemWebDirectory.StringPrefixMatcher(sql);
Handler invoked to process update.
} else {
key=new SystemWebDirectory.StringMatcher(sql);
}
Object prevValue=m_stringMatcherToUpdateHandlerMap.put(key, updateHandler);
Require.eqNull(prevValue, "prevValue");
}
public int executeUpdate(String sql) throws SQLException {
UpdateHandler updateHandler=(UpdateHandler)m_stringMatcherToUpdateHandlerMap.get(sql);
if (null==updateHandler) {
throw new SQLException("Unexpected update \""+sql+"\".");
} else {
return updateHandler.handleUpdate(sql);
}
}
//...
Code to configure generic MockStatement for this
group of unit tests factored out into Object Mother.
Example: setUpChangeStepTable
public class CaxManagementSimulatedDatabaseTables {
public static void setUpChangeStepTable(MockStatement mockStatement,
SimulatedDatabase.Table changeStepTable)
{ //...
SQL string to be handled.
mockStatement.registerUpdateHandler(
"INSERT INTO "+ChangeStepSqlHelper.TABLE_NAME+" ("
Build custom handler.
+ChangeStepSqlHelper.FIELD_PARENT_ID+", *",
getInsertNewHandler(changeStepTable, new String[] {ChangeStepSqlHelper.FIELD_PARENT_ID,
ChangeStepSqlHelper.FIELD_STEP_NUM, ChangeStepSqlHelper.FIELD_STEP_DETAIL}));
}
private static MockStatement.UpdateHandler getInsertNewHandler(final SimulatedDatabase.Table table,
final String[] columnNames)
Parse SQL string and add new
{
row to backing store.
return new MockStatement.UpdateHandler() {
public int handleUpdate(String sql) throws SQLException {
SimulatedDatabase.Table.Row row = table.addRow();
SimpleSqlTokenizer simpleSqlTokenizer = new SimpleSqlTokenizer(sql);
for (int nIndex = 0; nIndex<columnNames.length; nIndex++) {
Object columnValue = simpleSqlTokenizer.getNextParameter();
String sColumnName = columnNames[nIndex];
row.set(sColumnName, columnValue);
}
return 1;
}
};
}
Testing Database Interactions
• Read – must return something – trickier.
• Mocks will act as factories: statements return
record sets.
– Load your mock statement with the mock record set
to return.
– Load your mock connection with the mock statement
to return.
• Can start out with mocks with hard coded
expectations and returns, but will probably
refactor into more general objects.
Example: readChangeStepsFromDatabase
public static InstrumentListChangeRequest readChangeStepsFromDatabase(Statement dbStatement,
int nParentId, int nExpectedSteps) throws SQLException
{
InstrumentListChangeRequest.ChangeStep[] changeSteps = new
InstrumentListChangeRequest.ChangeStep[nExpectedSteps];
int nFoundSteps = 0;
SQL string we want to test.
// process all the rows
String sSql = "SELECT * FROM "+TABLE_NAME+" WHERE "
+FIELD_PARENT_ID+" = "+FORMAT_PARENT_ID.format(nParentId);
ResultSet resultSet = dbStatement.executeQuery(SqlFormatHelper.showSql(sSql));
try {
while (resultSet.next()) {
Method call on Statement.
int nStepNum = resultSet.getInt(FIELD_STEP_NUM);
checkForNull(resultSet, FIELD_STEP_NUM);
Method calls on ResultSet.
String sPersistedChangeStep = resultSet.getString(FIELD_STEP_DETAIL);
checkForNull(resultSet, FIELD_STEP_DETAIL);
InstrumentListChangeRequest.ChangeStep changeStep = new
InstrumentListChangeRequest.ChangeStep(new BloombergTokenizer(sPersistedChangeStep));
//…
private static void checkForNull(ResultSet resultSet, String sColumnName) throws SQLException {
if (resultSet.wasNull()) {
throw new SQLException("Unexpected null for column "+sColumnName+".");
}
}
Example: readChangeStepsFromDatabase
//…
if (nStepNum<0 || nStepNum>=nExpectedSteps) {
throw new SQLException("Found change step "+nStepNum
+" but expected 0 < changeStep <= "+nExpectedSteps+".");
} else if (null!=changeSteps[nStepNum]) {
throw new SQLException("Found second change step "+nStepNum+".");
}
changeSteps[nStepNum] = changeStep;
nFoundSteps++;
}
} finally {
try {
Method call on ResultSet.
resultSet.close();
} catch (SQLException e) {
Syslog.warning(ChangeStepSqlHelper.class, "Failed to close result set.", e);
}
}
if (nFoundSteps!=nExpectedSteps) {
throw new SQLException("Found only "+nFoundSteps+" change steps out of "
+nExpectedSteps+" expected.");
}
InstrumentListChangeRequest instrumentListChangeRequest = new InstrumentListChangeRequest();
instrumentListChangeRequest.setChangeSteps(changeSteps);
return instrumentListChangeRequest;
}
Example: testReadChangeStepsFromDatabase
public void testReadChangeStepsFromDatabase() throws Exception {
MockStatement mockStatement = new MockStatement();
setUpChangeStepTable(mockStatement);
InstrumentListChangeRequest instrumentListChangeRequest;
InstrumentListChangeRequest.ChangeStep[] changeSteps;
Set up MockStatement.
Invoke method to be tested,
passing mock.
instrumentListChangeRequest = ChangeStepSqlHelper.readChangeStepsFromDatabase(mockStatement, 5, 1);
changeSteps = instrumentListChangeRequest.getChangeStepsSnapshot();
assertEquals(1, changeSteps.length);
assertNull(changeSteps[0].getOldInstrument());
Verify correct behavior seen.
assertNotNull(changeSteps[0].getNewInstrument());
assertEquals(new InstrumentSnapshotId("0.CTO"), changeSteps[0].getNewInstrument().getInstrumentSnapshotI
instrumentListChangeRequest = ChangeStepSqlHelper.readChangeStepsFromDatabase(mockStatement, 10, 2)
changeSteps = instrumentListChangeRequest.getChangeStepsSnapshot();
assertEquals(2, changeSteps.length);
assertNull(changeSteps[0].getOldInstrument());
assertNotNull(changeSteps[0].getNewInstrument());
assertEquals(new InstrumentSnapshotId("0.B"), changeSteps[0].getNewInstrument().getInstrumentSnapshotId()
assertNotNull(changeSteps[1].getOldInstrument());
assertNotNull(changeSteps[1].getNewInstrument());
assertEquals(new InstrumentSnapshotId("0.A"), changeSteps[1].getNewInstrument().getInstrumentSnapshotId()
assertEquals("a-old", ((Equity)changeSteps[1].getOldInstrument()).getCompanyName());
}
Example: MockStatement
public class MockStatement implements Statement {
Beforehand, Handlers registered
public interface QueryHandler {
ResultSet handleQuery(String sql) throws SQLException; with SQL string they support.
}
private Map m_stringMatcherToQueryHandlerMap=new TreeMap(new
SystemWebDirectory.StringMatchComparator());
public void registerQueryHandler(String sql, QueryHandler queryHandler) {
Object key;
if (sql.endsWith("*")) {
SQL string compared against
sql=sql.substring(0, sql.length()-1);
known queries to find handler.
key=new SystemWebDirectory.StringPrefixMatcher(sql);
Handler invoked to process query.
} else {
key=new SystemWebDirectory.StringMatcher(sql);
}
Object prevValue=m_stringMatcherToQueryHandlerMap.put(key, queryHandler);
Require.eqNull(prevValue, "prevValue");
}
public ResultSet executeQuery(String sql) throws SQLException {
QueryHandler queryHandler=(QueryHandler)m_stringMatcherToQueryHandlerMap.get(sql);
if (null==queryHandler) {
throw new SQLException("Unexpected query \""+sql+"\".");
} else {
return queryHandler.handleQuery(sql);
}
}
//...
Code to configure generic MockStatement for this
group of unit tests factored out into Object Mother.
Example: setUpChangeStepTable
public class CaxManagementSimulatedDatabaseTables {
public static void setUpChangeStepTable(MockStatement mockStatement, SimulatedDatabase.Table
changeStepTable)
{ //…
SQL string to be handled.
mockStatement.registerQueryHandler(
"SELECT * FROM "+ChangeStepSqlHelper.TABLE_NAME+" WHERE "
Build custom handler.
+ChangeStepSqlHelper.FIELD_PARENT_ID+" = *",
getSelectByIdHandler(changeStepTable, new String[] { ChangeStepSqlHelper.FIELD_PARENT_ID }));
}
private static MockStatement.QueryHandler getSelectByIdHandler(final SimulatedDatabase.Table table,
final String[] columnNames)
Parse SQL string “where” clause
{
to find IDs.
return new MockStatement.QueryHandler() {
public ResultSet handleQuery(String sql) throws SQLException {
// identify the ids that must match
String[] ids = new String[columnNames.length];
SimpleSqlTokenizer simpleSqlTokenizer = new SimpleSqlTokenizer(sql);
for (int nIdIndex = 0; nIdIndex<ids.length; nIdIndex++) {
ids[nIdIndex] = simpleSqlTokenizer.getNextParameter();
}
//...
Example: setUpChangeStepTable
// create a new table containing all the matching rows
Create temp table to hold results.
final SimulatedDatabase.Table resultTable = new
SimulatedDatabase.Table(table.getColumnNames());
for (Iterator itr = table.getRowIterator(); itr.hasNext();) {
SimulatedDatabase.Table.Row row = (SimulatedDatabase.Table.Row)itr.next();
boolean bMatched = true;
for (int nIdIndex = 0; nIdIndex<ids.length; nIdIndex++) {
if (!idMatch(ids[nIdIndex], row.get(columnNames[nIdIndex]))) {
bMatched = false;
break;
Find all matching rows and
}
copy to result table.
}
if (true==bMatched) {
resultTable.addRow(row.getAll());
}
}
return new BaseMockResultSet() {
Return mock ResultSet that acts
protected int getTableSize() {
return resultTable.getRowCount();
as Iterator over result table.
}
public Object getObjectInternal(String columnName) throws SQLException {
return resultTable.getRow(m_nIndex).get(columnName);
}
};
}
};
Example: BaseMockResultSet
public abstract class BaseMockResultSet implements ResultSet {
protected int m_nIndex = -1;
private boolean m_bWasNull = false;
Simple interface for derived classes.
protected abstract int getTableSize();
protected abstract Object getObjectInternal(String columnName) throws SQLException;
public boolean next() throws SQLException {
m_nIndex++;
return m_nIndex<getTableSize();
}
public void close() throws SQLException {
// do nothing
}
public void setWasNull(boolean bWasNull) {
m_bWasNull = bWasNull;
}
public Object setWasNull(Object object) {
m_bWasNull = null==object;
return object;
}
public boolean wasNull() throws SQLException {
return m_bWasNull;
}
//...
Manage iterating through rows.
Implement JDBC null handling.
Example: BaseMockResultSet
//...
public String getString(String columnName) throws SQLException {
Object columnValue = setWasNull(getObjectInternal(columnName));
if (null==columnValue) {
return null;
} else {
return columnValue.toString();
}
}
Getting a string in the
current row is easy.
public int getInt(String columnName) throws SQLException {
Object columnValue = setWasNull(getObjectInternal(columnName));
if (null==columnValue) {
return -1;
} else {
String sValue = columnValue.toString();
try {
return Integer.parseInt(sValue);
} catch (NumberFormatException e) {
throw new SQLException("Value "+sValue+" of column "+columnName
+" can't be converted to int. "+e);
}
}
}
No other methods
Getting integers
is similar.
used,
so no other methods implemented.
Example: SimulatedDatabase.Table
public static class Table {
private String[] m_columnNames;
List
private int m_nColumns;
private Map m_columnNameToColumnIndexMap;
List
private List m_rows=new ArrayList();
public Table(String[] columnNames) {
Require.neqNull(columnNames, "columnNames");
Require.gtZero(columnNames.length, "columnNames.length");
m_columnNames=columnNames;
m_nColumns=m_columnNames.length;
createColumnMap();
}
public String[] getColumnNames() {
return m_columnNames;
}
public int getRowCount() {
return m_rows.size();
}
public Iterator getRowIterator() {
return m_rows.iterator();
}
public Row getRow(int nRowIndex) {
Require.geqZero(nRowIndex, "nRowIndex");
Require.lt(nRowIndex, "nRowIndex", getRowCount(), "getRowCount()");
return (Row)m_rows.get(nRowIndex);
}
of column names
of Rows
Example: SimulatedDatabase.Table
//...
public void addRow(Object[] objects) {
Require.neqNull(objects, "objects");
Require.eq(objects.length, "objects.length", m_nColumns, "m_nColumns");
Row row=new Row();
m_rows.add(row);
for (int nIndex=0; nIndex<objects.length; nIndex++) {
Object object=objects[nIndex];
row.set(nIndex, object);
}
}
public Row addRow() {
Row row=new Row();
m_rows.add(row);
return row;
}
//...
Example: SimulatedDatabase.Table
//...
private void createColumnMap() {
m_columnNameToColumnIndexMap=new HashMap();
for (int nIndex=0; nIndex<m_nColumns; nIndex++) {
String sColumnName=m_columnNames[nIndex];
m_columnNameToColumnIndexMap.put(sColumnName, new Integer(nIndex));
}
}
private int getIndexForName(String sColumnName) throws SQLException {
Integer columnIndex=(Integer)m_columnNameToColumnIndexMap.get(sColumnName);
if (null==columnIndex) {
throw new SQLException("Unknown column name \""+sColumnName+"\".");
}
return columnIndex.intValue();
}
//...
Example: SimulatedDatabase.Table.Row
public class Row {
private Object[] m_objects;
An object
public Row() {
m_objects = new Object[m_nColumns];
}
public Object get(String sColumnName) throws SQLException {
return m_objects[getIndexForName(sColumnName)];
}
public void set(String sColumnName, Object object) throws SQLException {
m_objects[getIndexForName(sColumnName)]=object;
}
public Object get(int nColumnIndex) {
return m_objects[nColumnIndex];
}
public void set(int nColumnIndex, Object object) {
m_objects[nColumnIndex]=object;
}
public Object[] getAll() {
return m_objects;
}
}
}
for each column.
Acceptance Tests With
Databases
Acceptance Tests With
Databases
• An acceptance test: Want to test the
"whole" app.
• Good for testing that the database really
likes the SQL we hard coded in the unit
tests, and really responds the way we
expect.
Acceptance Tests With
Databases, Cont’d
• Will take longer to run than a unit test.
• May need special environment (acquire
exclusive access to the testing database).
• Should still be as fast as possible.
– Use a small, interesting subset of the the
production dataset.
• Should still be as automated as possible.
• Test code will still look similar to unit tests.
Acceptance Tests With
Databases, Cont’d
• Big question is, how can we automate? I built up
a toolkit as I went.
– BulkLoadData: reads CSV files and loads data into
database. (Use Excel to edit.) (Just 214 lines.)
– ExecuteSqlScript: processes a text file of SQL
commands. (Just 222 lines.)
• Used to create tables, etc.
– ExecuteDatabaseSetupScript: allows me to write little
scripts (Just 205 lines.)
• Knows about 5 commands, including BulkLoadData and
ExecuteSqlScript
Example: BulkLoadData input file
null_value,null
Name of next table.
table,inst_definitions
First row is column names.
begin_data_with_columns
inst_snapshot_id,validity_begin,validity_end,inst_lineage_id,alleged_type,equity_ticker_detail,cusip,isin,sedol,
country_id,currency_id,company_name,round_lot_size,registrar_venue_id,opra_symbol_root,
opra_symbol_suffix,underlying_id,strike_price,expiration_timestamp,parity
1,null,null,1,:3:,A,a.cusip,null,null,1,1,null,100,5,null,null,null,null,null,null
2,null,null,2,:3:,B,null,null,null,1,1,null,100,4,null,null,null,null,null,null
All the column values for a row.
end_data
reset_sequence,inst_snapshot_id_seq,3,1
Sequences can be easily reset.
reset_sequence,inst_lineage_id_seq,3,1
table,inst_definitions_audit
begin_data_with_columns
revision,rev_begin,rev_begin_user,rev_begin_comment,rev_end,rev_end_user,rev_end_comment,
inst_snapshot_id,validity_begin,validity_end,inst_lineage_id,alleged_type,equity_ticker_detail,cusip,
isin,sedol,country_id,currency_id,company_name,round_lot_size,registrar_venue_id,
opra_symbol_root,opra_symbol_suffix,underlying_id,strike_price,expiration_timestamp,parity
0,"to_date('2005-01-01','YYYY-MM-DD')",lt,created A,null,null,null,1,null,null,1,:3:,A,
a.cusip,null,null,1,1,null,100,5,null,null,null,null,null,null
0,"to_date('2005-01-01','YYYY-MM-DD')",lt,null,null,null,null,2,null,null,2,:3:,B,
null,null,null,1,1,null,100,4,null,null,null,null,null,null
end_data
Example: ExecuteSqlScript input file
[def boolean]
Macros can be defined.
number(1)
[main]
Keyword to ignore errors for one statement.
@ignore_errors@ drop table table_metadata;
All other errors are fatal.
create table table_metadata (
table_id number not null,
SQL statements delimited by semicolons.
table_name varchar2(50) not null,
is_metatable &boolean; not null,
is_audited &boolean; not null,
is_editable &boolean; not null,
is_deletable &boolean; not null,
is_pend_changeable &boolean; not null,
display_name varchar2(100),
java_table_handler varchar2(500)
) tablespace instr_svc_data;
create unique index table_metadata_pk on table_metadata
(table_id)
tablespace instr_svc_idx;
alter table table_metadata
add (constraint table_metadata_pk primary
key(table_id));
@ignore_errors@ drop sequence table_id_seq;
create sequence table_id_seq start with 1 increment by 1;
Example: ExecuteDatabaseSetupScript files
setupScript-schemaAndAllData.txt
select_db_config DeephavenInstrumentService
execute_sql_script tearDownEntireSchema-1.0.txt
execute_sql_script createEntireSchema-1.0.txt
execute_setup_script setupScript-allDataOnly.txt
Identifies database config by name
from system properties.
Executes SQL script by file name.
Executes child setup script by file name.
setupScript-instDataOnly.txt
select_db_config DeephavenInstrumentService
delete_all_from_table inst_definitions
delete_all_from_table inst_definitions_audit
bulk_load_data data-inst_definitions-1.0.csv
Convenient truncate of table before bulk load
without requiring separate SQL script file.
Executes bulk load by file name.
Acceptance Tests With
Databases, Cont’d
• I built up a toolkit as I went, cont’d
– TestingResource framework
• I can define the testing resources that my test needs,
setup/teardown methods, and dependencies.
• Resources should be able to set themselves up from any
initial state (ex, delete all rows in table and reload)
– Now, the acceptance test can just declare all the
resources it needs, and framework will set them up.
Just needs to mark which resources it dirties, so they
can be reset for subsequent tests.
TestingResource Framework
*
TestingResourceManager
-nameToTestingResourceMap
1
+get()
+put()
+tearDownAll()
+markResourceAsDirty()
STATE_TORN_DOWN: 0
STATE_SET_UP: 1
STATE_DIRTY: 2
<<Interface>>
TestingResource
+getName()
+markAsDirty()
+tearDown()
+setUp()
+addDependant()
+addDependancy()
BaseTestingResource
-dependents
-dependencies
-name
-isAlwaysSetUp
-state
#doTearDown()
#doSetUp()
#assumeAlwaysSetUp()
DatabaseTestingResource
DeephavenInstrumentServiceTestingResource
+getDeephavenInstrumentService()
*
*
1
1
InstrumentServiceSessionTestingResource
+getInstrumentSession3()
TestingResource Framework
InstrumentServiceSessionTestingResource
DeephavenInstrumentServiceTestingResource
Active Session
Service
DatabaseTestingResource(s)
Instrument Tables
Fixed Tables
Schema
Depends on
Change Management Tables
Example: defineTestResources
private void defineTestResources() {
DatabaseTestingResource schemaResource = new
DatabaseTestingResource(TEST_RESOURCE_SCHEMA);
m_testingResourceManager.put(TEST_RESOURCE_SCHEMA, schemaResource);
DatabaseTestingResource fixedTablesResource = new
Create resource.
DatabaseTestingResource(TEST_RESOURCE_FIXED_TABLES);
Register dependencies.
fixedTablesResource.addDependency(schemaResource);
m_testingResourceManager.put(TEST_RESOURCE_FIXED_TABLES, fixedTablesResource);
Register
DatabaseTestingResource instTableResource = new
DatabaseTestingResource(TEST_RESOURCE_INST_TABLE);
instTableResource.addDependency(schemaResource);
m_testingResourceManager.put(TEST_RESOURCE_INST_TABLE, instTableResource);
//...
with manager
}
Set up needed resource (and all dependencies).
private void setUpInstrumentService() {
m_testingResourceManager.get(TEST_RESOURCE_ACTIVE_SESSION).setUp();
m_deephavenInstrumentService = ((DeephavenInstrumentServiceTestingResource)
m_testingResourceManager.get(TEST_RESOURCE_SERVICE)).getDeephavenInstrumentService();
m_instrumentSession3 = ((InstrumentServiceSessionTestingResource)
m_testingResourceManager.get(TEST_RESOURCE_ACTIVE_SESSION)).getInstrumentSession3();
}
Get resource by name to call specific helper method.
private void finished() {
m_testingResourceManager.tearDownAll();
At end of test run, clean up all resources.
}
Example: testReadTablesAndMetadata
private void testReadTablesAndMetadata() throws Exception {
Syslog.info(this, "===== testReadTablesAndMetadata =====");
setUpInstrumentService();
Set up resources.
// request metadata for regions table
ReplyCatcher replyCatcher=new ReplyCatcher();
Use resources as part
m_instrumentSession3.requestLookUp(new MultiTableSession.Filter[] {
new SingleTableFilterImpl(DisConstants.Tables.METADATA_OF+DisConstants.Tables.REGIONS,
new FilterPredicates.Any()) }, replyCatcher, null);
replyCatcher.waitForReply();
MultiTableSession.RefreshUpdate[] refreshUpdates=checkAndGetUpdatesFromReply(replyCatcher);
Assert.eq(refreshUpdates.length, "refreshUpdates.length", 3);
Object[][] regionsMetadataTableData=new Object[][] {
{ DisConstants.ColumnMetadata.COLUMN_ID, DisConstants.ColumnMetadata.COLUMN_NAME,
DisConstants.ColumnMetadata.TABLE_ID },
{ new Integer(200), "region_id", new Integer(9) },
{ new Integer(201), "region_name", new Integer(9) },
{ new Integer(202), "agg_id", new Integer(9) },
};
checkUpdateHasRows(refreshUpdates, regionsMetadataTableData);
of test.
//...
Mark used resources as dirty (if necessary).
// done
m_testingResourceManager.markResourceAsDirty(TEST_RESOURCE_ACTIVE_SESSION);
}
In Closing
But Wait, There’s More
• All tests from developer point of view so far –
What about users?
– Yes! Help users create tests that make sense to them.
– Same rules still apply: automated, decoupled, simple.
• Testing Stored Procedures and Referential
Integrity
– Yes! All rules and behaviors are worth testing. (I
haven’t done this yet.)
– Same rules still apply: automated, decoupled, simple.
Summary
•
•
•
•
•
Attitudes Toward Testing
What is a Unit Test
Common Unit Testing Patterns
Unit Testing Database Interactions
Acceptance Tests With Databases
• Questions?