测试 J2EE 数据库应用程序的最佳实践

      文重点介绍 J2EE 应用程序如何通过 JDBC 或 Oracle TopLink 与 Oracle 数据库相互作用,不过这些概念也适用于为其他应用程序环境(例如 .NET)编写的应用程序。本文仅讨论功能测试,不涉及性能测试或负载测试。

数据库操作测试:什么是数据库操作测试,为什么需要这种测试

测试 J2EE 应用程序最多也就是困难和费时一些,而测试 J2EE 应用程序的数据库操作则特别具有挑战性。数据库操作测试必须能够捕获棘手的逻辑错误 — 例如,当查询返回错误的数据时,或者当更新错误地或以意外的方式更改数据库状态时。

例如,假设您有一个 PERSON 类,它代表一个 PERSON 表,对 PERSON 表的数据库操作封装在一个数据访问对象 (DAO) PersonDAO 里,如下所示:

public interface PersonDAO 
{
/**
* Returns the list of persons
* with the given name
* or at least the minimum age
*/
public List listPersons(
String name, Integer minimumAge);
/**
* Returns all the persons
* in the database
*/
public List listAllPersons();
}

在这个简单的 PersonDAO 接口中,listPersons() 方法将(从 PERSON 表中)返回具有指定名称或指定最小年龄值的所有行。确定您是否在您自己的类中正确实现了该方法的测试必须考虑几个问题:

  • 该方法是否调用了正确的 SQL 语句(对 JDBC 应用程序而言)或者正确的查询过滤表达式(对基于对象角色建模 [ORM] 的应用程序而言)?
  • SQL 过滤表达式或查询过滤表达式编写是否正确,它是否返回了正确的行数?
  • 如果您提供了无效的参数,将会怎样?该方法的行为是否与预期的一致?它是否能正确地处理所有边界条件?
  • 该方法是否能够从数据库返回的结果集中正确地填充人员列表?

因此,即使一个简单的 DAO 方法也会有许多可能的结果和错误情形。为确保应用程序正确运行,应当测试每一种可能的结果和错误情形。在大多数情形下,您将希望这些测试能够与数据库交互并使用真实的数据 — 纯粹在个别类级别上进行的测试或使用模拟对象来模拟数据库相关性是不够的。数据库测试对于读/写操作同样重要,特别是那些在数据库上应用了许多更改的操作 — 这是 PL/SQL 存储过程非常常见的情况。

最关键的是:只有通过大量坚实的数据库测试,您才能验证这些操作是否正确运行。

本文中的最佳实践尤其与设计专门针对这些类型的数据访问挑战的测试相关。测试必须能够发现在数据访问抽象层中出现的数据检索和修改中的不明显错误。本文着重介绍数据库操作测试 — 用于负责持久数据访问和操作的 J2EE 应用程序层的测试。该层通常被封装在 DAO 中,后者将持久性机制从应用程序的其余部分中隐藏了起来。

最佳实践列表

在逐步了解这些最佳实践之前,请下载示例测试工具(参见“安装和使用最佳实践测试工具”边条)并利用 Oracle JDeveloper 按照说明进行操作。

以下列表列举了在本文中介绍的测试最佳实践:

最佳实践 1: 从“易测试的”应用程序体系结构开始。
最佳实践 2:使用精确的断言。
最佳实践 3:外化断言数据。
最佳实践 4:编写全面的测试。
最佳实践 5:创建稳定、有意义的测试数据集。
最佳实践 6:创建专用的测试库。
最佳实践 7:有效地隔离测试。
最佳实践 8:分割测试套件。
最佳实践 9:使用适当的框架(如 DbUnit)简化过程。

建立示例工程后,就该开始研究这些最佳实践了。

最佳实践 1:从“易测试的”应用程序体系结构开始

选择合适的应用程序体系结构以确保可以轻松地测试应用程序。在测试驱动的开发 (TDD) 周期中,这通常被称为“可测性设计”。无论您的测试环境如何严格或复杂,如果应用程序不适于测试,那它也起不了太大作用。在以下情况下,数据库操作将易于测试

  • 它不依赖于应用服务器提供的运行时环境(容器)
  • 不存在不必要的外部相关性
  • 可以轻松地在测试环境中满足保留的外部相关性

J2EE 应用程序开发的传统方法并不能很好地满足这些准则。由于数据库操作必须在 EJB 容器内部运行,因此使用 CMP 实体 Bean 的当前版本的 EJB 2.0 和 2.1 应用程序难以测试。不过,这种情况可能在 EJB 3.0 中有显著改善。Oracle 的 EJB 3.0 预览版提供了一个实体测试工具(参见 oracle.com/technology/pub/articles/debu_testability_of_ejb.html),它允许在容器外部测试数据库操作。

