嘲笑单身人士班

时间:2010-02-20 12:37:24

标签: java unit-testing oop

我最近读过,制作类单例使得无法模拟类的对象,这使得测试其客户端变得困难。我无法立即理解其根本原因。有人可以解释为什么不可能嘲笑单身人士课程?另外,还有更多问题与制作班级单身人士相关吗?

6 个答案:

答案 0 :(得分:56)

当然,我可以写一些像不要使用单身,它们是邪恶的,使用Guice / Spring /无论,但首先,这不会回答你的问题,其次,你有时< strong>必须处理单例,例如使用遗留代码时。

所以,我们不讨论单身人士的好坏(这里还有另一个question),但让我们看看如何在测试过程中处理它们。首先,让我们看一下单例的常见实现:

public class Singleton {
    private Singleton() { }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public String getFoo() {
        return "bar";
    }
}

这里有两个测试问题:

  1. 构造函数是私有的,所以我们不能扩展它(我们无法在测试中控制实例的创建,但是,这就是单例的重点)。

  2. getInstance是静态的,因此很难使用单例在代码中注入伪造而不是单例对象

  3. 对于基于继承和多态的模拟框架,这两点显然都是大问题。如果您可以控制代码,一个选项是通过添加一个允许调整内部字段的setter来使您的单例“更易于测试”,如Learn to Stop Worrying and Love the Singleton中所述(您甚至不需要一个模拟框架)案件)。如果不这样做,基于拦截和AOP概念的现代模拟框架可以克服前面提到的问题。

    例如,Mocking Static Method Calls显示了如何使用JMockit Expectations模拟单身人士。

    另一种选择是使用PowerMock,Mockito或JMock的扩展,它允许模拟通常不可模仿的东西,如静态,最终,私有或构造方法。您还可以访问类的内部。

答案 1 :(得分:14)

模拟单身人士的最佳方法是根本不使用它们,或者至少不使用传统意义。您可能想要查看的一些实践是:

  • 编程到接口
  • 依赖注入
  • 控制倒置

所以,不要像你这样访问一个人:

Singleton.getInstance().doSometing();

...将您的“单身人士”定义为一个界面,并让其他东西管理它的生命周期并将其注入您需要的地方,例如作为私有实例变量:

@Inject private Singleton mySingleton;

然后,当您单元测试依赖于单例的类/组件/等时,您可以轻松地注入它的模拟版本。

大多数依赖注入容器都会让你将组件标记为“singleton”,但是由容器来管理它。

使用上述实践可以更轻松地对代码进行单元测试,并让您专注于功能逻辑而不是布线逻辑。它还意味着您的代码真正开始成为真正的面向对象,因为任何静态方法(包括构造函数)的使用都是程序性的。因此,您的组件也开始变得真正可重用。

将Google Guice视为10的首发

http://code.google.com/p/google-guice/

你也可以看看可以做这种事情的Spring和/或OSGi。那里有很多IOC / DI东西。 :)

答案 2 :(得分:13)

根据定义,Singleton只有一个实例。因此,它的创造受到阶级本身的严格控制。通常它是一个具体的类,而不是一个接口,由于它的私有构造函数,它不是子类。此外,它的客户(通过调用Singleton.getInstance()或等效的)主动发现它,因此您不能轻易使用例如Dependency Injection用模拟实例替换它的“真实”实例:

class Singleton {
    private static final myInstance = new Singleton();
    public static Singleton getInstance () { return myInstance; }
    private Singleton() { ... }
    // public methods
}

class Client {
    public doSomething() {
        Singleton singleton = Singleton.getInstance();
        // use the singleton
    }
}

对于模拟,理想情况下,您需要一个可以自由子类化的接口,并通过依赖注入将其具体实现提供给其客户端。

您可以通过

放松Singleton实现以使其可测试
  • 提供可以由模拟子类以及“真实”
  • 实现的接口
  • 添加setInstance方法以允许替换单元测试中的实例

示例:

interface Singleton {
    private static final myInstance;
    public static Singleton getInstance() { return myInstance; }
    public static void setInstance(Singleton newInstance) { myInstance = newInstance; }
    // public method declarations
}

// Used in production
class RealSingleton implements Singleton {
    // public methods
}

// Used in unit tests
class FakeSingleton implements Singleton {
    // public methods
}

class ClientTest {
    private Singleton testSingleton = new FakeSingleton();
    @Test
    public void test() {
        Singleton.setSingleton(testSingleton);
        client.doSomething();
        // ...
    }
}

如您所见,您只能通过破坏Singleton的“清洁度”来使您的Singleton使用代码单元可测试。最后,如果你能避免它,最好不要使用它。

更新:以下是Michael Feathers对Working Effectively With Legacy Code的强制性引用。

答案 3 :(得分:9)

这在很大程度上取决于单例实现。但它主要是因为它有一个私有构造函数,因此你无法扩展它。但是您有以下选项

  • 制作界面 - SingletonInterface
  • 让你的单例类实现该接口
  • Singleton.getInstance()返回SingletonInterface
  • 在您的测试中提供SingletonInterface的模拟实现
  • 使用反射在private static上的Singleton字段中进行设置。

但你最好避免单身人士(代表全球状态)。 This lecture从可测试性的角度解释了一些重要的设计概念。

答案 4 :(得分:3)

并不是说Singleton模式本身就是纯粹的邪恶,但即使在它不合适的情况下,它也会大量过度使用。许多开发人员认为“哦,我可能只需要其中一个,所以让它成为一个单身人士”。事实上你应该想“我可能只需要其中一个,所以让我们在我的程序开始时构造一个并在需要的地方传递引用。”

单身和测试的第一个问题不是因为单身而是因为懒惰。由于获取单例的便利性,对单例对象的依赖通常直接嵌入到方法中,因此很难将单例更改为具有相同接口但具有不同实现的另一个对象(例如,模拟对象) )。

而不是:

void foo() {
   Bar bar = Bar.getInstance();
   // etc...
}

喜欢:

void foo(IBar bar) {
   // etc...
}

现在,您可以使用可以控制的模拟foo对象来测试函数bar。您已删除相关性,以便在不测试foo的情况下测试bar

单例和测试的另一个问题是测试单例本身。单例(按设计)很难重构,因此例如,您只能测试一次单例构造函数。 Bar的单个实例也可能在测试之间保持状态,从而导致成功或失败,具体取决于运行测试的顺序。

答案 5 :(得分:1)

有一种模拟Singleton的方法。使用powermock模拟静态方法,并使用Whitebox调用构造函数YourClass mockHelper = Whitebox .invokeConstructor(YourClass.class); Whitebox.setInternalState(mockHelper, "yourdata",mockedData); PowerMockito.mockStatic(YourClass.class); Mockito.when(YourClass.getInstance()).thenReturn(mockHelper);

正在发生的事情是Singleton字节代码在运行时发生了变化。

享受