Showing posts with label testing. Show all posts
Showing posts with label testing. Show all posts

Wednesday, February 18, 2009

STIQ, Eclipse, Ant and Hudson

Goal:


By the end of this entry, the user will have the tools for running a StoryTestIQ project from Eclipse and will be able to automate the test runs, either serially or in parallel, from a Continuous Integration server such as Hudson.

Prepare STIQ


First, we will need to download the STIQ binary. The latest version, as of this entry, is 2.0 RC5.
From the extracted distribution, delete all the /*.bat files. For this example, we will be running STIQ from inside Eclipse and from Ant. We can also delete the following unused files and directories: /repository/FrameworkTests, /repository/ProjectRoot/IntegrationTests/SmokeTests, /repository/ProjectRoot/SprintTests/SprintOne (the existing test suite is buggy and needs to be re-created), /bin/lib/jdbc-csv-mock.jar, /bin/lib/junit-3.8.1.jar, /bin/lib/ojdbc-14.jar, /bin/lib/sqlserver-1.0.809.jar.
If you need to use a specific database driver for your tests, then add it to /bin/lib, and configure it in storytestiq.properties.
For this example, we will add the latest jquery production download to /repository/extensions/display as well. As of this writing, the latest production distribution is jquery-1.3.1.min.js.

STIQ in Eclipse


To run STIQ from inside Eclipse, we can create a new plug-in project. For this example, the project is named timezra.blog.stiq. Because STIQ distributes its library files in /bin and /bin/lib, we will make sure to use build instead of bin for the compilation output folder.
In the plug-in MANIFEST.MF, we will also need to specify these dependencies: org.eclipse.core.runtime, org.eclipse.swt and org.eclipse.jface.
We can then copy the prepared STIQ distribution directly into the root of the project.

We will then add the required STIQ libraries to the classpath of the MANIFEST.MF: /bin/storytestiq.jar, /bin/lib/fitlibrary-1.1.jar, /bin/lib/selenium-server.jar, /bin/lib/Tidy.jar.
Finally, in order to run STIQ as a Java application from inside Eclipse, we can create a Main.java:

package timezra.blog.stiq;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
import java.util.Map.Entry;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import fitnesse.FitNesse;

public class Main {

    public static void main(final String[] args) throws Exception {
        loadProperties("storytestiq.properties");

        FitNesse fitnesse = null;
        try {
            fitnesse = FitNesse.startFitNesse(new String[] { "-o", "-p", "9999", "-r", "repository" });
            startBrowser();
        } finally {
            if (fitnesse != null) {
                fitnesse.stop();
            }
        }
    }

    private static void loadProperties(final String fileName) {
        final Properties properties = new Properties();
        try {
            properties.load(new FileInputStream(fileName));
            for (final Entry<Object, Object> entry : properties.entrySet()) {
                System.setProperty((String) entry.getKey(), (String) entry.getValue());
            }
        } catch (final IOException e) {
            e.printStackTrace();
        }
    }

    private static void startBrowser() {
        final Display display = new Display();
        final Shell shell = new Shell(display);
        final Browser browser = new Browser(shell, SWT.NONE);
        GridDataFactory.fillDefaults().span(3, 1).grab(true, true).applyTo(browser);
        GridLayoutFactory.fillDefaults().numColumns(3).generateLayout(shell);
        shell.open();
        browser.setUrl("http://localhost:9999/stiq/runner.hta?startPage=/ProjectRoot");
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch())
                display.sleep();
        }
        display.dispose();
    }
}



We can begin building acceptance tests by running Main.java as a Java application. For this example, I have setup two sets of SprintTests, all tagged as integration.

The STIQ project in Eclipse.
Your STIQ project in Eclipse should look similar.

STIQ and Ant


To run the the example STIQ tests from Ant, we can replace the existing /runner.xml with another version that uses better formatting and has options for running all integration tests serially or SprintTests in parallel.

<project name="STIQ-Runner" default="usage">

    <description>StoryTestIQ (STIQ) is a mashup of Selenium and FitNesse. This file is the STIQ runner.</description>

    <dirname file="${ant.file.STIQ-Runner}" property="install.dir" />
    <property file="${install.dir}/storytestiq.properties" />
    <property name="stiq-results" location="${install.dir}/stiq-results" />
    <property environment="env" />
    <loadfile property="jquery" srcfile="${install.dir}/repository/extensions/display/jquery-1.3.1.min.js" />

    <path id="stiq-classpath">
        <fileset dir="${install.dir}/bin">
            <include name="**/*.jar" />
        </fileset>
    </path>

    <target name="usage">
        <echo level="info">
            clean: remove any test result artifacts
            run-integration-tests: runs all STIQ IntegrationTests
            run-sprint-tests: runs STIQ Sprint Tests in Parallel
        </echo>
    </target>

    <target name="clean">
        <delete failonerror="false" includeemptydirs="true">
            <fileset dir="${stiq-results}">
                <include name="*/**" />
            </fileset>
        </delete>
    </target>

    <target name="init">
        <tstamp>
            <format property="today" pattern="MMMM d yyyy hh:mm a" locale="en,US" />
        </tstamp>
    </target>

    <target name="run-integration-tests" depends="clean, init">
        <run-stiq-suite start-page="/.ProjectRoot" suite-page="IntegrationTests" output-dir="${stiq-results}" />
        <publish-results-as-html />
        <fail>
            <condition>
                <not>
                    <equals arg1="${stiq-suite-result}" arg2="0" />
                </not>
            </condition>
        </fail>
    </target>

    <target name="run-sprint-tests" depends="clean, init">
        <parallel threadcount="2" >
            <run-stiq-suite start-page="/.ProjectRoot.SprintTests" suite-page="SprintOne" output-dir="${stiq-results}/SprintOne" port="9991" result-property="sprint.one.result" />
            <run-stiq-suite start-page="/.ProjectRoot.SprintTests" suite-page="SprintTwo" output-dir="${stiq-results}/SprintTwo" port="9992" result-property="sprint.two.result" />
        </parallel>
        <concat append="true" destfile="${stiq-results}/stiq-results.xml">&lt;parallel&gt;</concat>
        <concat append="true" destfile="${stiq-results}/stiq-results.xml">
            <fileset dir="${stiq-results}">
                <include name="*/stiq-results.xml" />
            </fileset>
        </concat>
        <concat append="true" destfile="${stiq-results}/stiq-results.xml">&lt;/parallel&gt;</concat>
        <publish-results-as-html />
        <fail>
            <condition>
                <not>
                    <and>
                        <equals arg1="${sprint.one.result}" arg2="0" />
                        <equals arg1="${sprint.two.result}" arg2="0" />
                    </and>
                </not>
            </condition>
        </fail>
    </target>

    <macrodef name="run-stiq-suite">
        <attribute name="output-dir" default="${install.dir}" />
        <attribute name="port" default="9999" />
        <attribute name="repository-path" default="repository" />
        <attribute name="result-property" default="stiq-suite-result" />
        <attribute name="start-page" />
        <attribute name="suite-page" />
        <sequential>
            <mkdir dir="@{output-dir}" />
            <echo level="info">Executing suite [@{suite-page}] in STIQ.</echo>
            <java classname="fitnesse.runner.FitnessRunner" classpathref="stiq-classpath" fork="true" resultproperty="@{result-property}">
                <sysproperty key="STIQDatabaseDriver" value="${STIQDatabaseDriver}" />
                <sysproperty key="STIQDatabaseConnectionString" value="${STIQDatabaseConnectionString}" />
                <sysproperty key="STIQDatabaseUsername" value="${STIQDatabaseUsername}" />
                <sysproperty key="STIQDatabasePassword" value="${STIQDatabasePassword}" />
                <arg line="@{port}" />
                <arg line="@{repository-path}" />
                <arg line="@{start-page}" />
                <arg line="@{suite-page}" />
                <arg line="@{output-dir}" />
            </java>
        </sequential>
    </macrodef>

    <macrodef name="publish-results-as-html">
        <sequential>
            <xslt basedir="${stiq-results}" destdir="${stiq-results}" includes="stiq-results.xml" out="stiq-results.html" style="stiq.xsl">
                <param name="applicationPath" expression="${install.dir}" />
                <param name="buildDate" expression="${today}" />
                <param name="buildNumber" expression="${env.BUILD_NUMBER}" />
                <param name="jquery" expression="${jquery}" />
            </xslt>
        </sequential>
    </macrodef>
