使用Seam、DBUnit和TestNG进行功能测试

作者:    |       Seam

我正在寻找一种将DBUnit与Seam集成的良好方式,以便我可以轻松地为功能测试准备数据集。这是我想到的,一个与Seam集成并在每个测试方法前后添加DBUnit操作的测试超类。

关于org.jboss.seam.mock.SeamTest类是您通常在Seam应用程序中进行功能测试的基础类。它允许您在表示层级别轻松地编写交互脚本(对于整个会话或单个事件)。我扩展了这个类以添加一些DBUnit特定功能

/**
 * Uses a JNDI datasource and JTA for DBUnit operations.
 *
 * @author christian.bauer@jboss.com
 */
public abstract class SeamDBUnitTest extends SeamTest {

    private static Log log = LogFactory.getLog(SeamDBUnitTest.class);

    private Context ctx;

    private ReplacementDataSet dataSet;

    @Configuration(beforeTestClass = true)
    public void prepare() throws Exception {
        log.debug("Preparing DBUnit setup for test class");

        // Prepare JNDI context
        ctx = new InitialContext();

        // Load the base dataset file
        URL input = Thread.currentThread()
                        .getContextClassLoader().getResource(getDataSetLocation());
        if (input == null)
            throw new RuntimeException(
                "Dataset '" + getDataSetLocation() + "' not found in classpath"
            );
        dataSet = new ReplacementDataSet(
                        new FlatXmlDataSet(input.openStream())
                      );
        dataSet.addReplacementObject("[NULL]", null);
    }

    @Configuration(beforeTestMethod = true)
    public void beforeTestMethod() throws Exception {
        executeOperations(getBeforeTestMethodStack());
    }

    @Configuration(afterTestMethod = true)
    public void afterTestMethod() throws Exception {
        executeOperations(getAfterTestMethodStack());
    }

    private void executeOperations(DatabaseOperation[] operations) throws Exception {
        log.debug("Executing database operations in JTA transaction");

        UserTransaction tx = getUserTransaction();
        tx.begin();

        IDatabaseConnection con = new DatabaseConnection( getConnection() );

        // TODO: Remove this once DBUnit works with HSQL DB
        DatabaseConfig config = con.getConfig();
        config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new FixDBUnit());

        for (DatabaseOperation op : operations) {
            op.execute(con, dataSet);
        }
        tx.commit();
    }

    // Subclasses can/have to override the following methods

    /**
     * Return a JDBC <tt>Connection</tt> with disabled foreign key checking.
     */
    protected Connection getConnection() throws Exception {
        log.debug("Getting database connection through JNDI lookup of DataSource");

        // Lookup Datasource in JNDI
        DataSource ds = (DataSource)ctx.lookup(getDataSourceJNDIName());
        Connection con = ds.getConnection();

        // Disable foreign key constraint checking
        // This really depends on the DBMS product... here for HSQL DB
        con.prepareStatement("set referential_integrity FALSE")
            .execute();

        return con;
    }


    /**
     * Add <tt>DatabaseOperation</tt> that run before every test method.
     */
    protected DatabaseOperation[] getBeforeTestMethodStack() {
        return new DatabaseOperation[]{};
    }

    /**
     * Add <tt>DatabaseOperation</tt> that run after every test method.
     */
    protected DatabaseOperation[] getAfterTestMethodStack() {
        return new DatabaseOperation[]{};
    }

    /**
     * The relative location of the data set on the classpath.
     */
    protected abstract String getDataSetLocation();

    /**
     * The JNDI name of the datasource used by DBUnit.
     */
    protected abstract String getDataSourceJNDIName();

}

这很简单:对于测试套件中的每个测试类,我从类路径中加载配置的DBUnit数据集,并将所有NULL标记替换为真实的null(请参阅DBUnit文档)。在每个测试方法之前(在Seam功能测试中具有会话范围),我运行一系列DBUnit操作,并在每个测试方法之后再次运行以进行清理。因此,我的功能测试中的每个会话都使用一个定义好的数据集。具体测试子类必须提供一些设置,例如DBUnit数据集文件的存储位置和使用的数据库连接名称。子类还可以覆盖getConnection()方法,如果需要自定义程序。(同时注意有关DBUnit/HSQL问题的TODO,也许有一天他们会真正修复这个问题。)

一个DBUnit数据集可能看起来像这样

<?xml version="1.0"?>

<dataset>
    <USERS      USER_ID             ="1"
                OBJ_VERSION         ="0"
                FIRSTNAME           ="Root"
                LASTNAME            ="Toor"
                USERNAME            ="root"
                PASSWORD            ="secret"
                EMAIL               ="root@toor.tld"
                RANK                ="0"
                IS_ADMIN            ="true"
                CREATED             ="2006-06-26 13:45:00"
                HOME_STREET         ="[NULL]"
                HOME_ZIPCODE        ="[NULL]"
                HOME_CITY           ="[NULL]"
                DEFAULT_BILLINGDETAILS_ID ="[NULL]"
            />
</dataset>

最后,这是一个具体的功能测试类

public class LoginLogoutFunction extends SeamDBUnitTest {

    protected String getDataSetLocation() {
        return "org/hibernate/ce/modules/user/test/basedata.xml";
    }

    protected String getDataSourceJNDIName() {
        return "[=>java:/caveatemptorDatasource]";
    }

    protected DatabaseOperation[] getBeforeTestMethodStack() {
        return new DatabaseOperation[] {
            DatabaseOperation.CLEAN_INSERT
        };
    }

    @Test
    public void testLoginLogout() throws Exception {

        // Test logged out state
        new Script() {

            // Test a component that requires a logged in user
            @Override
            protected void invokeApplication() {
                assert !isSessionInvalid();
                ChangePassword changePw =
                        (ChangePassword) Component.getInstance("changePassword", true);
                String outcome = changePw.doChange();
                assert "login".equals(outcome);
            }

            // Test if there is no loggedIn flag in the Session context
            @Override
            protected void renderResponse() {
                assert !Manager.instance().isLongRunningConversation();
                assert Contexts.getSessionContext().get("loggedIn") == null;
            }

        }.run();
...

前三个方法实现了并覆盖了DBUnit操作的设置,您可以为每个测试方法创建一个执行前后的DBUnit操作堆栈(本例中只有一个,在执行前运行)。测试方法testLoginLogout()这是一个单独的会话,我在其中运行了几个脚本(每个事件一个脚本),以测试我应用程序的登录和注销功能。如你所见,我的脚本中的方法是模拟JSF表示层事件。要查看不带DBUnit添加的全类,请浏览Seam教程,这来自酒店预订应用程序。

因此,一个测试类使用一个数据集,如果考虑到TestNG能够处理命名测试组和通配符匹配的能力,我认为这并不太限制。


返回顶部