出于同样的原因,从 Servlet 和 JSP 内部进行的数据库访问也难以测试。数据库操作难以与在 Web 应用程序的上下文中执行的其他操作分离开。

应用最佳实践。使应用程序易于测试的最简单方法是使用 DAO。DAO 在消除不必要的相关性时特别有用。根据定义,DAO 具有的唯一相关性是数据访问的相关性,如一个 Connection 或 DataSource 实例,在测试环境中很容易满足这种相关性。此外,DAO 将数据访问相关性从应用程序的其余部分中隐藏起来了,从而使应用程序的其他部分更易于测试。

我们的 DAO 实现 PersonDAOImpl 有一个简单的 setter 方法,它满足与 Connection 实例的相关性:

public void 
setConnection(Connection conn)
{
this.connection = conn;
}
populate

请注意,测试(而不是 DAO)负责获取连接。

更一般的情况是,现实中的 DAO 可能由一个 DataSource 实例而非 Connection 本身填充。在任意一种情况下都不存在对查找机制的相关性。

为了创建易测试的体系结构,建议配置您的应用程序来使用相关性注入 (DI) 容器。DI 容器将鼓励您以消除不必要相关性的方式编写应用程序。

试一试。安装 JDBC 和 TopLink 项目。(查看“测试 Oracle TopLink 应用程序”边条,了解安装信息。)为了运行 TopLink 测试,设置系统属性 useToplink。打开 Project Properties 对话框,选择 Run/Debug,然后在 Java Options 域中输入 -DuseToplink=true(如图 1 所示)。

图 1
图 1:配置测试套件,使用 TopLink 代替 JDBC

最佳实践 2:使用精确的断言

在测试运行时使用精确描述系统处于什么状态的精确断言。只要有一个可能的结果满足测试,数据库测试就是精确的。简单验证 SQL 语句是否成功执行并返回一些结果的数据库查询不是精确的,这是因为即使成功的执行也可能返回错误的结果集。下面是一个不精确测试的示例:

public class TestListPersons 
extends BaseDatabaseTest
{
...
/**
* Simple imprecise test that
* checks that results are
* returned but does not
* count the values returned
*/
public void testListPersons()
{

List results
= dao.listPersons(
"Phil", new Integer(25));
assertNotNull(results);
assertTrue(results.size() > 0);
}
}

如代码中的断言所示,该测试仅证明了 DAO 实现返回了一些东西(非空)以及结果大于 0 — 但我们不知道结果列表是否包含准确的数据。

应用最佳实践。精确的数据库测试将指定通过基于查询的 DAO 返回的项的数量和实际值。对更新操作而言,数据库测试还应验证是否已将预期的更改应用到精确定义的表和行的集合上。

您可以利用精确断言修改前面的示例(如清单 1 所示)。

代码清单 1: 利用精确断言进行测试

public void testPreciselyListPersons() 
{

List results = dao.listPersons("Phil", new Integer(25));
assertNotNull(results);
assertEquals(2, results.size());

for (Iterator iter = results.iterator(); iter.hasNext();)
{
Person person = (Person) iter.next();
assertNotNull(person.getId());
assertEquals("Phil", person.getFirstName());
assertNotNull(person.getAge());
assertTrue(person.getAge().intValue() >= 25);
assertNull(person.getSurName());
assertNull(person.getGender());
}
}

通过验证所有返回的行,人名是否是 Phil,年龄是否是 25 或者更大(由方法参数定义),我们的测试现在验证了该 DAO 实现遵守 listPersons() 方法约定。

试一试。在 Oracle JDeveloper 中,转至测试类 TestListPersons。再打开 PersonDAOImpl 类(在 TestListPersons 下)。在测试 listPersons() 实现时,比较 testListPersons()testPreciselyListPersons() 方法的精确性。

最佳实践 3:外化断言数据

将断言数据放在外部信息库中,以便于管理和维护您的测试。大多数开发人员都认为编写精确的断言是个不错的主意,但其编写方式可能与我们的精确断言之一 —assertEquals(2, results.size()) — 的编写方式不同,这是因为该断言值是硬编码的。如果您在用成百上千甚至成千上万个测试测试一个大型应用程序,您肯定不想在测试代码中遍布成百上千或者成千上万个硬编码的 String 或 int 值,其原因有两个:首先,如果您的测试数据发生变化,您希望能够轻松地找到需要更改的断言数据。第二,您希望利用在不同测试间共享断言数据的机制。该问题的解决方法是外化断言数据,正如您将在生产代码中外化字符串消息那样。