</project>



NB: if you are using a version of jQuery other than jquery-1.3.1.min.js, this version can be configured among the Ant properties.

There are two STIQ targets in the Ant script: run-integration-tests will run all STIQ tests tagged as integration; run-sprint-tests will run each of the SprintTest suites in parallel. Note that, when using the latter target, if we add more SprintTest suites, we will also need to add new parallel macro calls to run-stiq-suite as well as failure condition checks, and we may want to increase the parallel threadcount.

Note also that the publish-results-as-html macro expects a /stiq.xsl stylesheet for performing the transform between stiq.xml test output and human-readable HTML. This stylesheet, originally written by Kendrick Burson, transforms the output from either an integration or parallel STIQ run into an HTML report.

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="html" indent="yes" />
    <xsl:param name="applicationPath" />
    <xsl:param name="buildDate" />
    <xsl:param name="jquery" />
    <xsl:param name="buildNumber" select="" />

    <!-- ############################################################# -->
    <!-- Template: match / -->
    <xsl:template match="/">
        <html>
            <head>
                <title>STIQ Test Results</title>
                <link rel="stylesheet" type="text/css" media="screen"
                    href="file://{$applicationPath}/repository/extensions/display/selenium-test.css" />
                <xsl:call-template name="writeClientScript" />
            </head>
            <body>
                <table border="0">
                    <tr>
                        <td>
                            Hudson Build #
                            <xsl:value-of select="$buildNumber" />
                            completed on
                            <xsl:value-of select="$buildDate" />
                        </td>
                    </tr>
                </table>
                <br/>
                <xsl:apply-templates select="parallel" />
                <xsl:apply-templates select="stiq" />
            </body>
        </html>
    </xsl:template>

    <!-- ############################################################# -->
    <!-- Template: match <parallel> -->
    <xsl:template match="parallel">
        <table border="0">
            <tr>
                <td align="left">
                    <b>
                        <xsl:text>STIQ Tests total:  </xsl:text>
                        <xsl:value-of select="count(stiq/body/table/tbody/tr[td/a])" />
                        <xsl:text>&#160;&#160;&#160;(</xsl:text>
                        <font color="green">
                            <xsl:text>Passed: </xsl:text><xsl:value-of select = "count(stiq/body/table/tbody/tr[td/a and @class=' status_passed'])" />
                        </font>
                        <xsl:text>&#160;&#160;&#160;</xsl:text>
                        <font color="red">
                            <xsl:text>Failed: </xsl:text><xsl:value-of select = "count(stiq/body/table/tbody/tr[td/a and @class=' status_failed'])" />
                        </font>
                        <xsl:text>)</xsl:text>
                    </b>
                </td>
            </tr>
        </table>
        <br/>
        <xsl:apply-templates select="stiq" />
    </xsl:template>

    <!-- ############################################################# -->
    <!-- Template: match <stiq> -->
    <xsl:template match="stiq">
        <table class="suiteheader">
            <tr>
                <td align="left">
                    <b>
                        <xsl:text>Suite Tests total:  </xsl:text>
                        <xsl:value-of select="count(body/table/tbody/tr[td/a])" />
                        <xsl:text>&#160;&#160;&#160;(</xsl:text>
                        <font color="green">
                            <xsl:text>Passed: </xsl:text><xsl:value-of select = "count(body/table/tbody/tr[td/a and @class=' status_passed'])" />
                        </font>
                        <xsl:text>&#160;&#160;&#160;</xsl:text>
                        <font color="red">
                            <xsl:text>Failed: </xsl:text><xsl:value-of select = "count(body/table/tbody/tr[td/a and @class=' status_failed'])" />
                        </font>
                        <xsl:text>)</xsl:text>
                    </b>
                </td>
            </tr>
        </table>
        <table class="yo">
            <xsl:apply-templates select="body/table/tbody/tr" mode="r1">
                <xsl:sort select="td[1]/a" />
            </xsl:apply-templates>
        </table>
        <br/>
    </xsl:template>

    <!-- ############################################################# -->
    <!-- Template: match <tr> -->
    <xsl:template match="tr" mode="r1">
        <xsl:element name="tr">
            <xsl:attribute name="class">
                <xsl:value-of select="@class"/>
            </xsl:attribute>
        
            <xsl:choose>
                <!-- First Row - Test Suite -->
                <xsl:when test="position() = 1">
                    <!-- First Column - Row Number - blank for test suite name -->
                    <td valign="top">&#160;</td>
                    <!-- Second Column - Test Name -->
                    <td valign="top" colspan="2">
                        <xsl:copy-of select="td[1]/*" />
                    </td>
                </xsl:when>
                
                <!-- Test Results -->    
                <xsl:otherwise>
                    <!-- First Column - Row Number -->
                    <td valign="top" class="{./@class}"><xsl:value-of select="position() - 1"/></td>
                    
                    <!-- Second Column - Test Name + test results in hidden div -->
                    <td valign="top" class="{./@class}">
                    
                        <!-- clickable link to show test results using test name as title -->
                        <a href="javascript:void(0);" onclick="javascript:$('#{generate-id(td[2])}').slideToggle();" title="{td[1]/a/@href}">
                            <xsl:value-of select="td[1]/a" />
                        </a>
                        
                        <!-- Hidden div show results of test. -->
                        <div id="{generate-id(td[2])}" valign="top" style="display:none" class="{./@class}" >
                            <blockquote>                
                                <xsl:apply-templates select="td[2]/*" mode="copy" />
                            </blockquote>
                        </div>
                    </td>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:element>
    </xsl:template>

    <!-- ############################################################# -->
      <!-- Template: copy all nodes and attributes -->
      <xsl:template match="@*|node()" mode="copy">
        <xsl:choose>
            <!-- Hide stiq controls from report == we do not want viewers to accidentally click active links to stiq runtime env -->
            <xsl:when test="local-name() = 'div' and ./@class='actions'">
                <xsl:comment>Remove STIQ Controls from report</xsl:comment>
            </xsl:when>

            <!-- create collapsible sub table for included 'components' -->
            <xsl:when test="local-name() = 'div' and ./@class='included'">
                <div class="stiqincluded">
                    <span>
                         <xsl:if test="./div/table/tbody/tr/@class = ' status_failed'">
                               <xsl:attribute name="style">background-color:#FF9999</xsl:attribute>
                         </xsl:if>
                        <span class="{./span/@class}">
                            <xsl:text>Included page: </xsl:text>>
                            <a href="javascript:void(0);" onclick="javascript:$('#{./div[@class='collapsable']/@id}').slideToggle();">
                                <xsl:value-of select="./span/a"/>
                            </a>
                        </span>
                    </span>
                    <br/>
                    <xsl:apply-templates select="./div" mode="copy"/>
                </div>
            </xsl:when>
            
            <!-- Show table of values -->
            <xsl:when test="local-name() = 'table'">
                <table class="stiqtest">
                    <xsl:apply-templates select="./*" mode="copy"/>
                </table>
            </xsl:when>
            
            <xsl:when test="local-name() = 'a'">
                <span style="color:blue">
                    <xsl:value-of select="." />
                </span>
            </xsl:when>
            
            <!-- Huh?  Don't know what it is so just copy it -->
            <xsl:otherwise>
                <xsl:copy>
                    <xsl:apply-templates select="@*|node()" mode="copy"/>
                </xsl:copy>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
    
    <xsl:template name="writeClientScript">
        <style>
            <![CDATA[ 
            <!--
            div.stiqincluded
            {
                background: #F9F9F9;
                margin: 5px 0px 5px 0px;
                padding: 2px 2px 2px 2px;
                border: 1px dotted #909090;
            }
            div.collapsable
            {
                display: none;
            }
            table.suiteheader
            {
                margin: 2px;
                padding: 2px;
                border: solid #AAAAAA;
                border-width: 1px 1px 0px 1px;
            }
            table.stiqtest
            {
                background: #EEEEEE;
                margin: 2px;
                padding: 2px;
                border: solid #AAAAAA;
                border-width: 1px 1px 1px 1px;
                font-family: trebuchet ms, verdana, tahoma, arial, sans-serif;
                font-size: 1em;
            }
            .status_done {
                background-color: #EEFFEE;
            }
            .status_passed {
                background-color: #CCFFCC !important;
            }
            .status_failed {
                background-color: #FFCCCC;
            }
            -->
            
]]>
        </style>
        <script LANGUAGE="javascript">
            <xsl:value-of select="$jquery" />
        </script>
    </xsl:template>        
