2

我在我的应用程序中使用了这个服务定位器模式并作为单例实现:

服务定位器模式

现在我要测试它。到目前为止,我已经编写了一个测试来验证我的班级是单例。我也写了这个测试:

 [Test]
 [ExpectedException(typeof(ApplicationException))]
 public void GetService_Throws_Exception_When_Invalid_Key_Is_Provided()
 {
     locator.GetService<IRandomService>();
 }

但我不太喜欢最后一次测试,因为我永远不会使用 IRandomService。所以我正在寻找一种更好的方法来测试GetService<T>抛出异常。我也想知道我是否可以为这门课编写任何其他相关测试。

我正在使用最新版本的 NUnit。

干杯

4

3 回答 3

4

一些事情:

  • 验证类是单例的测试?单例模式将确保尝试将单例视为实例类甚至不会编译。如果您还没有编写类似以下的服务定位器,那就错了:

C# 中的单例模式:

public class MySingleton()
{
    //Could also be a readonly field, or private with a GetInstance method
    public static MySingleton Instance {get; private set;}
    static MySingleton()
    {
        Instance = new MySingleton();
    }
    private MySingleton() { ... }
} 

...

//in external code
var mySingletonInstanceRef = MySingleTon.Instance; //right
var mySingletonInstanceRef = new MySingleton(); //does not compile

//EDIT: The thread-safe lazy-loaded singleton
 public class MySingleton()
{
    //static fields with initializers are initialized on first reference, so this behaves lazily
    public static readonly MySingleton instance = new MySingleton();
    //instead of the below you could make the field public, or have a GetInstance() method
    public static MySingleton Instance {get{return instance;}

    private MySingleton() { ... }
} 
  • 服务定位器是一种反模式。听起来不错,但它并没有真正解决它被创建来解决的问题。主要问题是它将您与服务定位器紧密耦合。如果定位器改变,每个使用它的类都会改变。相比之下,依赖注入可以在没有花哨的框架的情况下完成;您只需确保通过其构造函数将任何复杂、昂贵、可重用等对象传递给需要它的对象。DI/IoC 框架通过确保提供所需对象的所有已知依赖关系来简化此过程,即使图中的对象不知道其子对象的依赖关系。

  • 你已经开发了这个类。TDD/BDD 的精神是您编写测试来证明您尚未编写的代码是正确的。我并不是说现在编写测试没有任何目的,但是失败的测试需要打开和修复对象,如果代码已经集成,你可能会破坏其他东西。

  • 单元测试大量使用永远不会投入生产的结构。模拟、存根、代理和其他“测试助手”的存在是为了将被测对象与其通常集成到的环境隔离开来,保证如果输入是 A、B 和 C,则被测对象将执行 X,无论它通常挂钩的内容是否会给它 A、B 和 C。因此,不要担心创建不会在生产中使用的简单构造,例如骨架接口;只要它是您在测试用例中期望的输入的良好表示,就可以了。

于 2011-02-16T18:44:46.763 回答
2

但我真的不喜欢最后一次测试,因为我永远不会使用IRandomService.

但这就是重点。您在测试设置方法(排列)中连接定位器,然后查找未连接的键(动作),然后检查是否引发了异常(断言)。您不需要使用实际要使用的类型,您只是想对您的方法有效的信心。

我也想知道我是否可以为这门课编写任何其他相关测试。

好吧,我将在这里回答一个不同的问题。

服务定位器模式是邪恶的吗?

是的,这是纯粹的邪恶。

它违背了依赖注入的目的,因为它没有明确依赖(任何类都可以从服务定位器中提取任何东西)。此外,它使您的所有组件都依赖于这一类。

它使维护成为令人难以置信的噩梦,因为现在您拥有一个遍布整个代码库的组件。您已经与这一类紧密耦合。

此外,测试是一场噩梦。假设你有

public class Foo {
    public Foo() { // }
    public string Bar() { // }
}

你想测试Foo.Bar

public void BarDoesSomething() {
    var foo = new Foo();
    Assert.Equal("Something", foo.Bar());
}

然后你运行你的测试,你得到一个异常

ServiceLocator could not resolve component Frob.

什么?哦,那是因为你的构造函数看起来像这样:

public Foo() {
    this.frob = ServiceLocator.GetService<Frob>();
}

不断地。

避免,避免,避免。

于 2011-02-16T18:41:34.943 回答
1

我不太明白你的问题。您是否对请求永远不会在生产中使用的类型不满意?您是否对以不代表生产代码的方式使用服务定位器不满意?测试本身对我来说看起来不错——您请求的东西不存在并证明发生了预期的行为。对于单元测试,这样做是完全合理的。

我们在使用依赖注入容器时做的一件事是将我们的应用程序连接阶段分成模块,然后我们会尝试在集成测试中解析根类型,以确保应用程序可以连接起来。如果接线测试失败,则表明该应用程序无法正常工作(尽管它也不能证明该应用程序有效)。使用服务定位器时做这种事情会比较棘手。

我也 100% 同意 Jason 的观点——服务定位器似乎是个好主意,但很快就会变得讨厌。它们“拉”(使用服务定位器实例的类型必须与其耦合),而依赖注入容器“推”(绝大多数应用程序与 DI 无关,因此代码不那么脆弱且可重用)。

其他几件事:

  1. [ExpectedException]已弃用。改用 Assert.Throws
  2. ApplicationException也已弃用。
于 2011-02-16T18:48:47.300 回答