Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1676049
  • 博文数量: 585
  • 博客积分: 14610
  • 博客等级: 上将
  • 技术积分: 7402
  • 用 户 组: 普通用户
  • 注册时间: 2008-05-15 10:52
文章存档

2013年(5)

2012年(214)

2011年(56)

2010年(66)

2009年(44)

2008年(200)

分类:

2010-11-29 01:54:19

使用模仿对象进行单元测试 

用模仿对象替换合作者以改进单元测试
级别:中级


Alexander Day Chaffee(alex@jguru.com),顾问,Purple Technology
William Pietri(william@scissor.com),顾问,Scissor

2003 年 3 月

模仿对象(Mock object)是为起中介者作用的对象编写单元测试的有用方法。测试对象调用模仿域对象(它只断言以正确的次序用期望的参数调用了正确的方法),而不是调用实际域对象。然而,当测试对象必须创建域对象时,我们面临一个问题。测试对象如何知道创建模仿域对象,而不是创建实际域对象呢?在本文中,软件顾问 Alexander Day Chaffee 和 William Pietri 将演示一种重构技术,该技术根据工厂方法设计模式来创建模仿对象。
单元测试已作为软件开发的“最佳实践”被普遍接受。当编写对象时,还必须提供一个自动化测试类,该类包含测试该对象性能的方法、用各种参数调用其各种公用(public)方法并确保返回值是正确的。

当您正在处理简单数据或服务对象时,编写单元测试很简单。然而,许多对象依赖基础结构的其它对象或层。当开始测试这些对象时,实例化这些合作者(collaborator)通常是昂贵的、不切实际的或效率低的。

例如,要单元测试一个使用数据库的对象,安装、配置和发送本地数据库副本、运行测试然后再卸装本地数据库可能很麻烦。模仿对象提供了解决这一困难的方法。模仿对象符合实际对象的接口,但只要有足够的代码来“欺骗”测试对象并跟踪其行为。例如,虽然某一特定单元测试的数据库连接始终返回相同的硬连接结果,但可能会记录查询。只要正在被测试的类的行为如所期望的那样,它将不会注意到差异,而单元测试会检查是否发出了正确的查询。

夹在中间的模仿
使用模仿对象进行测试的常用编码样式是:

创建模仿对象的实例 
设置模仿对象中的状态和期望值 
将模仿对象作为参数来调用域代码 
验证模仿对象中的一致性 
虽然这种模式对于许多情况都非常有效,但模仿对象有时不能被传递到正在测试的对象。而设计该对象是为了创建、查找或获得其合作者。

例如,测试对象可能需要获得对 Enterprise JavaBean(EJB)组件或远程对象的引用。或者,测试对象会使用具有副作用的对象,如删除文件的 File 对象,而在单元测试中不希望有这些副作用。

根据常识,我们知道这种情形下可以尝试重构对象,使之更便于测试。例如,可以更改方法签名,以便传入合作者对象。

在 Nicholas Lesiecki 的文章“Test flexibly with AspectJ and mock objects”中,他指出重构不一定总是合意的,也不一定总是产生更清晰或更容易理解的代码。在许多情况下,更改方法签名以使合作者成为参数将会在方法的原始调用者内部产生混淆的、未经试验的代码混乱。

问题的关键是该对象“在里面”获得这些对象。任何解决方案都必须应用于这个创建代码的所有出现。为了解决这个问题,Lesiecki 使用了查找方式或创建方式。在这个解决方案中,执行查找的代码被返回模仿对象的代码自动替换。

因为 AspectJ 对于某些情况不是选项,所以我们在本文中提供了一个替代方法。因为在根本上这是重构,所以我们将遵循 Martin Fowler 在他创新的书籍“Refactoring: Improving the Design of Existing Code”(请参阅参考资料)中建立的表达约定。(我们的代码基于 JUnit — Java 编程的最流行的单元测试框架,尽管它决不是 JUnit 特定的。)

重构:抽取和覆盖工厂方法
重构是一种代码更改,它使原始功能保持不变,但更改代码设计,使它变得更清晰、更有效且更易于测试。本节将循序渐进地描述“抽取”和“覆盖”工厂方法重构。