</xsl:stylesheet>



STIQ and Hudson


For this example, we can setup Apache Tomcat as a Windows service to run Hudson.
We will want to ensure that Tomcat is setup to "Allow service to interact with desktop" so that we can view the automated STIQ test run.
The Tomcat Service configuration.
The Windows Service setup for the instance of Tomcat running Hudson should look similar.

From Hudson, we can build a freestyle software project to launch our STIQ tests with a custom workspace (i.e., the path to the Eclipse STIQ project workspace). We can then add a build task to run-sprint-tests with the custom runner.xml, and we can archive the stiq-results/stiq-results.html artifact.

The Hudson STIQ project configuration.
The Hudson configuration for your STIQ project should look similar.

We now have a standard setup for an Eclipse STIQ project, a way to launch STIQ from inside Eclipse, a way to run STIQ tests serially or in parallel from Ant, and a way to automate STIQ test runs from Hudson.

Tuesday, November 4, 2008

Testing With Spring, Hibernate and JUnit4

This post builds off the functional code and test methods constructed in previous posts. While the techniques in here are not restricted to the features specific to those situations, it might be helpful to review those posts for more context about the original code, the problems that we solved and the patterns that emerged leading to these re-factorings.

Motivation


To use the AbstractTransactionalDataSourceSpringContextTests with JUnit 4 test cases.

