扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
数据库操作测试:什么是数据库操作测试,为什么需要这种测试
测试 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 表中)返回具有指定名称或指定最小年龄值的所有行。确定您是否在您自己的类中正确实现了该方法的测试必须考虑几个问题:
因此,即使一个简单的 DAO 方法也会有许多可能的结果和错误情形。为确保应用程序正确运行,应当测试每一种可能的结果和错误情形。在大多数情形下,您将希望这些测试能够与数据库交互并使用真实的数据 — 纯粹在个别类级别上进行的测试或使用模拟对象来模拟数据库相关性是不够的。数据库测试对于读/写操作同样重要,特别是那些在数据库上应用了许多更改的操作 — 这是 PL/SQL 存储过程非常常见的情况。
最关键的是:只有通过大量坚实的数据库测试,您才能验证这些操作是否正确运行。
本文中的最佳实践尤其与设计专门针对这些类型的数据访问挑战的测试相关。测试必须能够发现在数据访问抽象层中出现的数据检索和修改中的不明显错误。本文着重介绍数据库操作测试 — 用于负责持久数据访问和操作的 J2EE 应用程序层的测试。该层通常被封装在 DAO 中,后者将持久性机制从应用程序的其余部分中隐藏了起来。
最佳实践列表
在逐步了解这些最佳实践之前,请下载示例测试工具(参见“安装和使用最佳实践测试工具”边条)并利用 Oracle JDeveloper 按照说明进行操作。
以下列表列举了在本文中介绍的测试最佳实践:
最佳实践 1: 从“易测试的”应用程序体系结构开始。
最佳实践 2:使用精确的断言。
最佳实践 3:外化断言数据。
最佳实践 4:编写全面的测试。
最佳实践 5:创建稳定、有意义的测试数据集。
最佳实践 6:创建专用的测试库。
最佳实践 7:有效地隔离测试。
最佳实践 8:分割测试套件。
最佳实践 9:使用适当的框架(如 DbUnit)简化过程。
建立示例工程后,就该开始研究这些最佳实践了。
选择合适的应用程序体系结构以确保可以轻松地测试应用程序。在测试驱动的开发 (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 voidpopulate
setConnection(Connection conn)
{
this.connection = conn;
}
请注意,测试(而不是 DAO)负责获取连接。
更一般的情况是,现实中的 DAO 可能由一个 DataSource 实例而非 Connection 本身填充。在任意一种情况下都不存在对查找机制的相关性。
为了创建易测试的体系结构,建议配置您的应用程序来使用相关性注入 (DI) 容器。DI 容器将鼓励您以消除不必要相关性的方式编写应用程序。
试一试。安装 JDBC 和 TopLink 项目。(查看“测试 Oracle TopLink 应用程序”边条,了解安装信息。)为了运行 TopLink 测试,设置系统属性 useToplink。打开 Project Properties 对话框,选择 Run/Debug,然后在 Java Options 域中输入 -DuseToplink=true(如图 1 所示)。
|
最佳实践 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 文件中查找该属性。
这是一种相当方便的搜索方案,这是因为它使您能够将断言数据分组为特定测试类专有的项,它们可以在一个程序包的测试类间共享,也可以在应用程序的所有测试类间共享。
试一试。比较 TestListPersons 和 TestListPersonsExternal 的内容。请注意,除了 TestListPersonsExternal 使用了外化的断言数据外,这两个类几乎完全一样。
最佳实践 4:编写全面的测试
您的测试套件应当是全面的,要覆盖所有的数据库操作,并允许尽可能与实际约束一样多的情景。如果一组测试覆盖了您能够合理预期将在应用程序中实际发生的所有情景,那么它就是全面的。给定一组完整的先决条件而不只是最明显的先决条件,我们的测试需要验证每一个方法是否正确地工作。在我们的 listPersons() 示例中,不难想到一些可能影响方法行为方式的其他执行前状态:
在每种情况下,您都要验证是否都返回正确的数据。与精确测试使我们能够验证一种操作在给定先决条件下是否正确工作的方式相同,全面测试确定了所有操作在所有合理预期的先决条件下是否正确工作。
应用最佳实践。要使您的测试环境全面,首先应想象所有可能的情景。在测试优先的开发传统中,这将在要测试的方法实现期间甚至之前完成。其次,需要主观判断每种情景发生的可能性,以确定是否将其加入到测试套件中。极限编程 (XP) 风格的配对编程非常适用于这两种任务 — 在想像可能的情景以及判断要实际实施哪些测试时两个臭皮匠顶个诸葛亮。
不要总想着使用 Java 代码覆盖工具的输出作为您数据库测试覆盖完整的证据。对数据库测试而言,代码覆盖是必要非充分的。在一条 SQL 语句中可以包含相当复杂的数据检索逻辑,且每条语句都包含几种可分别测试的情景。
正如您可能已经体验到的,建立全面的测试是编写测试时最困难的技巧之一。对几乎所有项目而言,它都只是一个理想而非现实。开发人员需要在一方面覆盖相关的测试情景与另一方面由于测试的负担而变得无法承受之间找到合适的平衡。
在附带的源代码中,TestListPersons 类包含用于 listPersons() 方法的一组相当全面的测试的一个示例,有兴趣的读者有空时可仔细研读一下。
试一试。在 Oracle JDeveloper 中,打开 PersonDAOImpl 的实现类以及测试类 TestListPersons。试着在实现中找到一种没有为任何测试所覆盖的情景。
最佳实践 5:创建稳定、有意义的测试数据集
花些时间和精力创建一个稳定、有意义的测试数据集,您的测试将以该测试数据集为基础。 有了好的测试数据,编写数据库测试也就容易得多了。开发人员知道他们会得到所需的数据,因此也就可以信心十足地作出精确断言。
创建测试数据是在 TDD 环境中进行数据库开发的一个基础。对数据库操作而言,开发应是测试数据驱动的。在为用例编写测试之前,构建一个数据集。测试数据集并不总是需要很大;它只需足够覆盖与要测试的操作相关的情景。
创建测试数据可能是一项单调乏味的工作,但值得投入时间和脑力创建有意义且结构合理的数据。如果您在项目的早期就创建了高质量的数据,那么您将发现当您向应用程序中添加新特性和测试时可以更容易地更改、扩展或重用您的工作成果。好的测试数据对用户界面测试也极其有用。
应用最佳实践。您可以通过输入 SQL INSERT 语句或使用工具将数据导入成这种格式来轻松地创建测试数据。然而,为了实现测试自动化,您将需要一种易于从 JUnit 测试案例内部访问的数据插入和删除机制。您可以使用以下任一方式:
在我们的示例应用程序中,您使用从 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 单独运行。
|
如果您不应用它. . . 如果不保证在项目的早期创建有意义的数据,那么您将发现自己不得不重复地(经常是没有必要地)为新测试创建新数据,从而导致易可管理性和强健性较差的测试环境。
最佳实践 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());
}
注意截除数据脚本如何在加载测试数据之前运行。这就用非常少的努力保证了测试将在数据库处于可预测状态的前提下开始。除了这个明显的好处之外,它有以下缺点:
第三种方法涉及到回滚而非提交在测试运行期间所作的更改。这会非常有效,其原因有两个:
试一试。仔细查看 TestListPersons 类中的 setup() 的实现,其中将在每次测试之前加载测试数据。将类作为 Java 应用程序运行。在 Oracle JDeveloper 的输出日志中,您将看到所执行的 SQL*Plus 命令(如图 3 所示)。
|
最佳实践 8:分割测试套件
将您的整个测试集分割为一些个别的子套件,以使您的测试环境更加易于管理并共享代码和进程。
如果您在测试一个大型数据库驱动的系统,您可能要进行成百上千或者甚至成千上万个数据库测试。您需要将测试分割为个别的子套件以使测试环境易于管理。您可以从各种方案中进行选择来有效地将测试分组:
通过有效地分割测试,您可以创建一个更加一致和易管理的测试环境。您将有更多的机会共享测试代码、测试数据和常见的设置和拆卸操作。
应用最佳实践。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 在一些测试方法被修改为失败之后的输出。
|
最佳实践 9:使用适当的框架(如 DbUnit)简化过程。
诸如 DbUnit 的框架可以简化数据库单元测试的各个方面。它一定程度上扩展了该最佳实践的定义,并建议作为一种最佳实践,您应当使用 DbUnit。这是因为 DbUnit 不是一种最佳实践,而是一种帮助简化数据库应用程序测试的 JUnit 库扩展。使用 DbUnit 的优点是它使得应用本文介绍的许多最佳实践变得非常容易。
应用最佳实践。要应用该框架,您需要对主要的 DbUnit 类和接口有基本的了解。基于 DbUnit 的测试的主要接口是 IDataSet。您可以认为 IDataSet 代表一个或多个表的数据。可以将数据库模式的全部内容表示为单个 IDataSet 实例。这些表本身由 Itable 实例来表示。
IDataSet 的实现有很多,每一个都对应一个不同的数据源或加载机制。最常用的三种 IDataSet 实现为:
下面是两种真正有用的 DbUnit 特性:
以下示例显示了工作中的 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 文件中)。它还在测试更新操作的结果时非常有用;您可以使用查询来从比较数据中排除不相关的表和行。
令人遗憾的是,DbUnit 能够测试的查询类型非常有限。例如,要处理包含参数的查询,您必须扩展该框架。此外,您找不到对测试利用对象关系映射 (ORM) 库(例如 Oracle TopLink 和 Hibernate)运行的查询的任何支持。请参见“利用 DbUnit 进行数据库测试”,获得对 DbUnit 的介绍。
试一试。仔细查看并运行 TestUsingDbUnit 类,以了解使用中的 DbUnit。要运行 DbUnit 测试,您必须将 dbunit.jar 添加到您的 JDeveloper 项目中,如下所示:
从 Project Properties 对话框中,选择 Libraries -> Add Library -> New。Create Library 对话框将出现。
转至 dbUnit.jar,并将其添加到新库类路径中。输入 DbUnit 作为库名称,并将其添加到您的项目中。
总结
数据库操作在您设置和维护测试环境时提出了很大的挑战。本文介绍了可以帮助您应对这些挑战的最佳实践。通过遵循这些最佳实践,您将能更有效地编写数据库操作测试并在一个高效、一致和易维护的测试环境中执行它们。
查看本文来源如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者