应用最佳实践。我们的示例应用程序包含了为外化断言数据问题提供的一个基于属性文件的简单解决方案。清单 2 显示了一个工作中的示例。

代码清单 2: 利用精确断言进行测试

public class TestListAllPersonsExternal extends BaseDatabaseTest 
{

private int ALL_RESULT;

protected void setUp() throws Exception
{
...
//get the externalized assertion data
ALL_RESULT = getAssertionDataInt("ALL_RESULT");
}

public void testPreciselyListAllPersons()
{

List results = dao.listAllPersons();
assertNotNull(results);
assertEquals(ALL_RESULT, results.size());
...
}
...
}

一个断言数据存储在一个实例字段中并通过使用继承的 getAssertionDataInt() 方法在 setUp() 中将其加载。在这个例子中,您将把数据库中所有人的计数值加载到 ALL_RESULT 字段中。您可以看到测试方法断言如何不再依赖于硬编码数据。

我们的小型断言数据加载框架试图按如下方式加载断言值:

1. 它在 TestListAllPersonsExternal.properties 文件中查找一个名为 ALL_RESULT 的属性,该文件与 TestListAllPersonsExternal 类位于同一个程序包中。

2. 它在 assertions.properties 文件中查找 ALL_RESULT 属性,该文件也位于 TestListAllPersonsExternal 类的程序包中。

3. 它在类路径的根程序包中的 assertion.properties 文件中查找该属性。

这是一种相当方便的搜索方案,这是因为它使您能够将断言数据分组为特定测试类专有的项,它们可以在一个程序包的测试类间共享,也可以在应用程序的所有测试类间共享。

试一试。比较 TestListPersonsTestListPersonsExternal 的内容。请注意,除了 TestListPersonsExternal 使用了外化的断言数据外,这两个类几乎完全一样。

最佳实践 4:编写全面的测试

您的测试套件应当是全面的,要覆盖所有的数据库操作,并允许尽可能与实际约束一样多的情景。如果一组测试覆盖了您能够合理预期将在应用程序中实际发生的所有情景,那么它就是全面的。给定一组完整的先决条件而不只是最明显的先决条件,我们的测试需要验证每一个方法是否正确地工作。在我们的 listPersons() 示例中,不难想到一些可能影响方法行为方式的其他执行前状态:

  • 在调用 listPersons() 之前,没有为 DAO 实现提供连接。
  • 调用 listPersons() 时将一个或两个参数设为 null。
  • 使用不同的数据集调用 listPersons()

在每种情况下,您都要验证是否都返回正确的数据。与精确测试使我们能够验证一种操作在给定先决条件下是否正确工作的方式相同,全面测试确定了所有操作在所有合理预期的先决条件下是否正确工作。

应用最佳实践。要使您的测试环境全面,首先应想象所有可能的情景。在测试优先的开发传统中,这将在要测试的方法实现期间甚至之前完成。其次,需要主观判断每种情景发生的可能性,以确定是否将其加入到测试套件中。极限编程 (XP) 风格的配对编程非常适用于这两种任务 — 在想像可能的情景以及判断要实际实施哪些测试时两个臭皮匠顶个诸葛亮。

不要总想着使用 Java 代码覆盖工具的输出作为您数据库测试覆盖完整的证据。对数据库测试而言,代码覆盖是必要非充分的。在一条 SQL 语句中可以包含相当复杂的数据检索逻辑,且每条语句都包含几种可分别测试的情景。

正如您可能已经体验到的,建立全面的测试是编写测试时最困难的技巧之一。对几乎所有项目而言,它都只是一个理想而非现实。开发人员需要在一方面覆盖相关的测试情景与另一方面由于测试的负担而变得无法承受之间找到合适的平衡。

安装和使用最佳实践测试工具

下载示例测试工具

包含在 databaseunit.zip 文件中的一个示例应用程序和测试环境演示了如何应用这些最佳实践。该示例包含一个工程的两个版本 — 一个只使用 JDBC,另一个使用 JDBC 和 Oracle TopLink。要使用这个示例来完成这些最佳实践,您需要

  • Oracle 软件 — Oracle JDeveloper 10.1.3 预览版(包含内置的 Oracle JDBC 库),Oracle 数据库(我们的示例使用 Oracle 9.2.0.1.0)和 SQL*Plus。所有这些软件都可以从 OTN 上获得。
  • JUnit — 使用 Oracle JDeveloper Help 菜单上的“Check for Updates”选项自动下载和安装 JUnit 扩展。
  • DbUnit — 下载 DbUnit 并将其安装为一个 JDeveloper 用户库。
  • 示例代码 — 下载并解压缩 databaseunit.zip。

