通过向组件接口发送输入、等待组件处理它们、然后检查结果的方法测试组件。在其处理过程中,某个组件很可能使用其他组件,方法是发送输入给它们并使用它们的结果:
图 1:测试已实施的组件
这些其他组件可能引起测试问题:
-
它们可能尚未实施。
-
它们可能有缺陷,阻止您的测试工作或使您花费很多时间来发现测试故障并不是由您的组件所引起的。
-
它们可能使您难以运行所需的测试。如果组件是一个商业数据库,您的公司可能没有为每个人准备足够的浮动许可证。或某一组件可能是只在单独的实验室中、在计划好的时间内才可用的硬件。
-
它们可能使测试变得非常缓慢,以至不能运行足够的测试。例如,初始化数据库可能花费每个测试五分钟。
-
可能难以使组件产生某些结果。例如,您可能希望每个写入磁盘的方法都能处理“磁盘已满”错误。但您如何确保在调用该方法时磁盘已满呢?
为避免这些问题,可以选择使用存根组件(也称为模仿对象)。存根组件的行为与真实组件类似,至少对响应其测试时组件发送给它们的那些值如此。它们可能做得更好:它们可能是一般用途的仿真器,尽力如实地模仿组件的大多数或全部行为。例如,为硬件构建一个软件仿真器通常是个很好的策略。它们的行为就和硬件一样,只是较慢。它们很有用是因为它们支持更好地调试、它们有更多的副本可用并且可以在硬件完成之前先使用它们。
图 2:通过完成一个已实施的组件所依赖的存根组件来测试已实施的组件
存根有两个缺点。
-
构建它们会很昂贵。(特别是仿真器情况。)因为它们本身是软件,所以也需要维护。
-
它们可能会屏蔽错误。例如,假设您的组件使用三角函数,但还没有库可用。您的三个测试用例求三个角度的正弦值:10 度、45 度和 90
度。您使用计算器找到正确的值,然后构建一个存根分别返回其正弦值:0.173648178、0.707106781 和 1.0。所有都好,直到您将组件与真正的三角函数库集成,该库的正弦函数使用弧度实参,因此返回
-0.544021111、0.850903525 和 0.893996664。这是您代码中的一个缺陷并将在以后被发现,发现它需要付出更多努力。
除非因为真实组件尚不可用而构建存根,否则应在部署后保留它们。它们所支持的测试在产品维护期间可能非常重要。因此编写存根需要比一次性代码有更高的标准。虽然它们不需要符合产品代码标准(例如,大多数不需要它们自己的测试套件),但是以后随着产品组件的更改,开发人员将必须维护它们。如果太难以维护,则应丢弃该存根并将丢失对它们的所有投入。
特别是当要保留它们时,存根改变了组件设计。例如,假设您的组件将使用数据库来持久存储键/值对。考虑两个设计场景:
场景 1:数据库用于测试及常规用法。不需要对组件隐藏数据库的存在。您可能使用数据库名称初始化组件:
public Component(
String databaseURL) { try { databaseConnection = DriverManager.getConnection(databaseURL); ... } catch (SQLException e) {...} }
而且,您虽然不希望在每个读取或写入值的地方构造 SQL 语句,但是肯定会有一些包含 SQL 的方法。例如,需要值的组件代码可能会调用该组件方法:
public String get(String key) { try { Statement stmt = databaseConnection.createStatement(); ResultSet rs = stmt.executeQuery(
"SELECT value FROM Table1 WHERE key=" + key); ... } catch (SQLException e) {...} }
场景 2:对于测试,使用存根替换该数据库。无论是对真实数据库还是存根运行组件代码,该代码都应该一致。所以需要将它编写为使用抽象接口方法:
interface KeyValuePairs { String
get(String key); void
put(String key, String value); }
测试时应使用诸如散列表之类的简单内容实施 KeyValuePairs:
class FakeDatabase implements KeyValuePairs { Hashtable table = new Hashtable(); public String
get(String key) { return (String) table.get(key); } public void
put(String key, String value) { table.put(key, value); } }
当测试中未使用它时,该组件将使用适配器对象,该适配器对象用于将对 KeyValuePairs 接口的调用转换为 SQL
语句:
class DatabaseAdapter implements KeyValuePairs { private Connection databaseConnection; public DatabaseAdapter(String databaseURL) { try { databaseConnection = DriverManager.getConnection(databaseURL); ... } catch (SQLException e) {...} } public String
get(String key) { try { Statement stmt = databaseConnection.createStatement(); ResultSet rs = stmt.executeQuery( "SELECT value FROM Table1 WHERE key=" + key); ... } catch (SQLException e) {...} } public void
put(String key, String value) { ... } }
您的组件可能有用于测试和其他客户端的单个构造函数。该构造函数将采用实施 KeyValuePairs 的对象。或它可能提供仅用于测试的接口,需要组件的一般客户端传入数据库名称:
class Component {
public Component(String databaseURL) { this.valueStash = new DatabaseAdapter(databaseURL); } // For testing.
protected Component(KeyValuePairs valueStash) { this.valueStash = valueStash; } }
这样,从客户端程序员的视角,两个设计场景产生相同的 API,但一个更易于测试。(注意某些测试可能使用真实数据库,而另一些可能使用存根数据库。)
与存根有关的更多信息,请参阅以下内容:
|