oop - 与依赖项隔离的单元测试值对象

标签 oop unit-testing dependency-injection tdd domain-driven-design

TL;DR
如何在不 stub 或注入(inject)它们的情况下独立于其依赖项来测试值对象?

在 Misko Hevery 的博文中 To “new” or not to “new”…他主张以下内容(引自博客文章):

  • An Injectable class can ask for other Injectables in its constructor.(Sometimes I refer to Injectables as Service Objects, but that term is overloaded.). Injectable can never ask for a non-Injectable (Newable) in its constructor.
  • Newables can ask for other Newables in their constructor, but not for Injectables (Sometimes I refer to Newables as Value Object, but again, the term is overloaded)


现在,如果我有 Quantity像这样的值对象:
class Quantity{

    $quantity=0;

    public function __construct($quantity){
        $intValidator = new Zend_Validate_Int();
        if(!$intValidator->isValid($quantity)){
            throw new Exception("Quantity must be an integer.");    
        }

        $gtValidator = new Zend_Validate_GreaterThan(0);
        if(!$gtvalidator->isValid($quantity)){
            throw new Exception("Quantity must be greater than zero."); 
        }

        $this->quantity=$quantity;  
    }
}

我的 Quantity值对象的正确构造至少依赖于 2 个验证器。通常我会通过构造函数注入(inject)这些验证器,这样我就可以 stub 他们在测试期间。

然而,根据 Misko 的说法,newable 不应该在其构造函数中要求注入(inject)。坦率地说 Quantity看起来像这样的对象$quantity=new Quantity(1,$intValidator,$gtValidator);看起来真的很尴尬。

使用依赖注入(inject)框架来创建值对象更加尴尬。但是现在我的依赖项被硬编码在 Quantity 中。构造函数,如果业务逻辑发生变化,我无法更改它们。

您如何正确设计值(value)对象以测试和遵守注入(inject)剂和新剂之间的分离?

笔记:
  • 这只是一个非常简单的例子。我的真实对象在其中有严肃的逻辑,也可能使用其他依赖项。
  • 我使用了一个 PHP 示例来说明。其他语言的答案表示赞赏。
  • 最佳答案

    值对象应该只包含原始值(整数、字符串、 bool 标志、其他值对象等)。

    通常,最好让值对象本身 保护其不变量 .在您提供的 Quantity 示例中,它可以通过检查传入值轻松做到这一点,而无需依赖外部依赖项。但是,我意识到你写

    This is just a very very simplified example. My real object my have serious logic in it that may use other dependencies as well.



    因此,虽然我将基于 Quantity 示例概述一个解决方案,但请记住,它看起来过于复杂,因为这里的验证逻辑非常简单。

    既然你也写

    I used a PHP example just for illustration. Answers in other languages are appreciated.



    我将在 F# 中回答。

    如果您有外部验证依赖项,但仍希望将 Quantity 保留为值对象,则需要 解耦 来自值对象的验证逻辑。

    一种方法是定义一个验证接口(interface):
    type IQuantityValidator =
        abstract Validate : decimal -> unit
    

    在这种情况下,我设计了 Validate OP 示例中的方法,该方法在验证失败时引发异常。这意味着如果 Validate方法不会抛出异常,一切都很好。这就是该方法返回 unit 的原因。 .

    (如果我没有决定在 OP 上设计这个接口(interface),我会更喜欢使用 Specification pattern;如果是这样,我会改为将 Validate 方法声明为 decimal -> bool。)
    IQuantityValidator界面使您能够引入 Composite :
    type CompositeQuantityValidator(validators : IQuantityValidator list) =
        interface IQuantityValidator with
            member this.Validate value =
                validators
                |> List.iter (fun validator -> validator.Validate value)
    

    这个复合只是简单地遍历其他 IQuantityValidator实例并调用它们的Validate方法。这使您能够组合任意复杂的验证器图。

    一个叶子验证器可以是:
    type IntegerValidator() =
        interface IQuantityValidator with
            member this.Validate value =
                if value % 1m <> 0m
                then
                    raise(
                        ArgumentOutOfRangeException(
                            "value",
                             "Quantity must be an integer."))
    

    另一种可能是:
    type GreaterThanValidator(boundary) =
        interface IQuantityValidator with
            member this.Validate value =
                if value <= boundary
                then
                    raise(
                        ArgumentOutOfRangeException(
                            "value",
                             "Quantity must be greater than zero."))
    

    注意 GreaterThanValidator类通过其构造函数获取依赖项。在这种情况下,boundary只是一个 decimal ,所以它是 Primitive Dependency ,但它也可能是多态依赖(A.K.A 服务)。

    您现在可以从这些构建 block 组成您自己的验证器:
    let myValidator =
        CompositeQuantityValidator([IntegerValidator(); GreaterThanValidator(0m)])
    

    当您调用 myValidator与例如9m42m ,它返回没有错误,但如果你用例如调用它9.8m , 0m-1m它抛出适当的异常。

    如果你想构建比 decimal 更复杂的东西,您可以引入工厂,并使用适当的验证器组成工厂。

    由于 Quantity 在这里非常简单,我们可以将其定义为 decimal 上的类型别名。 :
    type Quantity = decimal
    

    工厂可能如下所示:
    type QuantityFactory(validator : IQuantityValidator) =
        member this.Create value : Quantity =
            validator.Validate value
            value
    

    您现在可以撰写 QuantityFactory使用您选择的验证器的实例:
    let factory = QuantityFactory(myValidator)
    

    这将使您提供decimal值作为输入,并获取(验证)Quantity值作为输出。

    这些调用成功:
    let x = factory.Create 9m
    let y = factory.Create 42m
    

    虽然这些抛出适当的异常:
    let a = factory.Create 9.8m
    let b = factory.Create 0m
    let c = factory.Create -1m
    

    现在,所有这些都是 非常复杂鉴于示例域的简单性质,但随着问题域变得越来越复杂,complex is better than complicated .

    关于oop - 与依赖项隔离的单元测试值对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22263646/

    相关文章:

    Python OOP : how to share a MongoDB connection with all classes

    oop - 如何使用 Playground/Workspace(而不是通过系统浏览器)创建方法?

    c++ - 如何使用 Google Test 检查零个或一个函数调用?

    jsf - 抽象类和具体类中的依赖注入(inject)

    javascript - 制作自己对象的副本而不是引用(更新后的帖子,不同的 Q)

    java - 处理中的 OOP,我应该如何构建我的类?

    asp.net-mvc - ASP.NET MVC - 单元测试过度? (时分驱动)

    java - 使用 Powermockito 模拟 ClassLoader.getSystemClassLoader().loadClass

    c# - 自定义过滤器属性注入(inject)依赖

    c# - 保持 .NET 依赖注入(inject)有序