The Problem


When our test cases extend AbstractTransactionalDataSourceSpringContextTests, the JUnit 3.8 test runner executes them, so we are not able to use some features available in JUnit 4.

Write a DAO Method and a JUnit 4 Test Case


We will first create a method in the DAO that would benefit from one of the features available in JUnit 4 but not JUnit 3.8, i.e., a verification that the method invocation throws a particular exception deterministically under specific conditions. For the example, the method can simply always throw the expected exception.


public void expectDataAccessException() {
getHibernateTemplate().execute(new HibernateCallback() {
public Object doInHibernate(final Session session) throws HibernateException, SQLException {
throw new SQLException("");
}
});
}



The test case will declare the expectation that the particular exception will be thrown when the method under test is invoked.


@Test(expected = DataAccessException.class)
public void testExpectDataAccessException() throws Exception {
getAuthorDAO().expectDataAccessException();
}



If we run this test now, the test case will fail with an UncategorizedSQLException, even though we have declared our expectation in the annotation.

Re-factor To Composition


The solution is to create a test delegate for Spring-specific configuration (e.g., the setup of the Spring configuration location and the injection of Spring-managed beans) and for Spring datasource transaction management. One of the more powerful features of this particular test base class is that transactions that occur in test cases are rolled back on tear-down, so we generally have no need to worry about test data corruption.