问题:正在测试的对象创建了合作者对象。必须用模仿对象替换这个合作者。

重构之前的代码 class Application {
...
public void run() {
View v = new View();
v.display();
...



解决方案:将创建代码抽取到工厂方法,在测试子类中覆盖该工厂方法,然后使被覆盖的方法返回模仿对象。最后,如果可以的话,添加需要原始对象的工厂方法的单元测试,以返回正确类型的对象:

重构之后的代码 class Application {
...
public void run() {
View v = createView();
v.display();
...
protected View createView() {
return new View();
}
...
}



该重构启用清单 1 中所示的单元测试代码:

清单 1. 单元测试代码 class ApplicationTest extends TestCase {

MockView mockView = new MockView();

public void testApplication {
Application a = new Application() {
protected View createView() {
return mockView;
}
};
a.run();
mockView.validate();
}

private class MockView extends View
{
boolean isDisplayed = false;

public void display() {
isDisplayed = true;
}

public void validate() {
assertTrue(isDisplayed);
}
}
}



角色
该设计引入了由系统中的对象扮演的下列角色:

目标对象:正在测试的对象 
合作者对象:由目标对象创建或获取的对象 
模仿对象:遵循模仿对象模式的合作者的子类(或实现) 
特殊化对象:覆盖创建方法以返回模仿对象而不是合作者的目标的子类 
技巧
重构由许多小的技术性步骤组成。这些步骤统称为技巧。如果您象按照食谱那样严格遵循这些技术,那么您在学习重构时应该没有太大的麻烦。

标识创建或获取合作者的代码的所有出现。

将抽取方法重构应用于这个创建代码,创建工厂方法(在 Fowler 书籍的第 110 页中讨论;有关更多信息,请参阅参考资料一节)。

确保目标对象及其子类可以访问工厂方法。(在 Java 语言中,使用 protected 关键字)。

在测试代码中,创建模仿对象且实现与合作者相同的接口。

在测试代码中,创建扩展(专用于)目标对象的特殊化对象。

在特殊化对象中,覆盖创建方法以返回为测试提供的模仿对象。

可选的:创建单元测试以确保原始目标对象的工厂方法仍返回正确的非模仿对象。 
示例:ATM
设想您正在编写用于银行自动柜员机(Automatic Teller Machine)的测试。其中一个测试可能类似于清单 2:

清单 2. 初始单元测试,在模仿对象引入之前 public void testCheckingWithdrawal() {
float startingBalance = balanceForTestCheckingAccount();

AtmGui atm = new AtmGui();
insertCardAndInputPin(atm);

atm.pressButton("Withdraw");
atm.pressButton("Checking");
atm.pressButtons("1", "0", "0", "0", "0");
assertContains("$100.00", atm.getDisplayContents());
atm.pressButton("Continue");

assertEquals(startingBalance - 100,
balanceForTestCheckingAccount());
}



另外,AtmGui 类内部的匹配代码可能类似于清单 3:

清单 3. 产品代码,在重构之前 private Status doWithdrawal(Account account, float amount) {
Transaction transaction = new Transaction();
transaction.setSourceAccount(account);
transaction.setDestAccount(myCashAccount());
transaction.setAmount(amount);
transaction.process();
if (transaction.successful()) {
dispense(amount);
}
return transaction.getStatus();
}



该方法将起作用,遗憾的是,它有一个副作用:支票帐户余额比测试开始时少,这使得其它测试变得更困难。有一些解决这种困难的方法,但它们都会增加测试的复杂性。更糟的是,该方法还需要对管理货币的系统进行三次往返。

要修正这个问题,第一步是重构 AtmGui 以允许我们用模仿事务替换实际事务,如清单 4 中所示(比较粗体的源代码以查看我们正在更改什么):

清单 4. 重构 AtmGui private Status doWithdrawal(Account account, float amount) {
Transaction transaction = createTransaction();
transaction.setSourceAccount(account);
transaction.setDestAccount(myCashAccount());
transaction.setAmount(amount);
transaction.process();
if (transaction.successful()) {
dispense(amount);
}
return transaction.getStatus();
}
protected Transaction createTransaction() {
return new Transaction();
}



后退到测试类内部,我们将 MockTransaction 类定义为成员类,如清单 5 中所示:

清单 5. 将 MockTransaction 定义为成员类 private MockTransaction extends Transaction {

private boolean processCalled = false;

// override process method so that no real work is done
public void process() {
processCalled = true;
setStatus(Status.SUCCESS);
}

public void validate() {
assertTrue(processCalled);
}
}



最后,我们可以重写测试,以便被测试的对象使用 MockTransaction 类,而不是使用实际类,如清单 6 中所示:

清单 6. 使用 MockTransaction 类 MockTransaction mockTransaction;

public void testCheckingWithdrawal() {
mockTransaction = new MockTransaction();

AtmGui atm = new AtmGui() {
protected Transaction createTransaction() {
return mockTransaction;
}
};

insertCardAndInputPin(atm);

atm.pressButton("Withdraw");
atm.pressButton("Checking");
atm.pressButtons("1", "0", "0", "0", "0");
assertContains("$100.00", atm.getDisplayContents());
atm.pressButton("Continue");
assertEquals(100.00, mockTransaction.getAmount());
assertEquals(TEST_CHECKING_ACCOUNT,
mockTransaction.getSourceAccount());
assertEquals(TEST_CASH_ACCOUNT,
mockTransaction.getDestAccount());
mockTransaction.validate();
}



该解决方案产生了一个稍长的测试,但该测试只关注正在测试的类的直接行为,而不是 ATM 接口之外整个系统的行为。也就是说,我们不再检查测试帐户的最终余额是否正确;我们将在对 Transaction 对象的单元测试中检查该函数,而不是在对 AtmGui 对象的单元测试中。

注:根据模仿对象的创造者所说,它应该在其 validate() 方法内部执行自己的所有验证。在本示例中,为了清晰起见,我们将验证的某些部分放在了测试方法内部。随着您更加熟练地使用模仿对象,对于将多少验证职责代理给模仿对象,您将会深有体会。

内部类魔法
在清单 6 中,我们使用了 AtmGui 的匿名内部子类来覆盖 createTransaction 方法。因为我们只需要覆盖一个简单的方法,所以这是实现我们目标的简明方法。如果我们覆盖多个方法或在许多测试之间共享 AtmGui 子类,那么创建一个完整的(非匿名)成员类是值得的。

我们还使用了实例变量来存储对模仿对象的引用。这是在测试方法和特殊化类之间共享数据的最简单方法。这是可以接受的,因为我们的测试框架不是多线程的或可重入的。(如果它是多线程的或可重入的,则必须用 synchronized 块保护我们自己。)

最后,我们将模仿对象本身定义为测试类的专用内部类 — 这通常是一种便利的方法,因为将模仿对象就放在使用它的测试代码旁边会更加清楚,又因为内部类有权访问包含它们的类的实例变量。

小心不出大错
因为我们覆盖了工厂方法来编写这个测试,所以其结果是:我们的测试不再包括任何原始创建代码(现在它在基类的工厂方法内部)。添加确实包括该代码的测试也许是有益的。这与调用基类的工厂方法并断言返回对象具有正确类型一样简单。例如:

AtmGui atm = new AtmGui();
Transaction t = atm.createTransaction();
assertTrue(!(t instanceof MockTransaction));



注:相反,assertTrue(t instanceof Transaction) 不能满足,因为 MockTransaction 也是 Transaction。

从工厂方法到抽象工厂
此时,您可能很想更进一步并用成熟的抽象工厂对象替换工厂方法,如 Erich Gamma 等人在设计模式中详细描述的那样。(请参阅参考资料)。实际上,许多人已经用工厂对象来着手这种方法,而不是用工厂方法 — 我们以前是这样做的,但很快就放弃了。

将第三种对象类型(角色)引入系统会有一些潜在的缺点:

它增加了复杂性,而没有相应地增加功能。

它会迫使您更改目标对象的公用接口。如果必须传入抽象工厂对象,那么您必须添加一个新的公用构造函数或赋值(mutator)方法。

许多语言对于“工厂”这一概念都附有一些约定,它们会使您误入歧途。例如,在 Java 语言中,工厂通常作为静态方法实现;在这种情况下,这是不合适的。 
请记住,本练习的宗旨是使对象更易于测试。通常,用于可测性的设计可以将对象的 API 推向一种更清晰更模块化的状态。但它会走得太远。测试驱动的设计更改不应该污染原始对象的公用接口。

在 ATM 示例中,对于产品代码,AtmGui 对象始终只产生一种类型的 Transaction 对象(实际类型)。测试代码希望它产生另一种类型的对象(模仿对象)。但强迫公用 API 适应工厂对象或抽象工厂(只因为测试代码要求它这样)是错误的设计。如果产品代码无需实例化该合作者的多个类型,那么添加该功能将使最终的设计不必要地变得难于理解。 




• 
作者Here is English Version [Re:palatum]
palatum 
 


CJSDN高级会员
 

发贴: 451 
积分: 80 
于 2003-03-21 17:44 user profilesend a private message to usersend email to palatumsearch all posts byselect and copy to clipboard. 
ie only, sorry for netscape users:-)add this post to my favorite list
Unit testing with mock objects 

Improve your unit tests by replacing your collaborators with mock objects
Level: Intermediate


Alexander Day Chaffee (alex@jguru.com), Consultant, Purple Technology
William Pietri (william@scissor.com), Consultant, Scissor

November 2002

Mock objects are a useful way to write unit tests for objects that act as mediators. Instead of calling the real domain objects, the tested object calls a mock domain object that merely asserts that the correct methods were called, with the expected parameters, in the correct order. However, when the tested object must create the domain object, we are faced with a problem. How does the tested object know to create a mock domain object instead of the true domain object? In this article, software consultants Alexander Day Chaffee and William Pietri present a refactoring technique to create mock objects based on the factory method design pattern.
Unit testing has become widely accepted as a "best practice" for software development. When you write an object, you must also provide an automated test class containing methods that put the object through its paces, calling its various public methods with various parameters and making sure that the values returned are appropriate.

When you're dealing with simple data or service objects, writing unit tests is straightforward. However, many objects rely on other objects or layers of infrastructure. When it comes to testing these objects, it is often expensive, impractical, or inefficient to instantiate these collaborators.

For example, to unit test an object that uses a database, it may be burdensome to install, configure, and seed a local copy of the database, run your tests, then tear the local database down again. Mock objects provide a way out of this dilemma. A mock object conforms to the interface of the real object, but has just enough code to fool the tested object and track its behavior. For example, a database connection for a particular unit test might record the query while always returning the same hardwired result. As long as the class being tested behaves as expected, it won't notice the difference, and the unit test can check that the proper query was emitted.

Mock in the middle
The common coding style for testing with mock objects is to:

Create instances of mock objects 
Set state and expectations in the mock objects 
Invoke domain code with mock objects as parameters 
Verify consistency in the mock objects 
While this pattern is very effective for many cases, sometimes the mock object cannot be passed into the object being tested. Instead, that object is designed to either create, look up, or otherwise obtain its collaborator.

For instance, the tested object may need to obtain a reference to an Enterprise JavaBean (EJB) component or remote object. Or the tested object may make use of objects with side effects that may not be desirable in unit testing, like File objects that delete files.

Common wisdom suggests that this situation provides an opportunity to refactor your object to make it more test-friendly. For instance, you may change the method signature so that the collaborator object is passed in.

In his article "Test flexibly with AspectJ and mock objects," Nicholas Lesiecki points out that refactoring is not always desirable, nor does it always result in code that is cleaner or easier to understand. In many cases, changing the method signature so the collaborator becomes a parameter will result in a confusing, untested snarl of code inside the method's original callers.

The heart of the problem is that the object is obtaining these objects "on the inside." Any solution must apply to all occurrences of this creation code. To solve this problem, Lesiecki uses a lookup aspect or a creation aspect. In this solution, the code that performs the lookup is replaced automatically with code that returns a mock object instead.

Because AspectJ is not an option for some, we offer an alternative approach in this article. Because this is, at root, a refactoring, we will follow the presentation convention established by Martin Fowler in his seminal book, Refactoring: Improving the Design of Existing Code (see Resources). (Our code is based on JUnit, the most popular unit testing framework for Java programming, though it is by no means JUnit-specific.)

Refactoring: Extract and override factory method
A refactoring is a code change that leaves the original functionality intact, but changes the design of the code so that it's cleaner, more efficient, and easier to test. This section offers a step-by-step description of the Extract and Override factory method refactoring.

Problem: The object being tested creates a collaborator object. This collaborator must be replaced with a mock object.

Code before refactoring class Application {
...
public void run() {
View v = new View();
v.display();
...



Solution: Extract the creation code into a factory method, override this factory method in a test subclass, and then make the overridden method return a mock object instead. Finally, if practical, add a unit test that requires the original object's factory method to return an object of the correct type:

Code after refactoring class Application {
...
public void run() {
View v = createView();
v.display();
...
protected View createView() {
return new View();
}
...
}



This refactoring enables the unit test code shown in Listing 1:

Listing 1. Unit test code class ApplicationTest extends TestCase {

MockView mockView = new MockView();

public void testApplication {
Application a = new Application() {
protected View createView() {
return mockView;
}
};
a.run();
mockView.validate();
}

private class MockView extends View
{
boolean isDisplayed = false;

public void display() {
isDisplayed = true;
}

public void validate() {
assertTrue(isDisplayed);
}
}
}



Roles
This design introduces the following roles played by objects in the system:

Target object: The object being tested 
Collaborator object: The object created or obtained by the target 
Mock object: A subclass (or implementation) of the collaborator that follows the mock object pattern 
Specialization object: A subclass of the target that overrides the creation method to return a mock instead of a collaborator 
Mechanics
A refactoring consists of many small, technical steps. Together, these are called the mechanics. If you follow the mechanics closely like a cookbook recipe, you should be able to learn the refactoring without much trouble.

Identify all occurrences of code that create or obtain the collaborator.

Apply the Extract Method refactoring to this creation code, creating the factory method (discussed on page 110 of Fowler's book; see the Resources section for more information).

Assure that the factory method is accessible to the target object and its subclasses. (in the Java language, use the protected keyword).

In your test code, create a mock object implementing the same interface as the collaborator.

In your test code, create a specialization object that extends (specializes) the target.

In the specialization object, override the creation method to return a mock object that accommodates your test.

Optional: create a unit test to assure that the original target object's factory method still returns the correct, non-mock object. 
Example: ATM
Imagine you are writing the tests for a bank's Automatic Teller Machine. One of those tests might look like Listing 2:

Listing 2. Initial unit test, before mock object introduction public void testCheckingWithdrawal() {
float startingBalance = balanceForTestCheckingAccount();

AtmGui atm = new AtmGui();
insertCardAndInputPin(atm);

atm.pressButton("Withdraw");
atm.pressButton("Checking");
atm.pressButtons("1", "0", "0", "0", "0");
assertContains("$100.00", atm.getDisplayContents());
atm.pressButton("Continue");

assertEquals(startingBalance - 100,
balanceForTestCheckingAccount());
}



In addition, the matching code inside the AtmGui class might look like Listing 3:

Listing 3. Production code, before refactoring private Status doWithdrawal(Account account, float amount) {
Transaction transaction = new Transaction();
transaction.setSourceAccount(account);
transaction.setDestAccount(myCashAccount());
transaction.setAmount(amount);
transaction.process();
if (transaction.successful()) {
dispense(amount);
}
return transaction.getStatus();
}



This approach will work, but it has an unfortunate side effect: the checking account balance is lower than when the test started, making other testing more difficult. There are ways to solve that, but they all increase the complexity of the tests. Worse, this approach also requires three round trips to the system in charge of the money.

To fix this problem, the first step is to refactor AtmGui to allow us to substitute a mock transaction for the real transaction, as shown in Listing 4 (compare the boldface source code to see what we're changing):

Listing 4. Refactoring AtmGui private Status doWithdrawal(Account account, float amount) {
Transaction transaction = createTransaction();
transaction.setSourceAccount(account);
transaction.setDestAccount(myCashAccount());
transaction.setAmount(amount);
transaction.process();
if (transaction.successful()) {
dispense(amount);
}
return transaction.getStatus();
}
protected Transaction createTransaction() {
return new Transaction();
}



Back inside the test class, we define the MockTransaction class as a member class, as shown in Listing 5:

Listing 5. Defining MockTransaction as a member class private MockTransaction extends Transaction {

private boolean processCalled = false;

// override process method so that no real work is done
public void process() {
processCalled = true;
setStatus(Status.SUCCESS);
}

public void validate() {
assertTrue(processCalled);
}
}



And finally, we can rewrite our test so that the tested object uses the MockTransaction class rather than the real one, as shown in Listing 6.

Listing 6. Using the MockTransaction class MockTransaction mockTransaction;

public void testCheckingWithdrawal() {
mockTransaction = new MockTransaction();

AtmGui atm = new AtmGui() {
protected Transaction createTransaction() {
return mockTransaction;
}
};

insertCardAndInputPin(atm);

atm.pressButton("Withdraw");
atm.pressButton("Checking");
atm.pressButtons("1", "0", "0", "0", "0");
assertContains("$100.00", atm.getDisplayContents());
atm.pressButton("Continue");
assertEquals(100.00, mockTransaction.getAmount());
assertEquals(TEST_CHECKING_ACCOUNT,
mockTransaction.getSourceAccount());
assertEquals(TEST_CASH_ACCOUNT,
mockTransaction.getDestAccount());
mockTransaction.validate();
}



This solution yields a test that is slightly longer, but is only concerned with the immediate behavior of the class being tested, rather than the behavior of the entire system that lies beyond the ATM's interface. That is, we no longer check that the final balance of the test account is correct; we would check that function in the unit test for the Transaction object, not the AtmGui object.

Note: According to its inventors, a mock object is supposed to perform all of its own validation inside its validate() method. In this example, for clarity, we left some of the validation inside the test method. As you grow more comfortable using mock objects, you will develop a feel for how much validation responsibility to delegate to the mock.

Inner class magic
In Listing 6, we used an anonymous inner subclass of AtmGui to override the createTransaction method. Because we only had to override one simple method, this was a concise way to achieve our goal. If we were overriding multiple methods or sharing the AtmGui subclass between many tests, it could pay to create a full (non-anonymous) member class.

We also used an instance variable to store a reference to the mock object. This is the simplest way to share data between the test method and the specialization class. It is acceptable because our test framework is not multithreaded or reentrant. (If it were, we would have to protect ourselves with synchronized blocks.)

Finally, we defined the mock object itself as a private inner class of the test class -- often a convenient approach because it is clearer to put the mock right next to the test code that uses it, and because inner classes have access to the instance variables of their surrounding class.

Better safe than sorry
Because we overrode the factory method to write this test, it turns out that we no longer have any test coverage of the original creation code (which is now inside the base class's factory method). It may be beneficial to add a test that covers this code explicitly. This can be as simple as invoking the base class's factory method and asserting that the returned object is of the correct type. For example:

AtmGui atm = new AtmGui();
Transaction t = atm.createTransaction();
assertTrue(!(t instanceof MockTransaction));



Note that the inverse, assertTrue(t instanceof Transaction) would not suffice, because a MockTransaction is a Transaction as well.

From factory method to abstract factory
At this point, you may be tempted to go one step further and replace the factory method with a full-fledged abstract factory object, as detailed in Design Patterns by Erich Gamma, et al. (see Resources). In fact, many would have started this approach with a factory object, rather than a factory method -- we did, but soon backed away.

Introducing a third object type (role) into the system has some potential disadvantages:

It increases the complexity without a corresponding increase in functionality.

It may force you to change the public interface to the target object. If an abstract factory object must be passed in, then you must add a new public constructor or mutator.

Many languages have conventions attached to the concept of "factory" that may lead you astray. For instance, in the Java language, factories are often implemented as static methods; this is not appropriate in this situation. 
Remember, the whole point of this exercise is to make the object easier to test. Often, designing for testability can push the object's API toward a cleaner, more modular state. But it can be taken too far. Test-driven design changes should not pollute the public interface of the original object.

In the ATM example, as far as the production code is concerned, the AtmGui object only ever makes the one type of Transaction object (the real kind). The test code would like it to produce a different type (a mock). But forcing the public API to accommodate factory objects or abstract factories, just because the test code wants it to, is the wrong design. If production code has no need to instantiate many types of this collaborator, then adding that ability will make the resulting design needlessly hard to understand. 

阅读(715) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~