在下载并安装了必要的软件后,按照这些步骤设置 Oracle JDeveloper:

第 1 步:在 JDeveloper 中创建一个新项目。在 Oracle JDeveloper 中为示例应用程序创建一个工作区,然后在工作区中创建一个新项目:右键单击 Applications Navigator 视图中的应用程序工作区节点,选择 New Project

第 2 步:将源文件添加到项目中。假定您已经下载并解压缩了 databaseunit.zip 存档,并将 jdbc_only 文件夹的内容复制到了新项目目录中。

右键单击 Applications Navigator 视图中的项目节点,打开 Project Properties 对话框。单击 Project Content。选择项目文件夹的内容,并将这些内容添加到项目中。Oracle JDeveloper 自动检测 Java 源文件的位置并将它们添加到项目中。

第 3 步:将库添加到项目中。在您下载了 JUnit 和 DbUnit 之后,从 Project Properties 对话框中,单击 Libraries。将 JUnit Runtime 和 Oracle JDBC 库以及新的 DbUnit 库添加到您的项目中。

第 4 步:配置测试环境。在 connection.properties 中设置数据库连接的详细信息,并设置 sqlplus.properties 和 dbunit.properties 中的路径使之与您的环境相符。确保您的 Oracle 数据库正在运行。我们的例子使用 Oracle 9.2.0.1.0。

第 5 步:运行测试套件。打开 AllTests.java 文件,右键单击,选择 Run。JUnit TestRunner 在 Oracle JDeveloper 的环境中运行,并显示测试案例运行成功或失败。

在附带的源代码中,TestListPersons 类包含用于 listPersons() 方法的一组相当全面的测试的一个示例,有兴趣的读者有空时可仔细研读一下。

试一试。在 Oracle JDeveloper 中,打开 PersonDAOImpl 的实现类以及测试类 TestListPersons。试着在实现中找到一种没有为任何测试所覆盖的情景。

最佳实践 5:创建稳定、有意义的测试数据集

花些时间和精力创建一个稳定、有意义的测试数据集,您的测试将以该测试数据集为基础。 有了好的测试数据,编写数据库测试也就容易得多了。开发人员知道他们会得到所需的数据,因此也就可以信心十足地作出精确断言。

创建测试数据是在 TDD 环境中进行数据库开发的一个基础。对数据库操作而言,开发应是测试数据驱动的。在为用例编写测试之前,构建一个数据集。测试数据集并不总是需要很大;它只需足够覆盖与要测试的操作相关的情景。

创建测试数据可能是一项单调乏味的工作,但值得投入时间和脑力创建有意义且结构合理的数据。如果您在项目的早期就创建了高质量的数据,那么您将发现当您向应用程序中添加新特性和测试时可以更容易地更改、扩展或重用您的工作成果。好的测试数据对用户界面测试也极其有用。

应用最佳实践。您可以通过输入 SQL INSERT 语句或使用工具将数据导入成这种格式来轻松地创建测试数据。然而,为了实现测试自动化,您将需要一种易于从 JUnit 测试案例内部访问的数据插入和删除机制。您可以使用以下任一方式:

  • 从 JUnit 内部运行 SQL*Plus 或 SQL*Loader 脚本。附带的源代码就使用了这种方法。有兴趣的读者可以看一下 SqlPlusRunner 的实现。它通过一个单独的 Java 过程运行了一个 SQL*Plus 脚本。
  • ANT SQL 任务。
  • 定制的测试数据加载程序。这种加载程序从文本文件中读取 SQL 命令并通过 JDBC 执行它们。
  • 库,如 DbUnit,它是专门设计用来简化数据库测试的一个 JUint 扩展。DbUnit 允许将测试数据从 XML 文件导入到数据库中,并且还允许将数据库中的数据导出成 XML 和其他格式。
  • 应用程序的 API 本身。当然,没有该 API 的实际实现,您就无法使用这种机制。

在我们的示例应用程序中,您使用从 Java 内部启动的 SQL*Plus 加载测试类的 setUp() 方法中的测试数据,如清单 3 所示。

代码清单 3: setup() 方法与 SqlPlusRunner

protected void setUp() throws Exception 
{
super.setUp();
SqlPlusRunner.runSQLPlusFile("truncate_data.sql");
assertEquals(0,SqlPlusRunner.runSQLPlusFile("persons_data.sql"));
...
}

