I needed to run some Java test code in a SQL transaction. It modifies the database, changes that should be 1) invisible to other tests, and 2) thrown away after tests finish.
I am using JUnit (of course) so I wrote a @Rule to provide a transaction to my test methods (note I'm using Lombok for convenience):
@RequiredArgsConstructor
public final class SQLTransactionRule
extends ExternalResource {
private final DataSource original;
private final String user;
private final String password;
private final Consumer testDataSource;
private Connection conn;
public SQLTransactionRule(final DataSource original,
final Consumer testDataSource) {
this(original, null, null, testDataSource);
}
@Override
protected void before()
throws Throwable {
super.before();
beginTransaction();
testDataSource.accept(testDataSource(conn));
}
@Override
protected void after() {
testDataSource.accept(null);
rollbackTransaction();
super.after();
}
private void beginTransaction() {
try {
final Connection conn = original.getConnection(user, password);
conn.setAutoCommit(false);
this.conn = conn;
} catch (final SQLException e) {
throw new RuntimeException(
format("Cannot create transaction from %s: %s", original,
getRootCause(e)), e);
}
}
private void rollbackTransaction() {
try {
conn.rollback();
} catch (final SQLException e) {
throw new RuntimeException(
format("Cannot rollback transaction to %s: %s", original,
getRootCause(e)), e);
}
}
private DataSource testDataSource(final Connection conn) {
return (DataSource) newProxyInstance(getClass().getClassLoader(),
new Class[]{DataSource.class}, (proxy, method, args) -> {
System.out.println("SQLTransactionRule.testDataSource");
if (Object.class.equals(method.getDeclaringClass()))
return method.invoke(proxy, args);
else if ("getConnection".equals(method.getName()))
return conn;
else
return method.invoke(original, args);
});
}
} The motivation for the Consumer of "testDataSource" and for the proxied data source was avoidance of dependencies. Original code has this convenience method, but I decided that was a concern of the test code not the @Rule:
public JdbcTemplate newJdbcTemplate() {
return new JdbcTemplate(testDataSource);
} Of course there are tests for the @Rule as well! And this post on Mockito was handy.
@RunWith(MockitoJUnitRunner.class)
public final class SQLTransactionRuleTest {
@Mock
private DataSource original;
@Mock
private Connection conn;
@Rule
public final ExpectedException thrown = ExpectedException.none();
private SQLTransactionRule rule;
private DataSource testDataSource;
@Before
public void setUp()
throws SQLException {
when(original.getConnection(anyString(), anyString())).
thenReturn(conn);
rule = new SQLTransactionRule(original,
testDataSource -> this.testDataSource = testDataSource);
}
@Test
public void shouldExecuteStatement()
throws Throwable {
final boolean[] called = {false};
executeStatement(() -> called[0] = true);
assertThat(called[0], is(true));
}
@Test
public void shouldPublishTestDataSource()
throws Throwable {
executeStatement(
() -> assertThat(testDataSource, is(notNullValue())));
}
@Test
public void shouldReuseOriginalConnection()
throws Throwable {
executeStatement(() -> assertThat(testDataSource.getConnection(),
is(sameInstance(conn))));
}
@Test
public void shouldTransact()
throws Throwable {
executeStatement(() -> verify(conn).setAutoCommit(eq(false)));
verify(conn).rollback();
}
@Test
public void shouldDescribe()
throws Throwable {
thrown.expect(AssertionError.class);
thrown.expectMessage(containsString("alpha"));
thrown.expectMessage(containsString("beta"));
executeStatement(() -> assertThat("alpha", is(equalTo("beta"))));
}
@FunctionalInterface
private interface RunMe {
void execute()
throws Throwable;
}
private void executeStatement(final RunMe statement)
throws Throwable {
rule.apply(new Statement() {
@Override
public void evaluate()
throws Throwable {
statement.execute();
}
}, EMPTY).evaluate();
}
}