package spring.hibernate.oracle.stored.procedures.dao;

import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;

public class AbstractTransactionalDataSourceSpringContextTestsDelegate extends
AbstractTransactionalDataSourceSpringContextTests {

private AuthorDAO authorDAO;

public void setup() throws Exception {
super.setUp();
}

public void teardown() throws Exception {
super.tearDown();
}

@Override
protected String[] getConfigLocations() {
return new String[] { "applicationContext.xml" };
}

public AuthorDAO getAuthorDAO() {
return authorDAO;
}

public void setAuthorDAO(final AuthorDAO authorDAO) {
this.authorDAO = authorDAO;
}
}



Now we are ready to remove the delegate code from the test class (and remove the superclass dependency on the AbstractTransactionalDataSourceSpringContextTests that causes the test to execute with the JUnit 3.8 runner). Note that we must declare @Before and @After methods to setup and tear-down the Spring delegate. Also note that we have retained the test-case specific setup (i.e., the population of Authors). Our class consists only of data setup and tests.


package spring.hibernate.oracle.stored.procedures.dao;

import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.DataAccessException;
import spring.hibernate.oracle.stored.procedures.domain.Author;

public class AuthorDAOTest {

private final AbstractTransactionalDataSourceSpringContextTestsDelegate delegate;

public AuthorDAOTest() {
delegate = new AbstractTransactionalDataSourceSpringContextTestsDelegate();
}

@Before
public void setUp() throws Exception {
delegate.setup();
createAuthor(1, "Jules", "Verne");
createAuthor(2, "Charles", "Dickens");
createAuthor(3, "Emily", "Dickinson");
createAuthor(4, "Henry", "James");
createAuthor(5, "William", "James");
createAuthor(6, "Henry", "Thoreau");
}

@After
public void tearDown() throws Exception {
delegate.teardown();
}

@Test
public void testFindByLastNameUsingHQL() throws Exception {
assertEquals(2, delegate.getAuthorDAO().findByLastNameUsingHQL("James").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingHQL("Verne").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingHQL("Dickinson").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingHQL("Dickens").size());
assertEquals(0, delegate.getAuthorDAO().findByLastNameUsingHQL("Whitman").size());
}

@Test
public void testFindByLastNameUsingStoredProcedure() throws Exception {
assertEquals(2, delegate.getAuthorDAO().findByLastNameUsingStoredProcedure("James").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingStoredProcedure("Verne").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingStoredProcedure("Dickinson").size());
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingStoredProcedure("Dickens").size());
assertEquals(0, delegate.getAuthorDAO().findByLastNameUsingStoredProcedure("Whitman").size());
}