注意在测试运行之前对 SqlPlusRunner 的第一次调用如何从相关的表中删除现有的数据(通过调用 truncate_data.sql 文件)。接下来对 SqlPlusRunner 的调用将插入测试本身所需的数据(通过调用 persons_data.sql 文件)。

试一试。Applications Navigator 视图中,打开 Resources 节点。您将看到几个 SQL 文件(如图 2 所示)。这些文件包含了项目的测试数据以及用于截除数据、创建和删除表的脚本。这些脚本将作为测试环境的一部分来运行,并且还可以用 SQL*Plus 单独运行。

图 2
图 2:用于创建测试数据的资源

如果您不应用它. . . 如果不保证在项目的早期创建有意义的数据,那么您将发现自己不得不重复地(经常是没有必要地)为新测试创建新数据,从而导致易可管理性和强健性较差的测试环境。

最佳实践 6:创建专用的测试库

创建一个专门支持数据库测试的类库可以大大减轻测试的负担。作为编写数据库测试的开发人员,您可能发现自己为支持测试编写了大量的代码。例如,您将需要获取和释放数据库连接。您将需要启动和结束事务。您还经常需要在运行测试之后运行数据库状态查询。有时您将需要在运行数据库测试前执行修改数据库状态的操作,特别是您在一个无法在测试运行之前清理和重新插入数据的环境中工作时。

在所有这些情况下,您都将因专用测试库 — 包含在测试源文件夹中的一个类库,完全为简化测试而编写,不用于生产代码 — 的存在而获益。您不需要提前创建测试库,可以到时候再添加。例如,假定您编写了一些修改或检查系统状态的测试代码 — 只需将这些代码从测试类中取出,并将其转移到测试库中以供将来使用。

应用最佳实践。 您可以使用与生产代码所遵循规则迥异的规则来编写要加入测试库的代码:

  • 测试库代码不需要严格检查先决条件(例如空指针),这是因为测试中的失败将捕获这种问题。
  • 测试库代码不需要抛出检查到的异常。任何被捕获的检查到的异常都可以只作为运行时异常再次抛出,并且可以由测试框架捕获。
  • 通常使用静态方法更合适。这突出了创建测试库的目标。生产代码将使用通过接口提供的对象实例来使灵活性和代码重用最大化。另一方面,您的测试实用工具经常需要执行非常具体的任务并与测试环境本身紧密耦合。正确使用静态方法会使您减少测试支持代码的数量,这是因为您甚至不需要担心对象的实例化。

下面代码显示了使用中的示例应用程序测试库:

public void testPersonsWithIdLookup() 
{

List results = dao.listPersons("Phil", null);
assertNotNull(results);
assertEquals(2, results.size());

for (Iterator iter = results.iterator(); iter.hasNext();)
{
Person person = (Person) iter.next();
assertNotNull(person.getId());
assertEquals("Phil", PersonUtils.getFirstName(person.getId(),
connection));
doCommonAsserts(person);
}
}

第一次使用显示了如何获取数据库连接。您将获取连接的确切机制从测试中隐藏起来了。ConnectionUtils 用于获取连接的机制 — DriverManager 的静态方法而不是 JDBC DataSource 或连接池实现 — 是专门用于测试环境的。第二次使用显示了如何能够在测试库中添加应用程序特有的功能。

PersonUtils.getFirstName() 只是一个使您能够通过最少的击键次数从数据库中检索一条信息的方便方法。测试库中这种类型的功能越多,您对测试环境可以施加的控制就越大。

试一试。使用 Applications Navigator 视图仔细研读 co.uk.realsolve.databasetest.library 程序包及其子程序包,获得测试库方法的示例。通过选择方法名称然后输入 CTRL + ALT-U 查看任意方法的用法。

最佳实践 7:有效地隔离测试

隔离测试以便一个测试的错误或失败不会影响其他测试或使它们无法成功执行。任何数据库测试的运行都需要预先将系统设置为某个稳定的已知状态。您的测试环境应使您能够自由地以任意顺序运行任意测试集。如果一个测试更改了系统但没有撤销这些更改,那么除非您考虑了这种可能性,否则所有后续测试都将以失败告终。因此,需要隔离测试以消除相互的影响,从而防止这种情况的发生。

应用最佳实践。 有三种简化测试隔离的方法:

  • 撤销在测试执行前或期间所作的所有更改。
  • 在执行任何测试之前清除所有测试状态。
  • 回滚(而非提交)测试期间所作的更改。

