【问题标题】:Making an Enterprise Framework Call while Unit Testing在单元测试时进行企业框架调用
【发布时间】:2016-03-23 05:20:18
【问题描述】:

以下是我们所做工作的基本理念:

  • 网站以表单的形式从用户那里收集数据并将其全部保存到表中的一行中,我们将其称为 FooObject。
  • FooObject 然后被传递到业务逻辑类中
  • 根据 app.config 中的键,工厂类将实例化三个具体类之一:
    • XMLStringA : IFooInterface
    • XMLStringB : IFooInterface
    • XMLStringC : IFooInterface
  • 每个类都有两种方法:
    • GenerateXML(FooObject fooObject)
    • ParseResponse(string serviceCallResponse)
  • 在类实例化后,我们调用 GenerateXML(),传入 FooObject,取回一个 XML 字符串,然后将其提交给三个独立的外部第三方 Web 服务中的任何一个。 (服务的地址也在app.config中。)
  • 我们得到响应,并将其发送到 ParseResponse()

一切都好。但是,C 有一些额外的要求。向该服务提交 XML 时,其中一个 XML 元素需要大量查找。我们决定将查找所需的数据添加到数据库中的表中。因此,在 XMLStringC 中有一个私有方法,它使用 EF 进行 DB 调用并获取所需的数据以添加到 XML 字符串中。

我有点意识到这样做违反了单一职责原则,因为这些类实际上应该只是构建一个 XML 字符串。 A 和 B 类不调用 DB。

当我尝试进行单元测试以测试 A、B 和 C 时,我所做的可能愚蠢的事情被赶回家了。由于在运行单元测试时我们不在上下文中,因此 C 在尝试调用数据库。

我不确定在哪里为 C 执行此自定义逻辑。一方面,它仅在我们要提交到 C 服务时发生,因此在 C 类中执行此操作是有意义的。另一方面,我不喜欢从该类内部进行数据库调用。最终,如果我能弄清楚如何对其进行单元测试并使其工作,这可能并不重要。

这样做的最佳做法是什么?

【问题讨论】:

  • 为什么不将 C 需要的任何参数作为参数传递给 C 而不是让它进入数据库?这样,在进行单元测试时,您可以只传递一个模拟对象。
  • 如果我这样做了,那么 A、B 和 C 都需要它。但是A和B并不在意。它们都实现了相同的接口。
  • 如果您使用对象来解决它,则不会。传递一个类ExternalDependencyProvider,它有一个方法GetExternalData,它执行数据库查找。在您的应用程序中,您构建了一个进入数据库的子类,在您的单元测试中,您传递了一个返回静态对象的类的版本。如果从未调用 GetExternalData(A 和 B),则不会造成任何损害,如果是(C),那么您将获得数据。
  • 这一切都是有道理的,但只有一件事。在实例化 A B 或 C 时,我不希望逻辑说“如果 C 则传入 ExternalDependencyProvider”。

标签: c# .net entity-framework unit-testing factory-pattern


【解决方案1】:

如果我这样做了,那么 A、B 和 C 都需要它。但是A和B并不在意。它们都实现了相同的接口。

如果你遵循依赖注入最佳实践,你的依赖就不是接口的一部分,它们是对象构造函数的一部分。

您的评估是正确的,这违反了 SRP。您需要一个服务来执行作为依赖项传递给 C 的查找。这样您的服务就不会违反 SRP,您仍然可以对 XMLStringC 类进行单元测试。

public class XMLStringB : IFooInterface
{
    // No constructor defined here - we have no dependencies

    public string GenerateXML(FooObject fooObject)
    {
        // implementation here
    }

    public void ParseResponse(string serviceCallResponse)
    {
        // implementation here
    }
}

public class XMLStringC : IFooInterface
{
    private readonly IDatabaseLookupService databaseLookupService;

    public XMLStringC(IDatabaseLookupService databaseLookupService)
    {
        if (databaseLookupService == null)
            throw new ArgumentNullException("databaseLookupService");
        this.databaseLookupService = databaseLookupService;
    }

    public string GenerateXML(FooObject fooObject)
    {
        // Use this.databaseLookupService as needed.
        var data = this.databaseLookupService.Lookup(fooObject.ID);

        // implementation here
    }

    public void ParseResponse(string serviceCallResponse)
    {
        // Use this.databaseLookupService as needed.
        var data = this.databaseLookupService.Lookup(someID);

        // implementation here
    }
}

您对数据库的依赖将转移到IDatabaseLookupService,而不是与您的业务逻辑绑定。

【讨论】:

  • 谢谢先生!这很有帮助。
  • 好的,在进一步研究这一点时,我不清楚如何完全实现这一点。在实例化 A、B 或 C 的工厂模式中,如果我正在实例化 C,我是否需要传入服务?该工厂的全部意义在于它不关心或不需要知道它将在运行时之前实现哪个。我不想让额外的逻辑不得不说“如果 C 则传入 databaseLookupService。”
  • 一种常见的实现方式是将依赖注入容器实例传递给工厂的构造函数,或者传递给pass in a factory method,这样可以优雅地解决问题。第一个选项假定工厂是您的组合根的一部分。请记住,构造函数不是对象接口的一部分,因此您传递给不同实现的值(依赖项)可以根据需要而变化 - 如果它们在运行时发生变化,您只需通过接口传递它们。
  • 您可以做的另一件事是将策略模式与抽象工厂模式as shown here 结合起来。 (与我之前的评论有关——见composition root的定义)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-15
相关资源
最近更新 更多