@Test
public void testFindByFirstNameUsingFunction() throws Exception {
assertEquals(0, delegate.getAuthorDAO().findByFirstNameUsingFunction("James").size());
assertEquals(2, delegate.getAuthorDAO().findByFirstNameUsingFunction("Henry").size());
}

@Test
public void testUpdate() throws Exception {
final Author author = delegate.getAuthorDAO().findByLastNameUsingHQL("Thoreau").get(0);
author.setLastName("Miller");
delegate.getAuthorDAO().update(author);
assertEquals(1, delegate.getAuthorDAO().findByLastNameUsingHQL("Miller").size());
}

@Test(expected = DataAccessException.class)
public void testExpectDataAccessException() throws Exception {
delegate.getAuthorDAO().expectDataAccessException();
}

private void createAuthor(final int id, final String firstName, final String lastName) {
delegate.getJdbcTemplate().execute(
String.format("insert into author (id, first_name, last_name) values (%d, '%s', '%s')", id, firstName, lastName));
}
}



Observe Lifecycle Events


In order to listen for transaction-specific lifecycle events, we will create an Observer, effectively re-factoring method override to method observation.


package spring.hibernate.oracle.stored.procedures.dao;

public interface ITransactionListener {

void onSetUpBeforeTransaction() throws Exception;

void onSetUpInTransaction() throws Exception;

void onTearDownInTransaction() throws Exception;

void onTearDownAfterTransaction() throws Exception;
}



In the delegate, we now need to broadcast the transaction lifecycle events.


....
private final Collection transactionListeners;

public AbstractTransactionalDataSourceSpringContextTestsDelegate() {
transactionListeners = new ArrayList();
}
....
public void registerTransactionListener(final ITransactionListener listener) {
transactionListeners.add(listener);
}

@Override
protected void onSetUpBeforeTransaction() throws Exception {
super.onSetUpBeforeTransaction();
for (final ITransactionListener listener : transactionListeners)
listener.onSetUpBeforeTransaction();
}

@Override
protected void onSetUpInTransaction() throws Exception {
super.onSetUpInTransaction();
for (final ITransactionListener listener : transactionListeners)
listener.onSetUpInTransaction();
}

@Override
protected void onTearDownInTransaction() throws Exception {
try {
for (final ITransactionListener listener : transactionListeners)
listener.onTearDownInTransaction();
} finally {
super.onTearDownInTransaction();
}
}

@Override
protected void onTearDownAfterTransaction() throws Exception {
try {
for (final ITransactionListener listener : transactionListeners)
listener.onTearDownAfterTransaction();
} finally {
super.onTearDownAfterTransaction();
}
}



We can easily create a tracing transaction listener to demonstrate how this lifecycle observation works.


package spring.hibernate.oracle.stored.procedures.dao;

public class TracingTransactionListener implements ITransactionListener {

public void onSetUpBeforeTransaction() throws Exception {
System.out.println("TracingTransactionListener.onSetUpBeforeTransaction()");
}

public void onSetUpInTransaction() throws Exception {
System.out.println("TracingTransactionListener.onSetUpInTransaction()");
}

public void onTearDownAfterTransaction() throws Exception {
System.out.println("TracingTransactionListener.onTearDownAfterTransaction()");
}

public void onTearDownInTransaction() throws Exception {
System.out.println("TracingTransactionListener.onTearDownInTransaction()");
}
}



We will add this listener to the delegate on test setup. Note that the listener must be registered before the delegate itself is setup.


....
@Before
public void setUp() throws Exception {
delegate.registerTransactionListener(new TracingTransactionListener());
delegate.setup();
....
}



If we run the this test class now, we should note console output that includes these statements:
  • TracingTransactionListener.onSetUpBeforeTransaction()
  • TracingTransactionListener.onSetUpInTransaction()
  • TracingTransactionListener.onTearDownInTransaction()
  • TracingTransactionListener.onTearDownAfterTransaction()