前两种方法是关于如何管理测试固件、封装已知系统状态的对象和测试启动时的运行时环境。通过第一种方法,您一般可以使用 JUnit tearDown() 方法来重置系统状态。这种方法存在一些缺点。首先,您需要编写拆卸代码,它本身如果不经过严格测试可能会包含错误。由于这些错误会以不可预测和难以追踪的方式影响后续测试,因此这种错误极难发现。

第二个方法涉及到在测试执行之前清除所有的测试状态。该方法对不与现有数据库交互的新应用程序而言肯定特别合适。以下示例显示了如何将这种方法应用到我们测试类的 setUp() 方法中:

protected void setUp() throws Exception 
{
super.setUp();
SqlPlusRunner.runSQLPlusFile("truncate_data.sql");
assertEquals(0, SqlPlusRunner.runSQLPlusFile("persons_data.sql"));

//complete setup of the fixture by initializing DAO with connection to database
dao = new PersonDAOImpl();
dao.setConnection(connection = ConnectionUtils.getConnection());
}

注意截除数据脚本如何在加载测试数据之前运行。这就用非常少的努力保证了测试将在数据库处于可预测状态的前提下开始。除了这个明显的好处之外,它有以下缺点:

  • 各个测试类可以使用它自己的测试数据,这增加了真正的测试数据文件爆炸的可能性。如果测试操作需要处理测试与测试间变化不大的复杂数据集,那么一定程度的测试数据共享会很有好处,即使这意味着偶尔需要使用 tearDown() 方法来重置状态。
  • 有时只是不可能在每次运行之前都重置数据状态。如果您在开发一个使用了由外部 DBA 小组管理的共享旧式数据库实例的应用程序,那么这可能就是这种情况了。

第三种方法涉及到回滚而非提交在测试运行期间所作的更改。这会非常有效,其原因有两个:

  • 现代数据库(如 Oracle 数据库)提供了出色的事务支持,以便您可以仅使用 Connection.rollback(); 轻松撤销当前事务所做的所有工作。
  • 在 Oracle 数据库事务期间所作的所有更改在事务内均可见。换句话说,如果您要继续执行 Connection.commit() 调用,那么查询要提交的任何更改将非常容易。唯一的限制是您需要使用相同的 Connection 实例来查询在事务期间所作的查询更改。该策略不适于需要在执行期间提交多次的操作。

试一试。仔细查看 TestListPersons 类中的 setup() 的实现,其中将在每次测试之前加载测试数据。将类作为 Java 应用程序运行。在 Oracle JDeveloper 的输出日志中,您将看到所执行的 SQL*Plus 命令(如图 3 所示)。

图 3
图 3:作为 Java 应用程序运行的 TestListPersons 的控制台输出

最佳实践 8:分割测试套件

将您的整个测试集分割为一些个别的子套件,以使您的测试环境更加易于管理并共享代码和进程。

如果您在测试一个大型数据库驱动的系统,您可能要进行成百上千或者甚至成千上万个数据库测试。您需要将测试分割为个别的子套件以使测试环境易于管理。您可以从各种方案中进行选择来有效地将测试分组:

  • 将依赖于相同的基础测试数据集的测试分组
  • 为只读的测试与读/写测试创建单独的套件。
  • 为细粒度测试(可能直接利用 SQL 和 PreparedStatements 执行)与在 DAO 实现层执行的粗粒度测试创建单独的套件。
  • 根据应用程序的功能范围分离测试。

通过有效地分割测试,您可以创建一个更加一致和易管理的测试环境。您将有更多的机会共享测试代码、测试数据和常见的设置和拆卸操作。

应用最佳实践。JUnit suite() 方法提供了一种非常方便的分割测试套件的机制。我们的示例应用程序将整个测试套件分割为三个子套件:

public class AllTests extends TestCase 
{

public static void main(String[] args)
{
junit.textui.TestRunner.run(suite());
}

public static Test suite()
{
TestSuite suite = new TestSuite();
suite.addTest(SqlPlusBasedTests.suite());
suite.addTest(DbUnitTests.suite());
suite.addTest(TestsOfTests.suite());
return suite;
}
}

基于 SQL*Plus 的测试从使用 DbUnit 的测试中分离出来,并为本文创建了一个用于小型框架的测试套件。各个套件的设置和拆卸操作都包含在相关的 suite() 方法内。例如,SqlPlusBasedTest 实现了以下 suite() 方法:

public static Test suite()
{

TestSuite suite = new TestSuite();
suite.addTestSuite(TestListPersons.class);
... add other tests

//use test wrapper to drop and re-create tables
TestSetup wrapper = new TestSetup(suite)
{
protected void setUp()
{
SqlPlusRunner.runSQLPlusFile("drop_tables.sql");
System.out.println("Setting up all tables ...");
SqlPlusRunner.runSQLPlusFile("create_tables.sql");
}

protected void tearDown()
{
//no implementation required
}
};
return wrapper;
}

TestSetup 绕接器允许套件中的所有测试共用设置,在这种情况下将删除数据库表然后重新创建它们。

试一试。您可以通过从 Oracle JDeveloper 中打开 Java 源文件,右键单击,然后选择 Run 来运行任何测试类。安装 JUnit 后,您可以将类作为 JUnit 测试来运行。JDeveloper 将显示一个 JUnit 输出窗口,该窗口显示了测试运行的进度,并指示哪些测试失败或导致错误。图 4 显示了 SqlPlusBasedTests.java 在一些测试方法被修改为失败之后的输出。

图 4
图 4:失败测试的示例输出

佳实践 9:使用适当的框架(如 DbUnit)简化过程。

诸如 DbUnit 的框架可以简化数据库单元测试的各个方面。它一定程度上扩展了该最佳实践的定义,并建议作为一种最佳实践,您应当使用 DbUnit。这是因为 DbUnit 不是一种最佳实践,而是一种帮助简化数据库应用程序测试的 JUnit 库扩展。使用 DbUnit 的优点是它使得应用本文介绍的许多最佳实践变得非常容易。

应用最佳实践。要应用该框架,您需要对主要的 DbUnit 类和接口有基本的了解。基于 DbUnit 的测试的主要接口是 IDataSet。您可以认为 IDataSet 代表一个或多个表的数据。可以将数据库模式的全部内容表示为单个 IDataSet 实例。这些表本身由 Itable 实例来表示。

IDataSet 的实现有很多,每一个都对应一个不同的数据源或加载机制。最常用的三种 IDataSet 实现为:

  • FlatXmlDataSet:数据的简单平面文件 XML 表示
  • QueryDataSet:用 SQL 查询获得的数据
  • DatabaseDataSet:数据库表本身内容的一种表示

下面是两种真正有用的 DbUnit 特性:

  • 您可以从一种 IDataSet 实现中读取数据,并写入另一种实现。这使您能够(比如说)从平面 XML 文件中填充数据库,或者用数据库表中的数据或查询检索得到的数据创建平面 XML 文件。这使您能够非常容易地管理数据库测试环境。
  • 您可以比较包含在不同数据集中的数据的内容。利用这种功能,您可以比较数据库表的内容或利用纯文本 XML 文件的内容进行查询。这是一种强大的特性,因为它允许非常精确的测试,而不必编写很多代码来支持它。

以下示例显示了工作中的 IDataSet 比较特性:

public void testListPersons() throws Exception 
{
FlatXmlDataSet loadedDataSet = new FlatXmlDataSet(DbUnitUtils
.getDbUnitFile("persons_data.xml"));
QueryDataSet queryDataSet = new QueryDataSet(getConnection());
queryDataSet.addTable("PERSON", "SELECT * FROM PERSON ORDER BY ID");
Assertion.assertEquals(loadedDataSet, queryDataSet);
}

该功能允许您外化断言数据(在上面的示例中,断言数据被包含在 XML 文件中)。它还在测试更新操作的结果时非常有用;您可以使用查询来从比较数据中排除不相关的表和行。

后续步骤

了解关于
核心 J2EE 模式 — 数据访问对象
利用 Oracle 实体测试工具进行容器外的 EJB 3.0 测试

的更多内容

下载示例测试工具

令人遗憾的是,DbUnit 能够测试的查询类型非常有限。例如,要处理包含参数的查询,您必须扩展该框架。此外,您找不到对测试利用对象关系映射 (ORM) 库(例如 Oracle TopLink 和 Hibernate)运行的查询的任何支持。请参见“利用 DbUnit 进行数据库测试”,获得对 DbUnit 的介绍。

试一试。仔细查看并运行 TestUsingDbUnit 类,以了解使用中的 DbUnit。要运行 DbUnit 测试,您必须将 dbunit.jar 添加到您的 JDeveloper 项目中,如下所示:

从 Project Properties 对话框中,选择 Libraries -> Add Library -> New。Create Library 对话框将出现。

转至 dbUnit.jar,并将其添加到新库类路径中。输入 DbUnit 作为库名称,并将其添加到您的项目中。

总结

数据库操作在您设置和维护测试环境时提出了很大的挑战。本文介绍了可以帮助您应对这些挑战的最佳实践。通过遵循这些最佳实践,您将能更有效地编写数据库操作测试并在一个高效、一致和易维护的测试环境中执行它们。

测试 Oracle TopLink 应用程序

本文中讨论的最佳实践不仅限于基于 JDBC 的应用程序。当您使用对象关系映射 (ORM) 工具(例如 Oracle TopLink)时,可以 — 而且应当 — 同样遵循这些最佳实践。对象关系映射库正变得越来越流行,因为它们减少了典型 J2EE 应用程序所需的数据访问代码数量,并允许 Java 开发人员使用一种更直观的、面向对象的编程模型。

您可以将同样的测试集和设置脚本用于 Oracle TopLink 实现(正如您可以将它们单独用于 JDBC 一样) — 实际上,本文包含的示例项目包含了该项目的第二个版本,它正是这么做的:我们的第二个示例项目包含了 PersonDAO 接口的 TopLink 以及 JDBC 实现。该项目的源文件位于 jdbc_toplink 文件夹的 databaseunit.zip 存档中。设置该项目几乎与设置仅针对 JDBC 的项目完全相同。唯一的区别就是要编译和使用 TopLink,您还需要将 TopLink 和 Oracle XML Parser v2 库添加到项目中。

同样的测试集和设置脚本如何能够用于两种实现?答案就位于我们的测试固件(封装初始的已知系统状态和运行时环境的对象)的设置中。

请注意,在仅针对 JDBC 的实现中,测试将获取一个连接并将其传递给 setUp() 方法中的 DAO。这不适用于 TopLink 应用程序,后者使用 Session 接口而非 Connection。我们的 listAllPersons() 方法的 TopLink 实现显示了如何使用 Session

public List listAllPersons()
{
if (session == null)
throw new IllegalStateException("Session must be set before business method is called");

ReadAllQuery query = new ReadAllQuery(Person.class);
List persons = (Vector) session.executeQuery(query);
return persons;
}

释放 TopLink 资源还需要与 JDBC 的方式不同 — 使用 session.logout() 而非 Connection.close()

您可以通过为设置以及释放我们的两种 DAO 所需的资源定义接口用一种通用的方式解决这些新的测试固件处理需求。接口 DAOFixtureStrategy 显示如下:

public interface DAOFixtureStrategy
{

/**
* Called in test's setUp() method to set up connections, sessions, etc.
*/
public void setUp(DAO dao);

/**
* Called in test's teardown() method to release connections, sessions, etc.
*/
public void tearDown(DAO dao);
}

为 JDBC 和 TopLink 都提供了 DAOFixtureStrategy 的一种实现。

我们修改后的测试将在每次测试前调用 JUnit setUp() 方法中的 getDAOFixtureStrategy().setUp(dao) 来初始化 DAO(如下所示)。

protected void setUp() throws Exception
{
super.setUp();
SqlPlusRunner.runSQLPlusFile("truncate_data.sql");
assertEquals(0, SqlPlusRunner.runSQLPlusFile("persons_data.sql"));
dao = getDAO();
getDAOFixtureStrategy().setUp(dao);
}

在测试之后,测试的 tearDown() 方法中的 getDAOFixtureStrategy.tearDown() 方法将释放资源。TopLink 实现还在每次测试之后通过调用 session.getIdentityMapAccessor().initializeIdentityMaps() 方法来清除对象高速缓存。提醒:当使用这个方法时要小心。当运行测试时,确保您的应用程序没有持有由 TopLink 读取的任何对象引用,否则可能发生不可预测的结果。

系统属性 useToplink 的存在决定了为测试初始化哪种 DAO 和测试固件策略。关于更多详细信息,请参见 co.uk.realsolve.databasetest.dao.fixture 测试程序包,以及 BaseDatabaseTest 中的 getDAO()getDAOFixtureStrategy() 方法。

请注意,对 JDBC 和 TopLink 测试而言,测试固件都包含一个 DAO,它包含一个初始化为一种已知状态的数据库的资源连接(连接或会话)。JDBC 和 TopLink DAO 查询均返回域对象,因此我们的测试直接使用这些对象而不使用 SQL 或 TopLink 查询过滤表达式。

总之,要注意的重要一点是,TopLink 应用程序和 JDBC 应用程序一样易于测试。练习还证明了拥有易测试体系结构的价值(参见“最佳实践 1: 从易测试的应用程序体系结构开始”)。适当地将我们的数据库逻辑隔离在 DAO 之后使得可以将同样的测试用于这两种 API。


Phil Zoio (philzoio@realsolve.co.uk) 是一个独立的 J2EE 开发人员和顾问以及总部位于英国萨福克郡的 Realsolve Solutions 的创立者。他目前的兴趣包括敏捷开发方法、Java 开放源代码框架和持久性体系结构和技巧
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值