简体   繁体   English

单元测试值对象与其依赖项隔离

[英]Unit testing value objects in isolation from its dependencies

TL;DR TL; DR
How do you test a value object in isolation from its dependencies without stubbing or injecting them? 如何在不对其进行存根或注入的情况下,独立于其依赖项测试值对象?


In Misko Hevery's blog post To “new” or not to “new”… he advocates the following (quoted from the blog post): 在Misko Hevery的博客文章中, 对于“新”或不是“新”......他主张以下内容(引自博客文章):

  • 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类可以在其构造函数中请求其他Injectable。(有时我将Injectables称为服务对象,但该术语被重载。)。 Injectable can never ask for a non-Injectable (Newable) in its constructor. Injectable永远不会在其构造函数中请求非Injectable(Newable)。
  • 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) Newables可以在其构造函数中请求其他Newables,但不能用于Injectables(有时我将Newables称为Value Object,但同样,该术语被重载)

Now if I have a Quantity value object like this: 现在如果我有一个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;  
    }
}

My Quantity value object depends on at least 2 validators for its proper construction. 我的Quantity值对象依赖于至少2个验证器来正确构造。 Normally I would have injected those validators through the constructor, so that I can stub them during testing. 通常我会通过构造函数注入那些验证器,这样我就可以在测试期间将它们存根

However, according to Misko a newable shouldn't ask for injectables in its constructor. 然而,根据Misko的说法,新手不应该在其构造函数中要求注射剂。 Frankly a Quantity object that looks like this 坦率地说,这是一个看起来像这样的Quantity对象
$quantity=new Quantity(1,$intValidator,$gtValidator); looks really awkward. 看起来真的很尴尬。

Using a dependency injection framework to create a value object is even more awkward. 使用依赖注入框架来创建值对象更加尴尬。 However now my dependencies are hard coded in the Quantity constructor and I have no way to alter them if the business logic changes. 但是现在我的依赖项在Quantity构造函数中被硬编码,如果业务逻辑发生更改,我无法更改它们。

How do you design the value object properly for testing and adherence to the separation between injectables and newables? 如何正确设计价值对象以测试和遵守注射剂和新药之间的分离?

Notes: 笔记:

  1. This is just a very very simplified example. 这只是一个非常简单的例子。 My real object my have serious logic in it that may use other dependencies as well. 我的真实对象我有严肃的逻辑,也可能使用其他依赖。
  2. I used a PHP example just for illustration. 我使用PHP示例仅用于说明。 Answers in other languages are appreciated. 感谢其他语言的答案。

A Value Object should only contain primitive values (integers, strings, boolean flags, other Value Objects, etc.). 值对象应该只包含原始值(整数,字符串,布尔标志,其他值对象等)。

Often, it would be best to let the Value Object itself protect its invariants . 通常,最好让Value Object本身保护其不变量 In the Quantity example you supply, it could easily do that by checking the incoming value without relying on external dependencies. 在您提供的Quantity示例中,可以通过检查传入值而不依赖于外部依赖性来轻松实现。 However, I realize that you write 但是,我意识到你写的

This is just a very very simplified example. 这只是一个非常简单的例子。 My real object my have serious logic in it that may use other dependencies as well. 我的真实对象我有严肃的逻辑,也可能使用其他依赖。

So, while I'm going to outline a solution based on the Quantity example, keep in mind that it looks overly complex because the validation logic is so simple here. 因此,虽然我将基于Quantity示例概述解决方案,但请记住,它看起来过于复杂,因为验证逻辑在这里非常简单。

Since you also write 既然你也写

I used a PHP example just for illustration. 我使用PHP示例仅用于说明。 Answers in other languages are appreciated. 感谢其他语言的答案。

I'm going to answer in F#. 我打算用F#回答。

If you have external validation dependencies, but still want to retain Quantity as a Value Object, you'll need to decouple the validation logic from the Value Object. 如果您有外部验证依赖项,但仍希望将Quantity保留为值对象,则需要验证逻辑与值对象分离。

One way to do that is to define an interface for validation: 一种方法是定义验证接口:

type IQuantityValidator =
    abstract Validate : decimal -> unit

In this case, I patterned the Validate method on the OP example, which throws exceptions upon validation failures. 在这种情况下,我在OP示例上构建了Validate方法,它在验证失败时抛出异常。 This means that if the Validate method doesn't throw an exception, all is good. 这意味着如果Validate方法没有抛出异常,那么一切都很好。 This is the reason the method returns unit . 这是该方法返回unit的原因。

(If I hadn't decided to pattern this interface on the OP, I'd have preferred using the Specification pattern instead; if so, I'd instead have declared the Validate method as decimal -> bool .) (如果我没有决定在OP上设置这个接口的模式,我宁愿使用规范模式 ;如果是这样,我Validate方法声明为decimal -> bool 。)

The IQuantityValidator interface enables you to introduce a Composite : IQuantityValidator接口使您可以引入Composite

type CompositeQuantityValidator(validators : IQuantityValidator list) =
    interface IQuantityValidator with
        member this.Validate value =
            validators
            |> List.iter (fun validator -> validator.Validate value)

This Composite simply iterates through other IQuantityValidator instances and invokes their Validate method. 此Composite只是遍历其他IQuantityValidator实例并调用其Validate方法。 This enables you to compose arbitrarily complex validator graphs. 这使您可以组成任意复杂的验证器图。

One leaf validator could be: 一个叶子验证器可能是:

type IntegerValidator() =
    interface IQuantityValidator with
        member this.Validate value =
            if value % 1m <> 0m
            then
                raise(
                    ArgumentOutOfRangeException(
                        "value",
                         "Quantity must be an integer."))

Another one could be: 另一个可能是:

type GreaterThanValidator(boundary) =
    interface IQuantityValidator with
        member this.Validate value =
            if value <= boundary
            then
                raise(
                    ArgumentOutOfRangeException(
                        "value",
                         "Quantity must be greater than zero."))

Notice that the GreaterThanValidator class takes a dependency via its constructor. 请注意, GreaterThanValidator类通过其构造函数获取依赖关系。 In this case, boundary is just a decimal , so it's a Primitive Dependency , but it could just as well have been a polymorphic dependency (AKA a Service ). 在这种情况下, boundary只是一个decimal ,所以它是一个原始依赖 ,但它也可能是一个多态依赖(AKA a Service )。

You can now compose your own validator from these building blocks: 您现在可以从这些构建块中构建自己的验证器:

let myValidator =
    CompositeQuantityValidator([IntegerValidator(); GreaterThanValidator(0m)])

When you invoke myValidator with eg 9m or 42m , it returns without errors, but if you invoke it with eg 9.8m , 0m or -1m it throws the appropriate exception. 当您使用例如9m42m调用myValidator时,它会返回而不会出现错误,但如果您使用例如9.8m0m-1m调用它,则会抛出相应的异常。

If you want to build something a bit more complicated than a decimal , you can introduce a Factory, and compose the Factory with the appropriate validator. 如果要构建比decimal更复杂的东西,可以引入Factory,并使用适当的验证器组合Factory。

Since Quantity is very simple here, we can just define it as a type alias on decimal : 由于数量在这里非常简单,我们可以将其定义为decimal的类型别名:

type Quantity = decimal

A Factory might look like this: 工厂可能如下所示:

type QuantityFactory(validator : IQuantityValidator) =
    member this.Create value : Quantity =
        validator.Validate value
        value

You can now compose a QuantityFactory instance with your validator of choice: 您现在可以使用您选择的验证器组成QuantityFactory实例:

let factory = QuantityFactory(myValidator)

which will let you supply decimal values as input, and get (validated) Quantity values as output. 这将允许您提供decimal值作为输入,并获取(验证) Quantity值作为输出。

These calls succeed: 这些调用成功:

let x = factory.Create 9m
let y = factory.Create 42m

while these throw appropriate exceptions: 虽然这些抛出适当的例外:

let a = factory.Create 9.8m
let b = factory.Create 0m
let c = factory.Create -1m

Now, all of this is very complex given the simple nature of the example domain, but as the problem domain grows more complex, complex is better than complicated . 现在,考虑到示例域的简单特性,所有这些都非常复杂 ,但随着问题域变得越来越复杂, 复杂性比复杂性更好

Avoid value types with dependencies on non-value types. 避免使用依赖于非值类型的值类型。 Also avoid constructors that perform validations and throw exceptions. 还要避免执行验证并抛出异常的构造函数。 In your example I'd have a factory type that validates and creates quantities. 在您的示例中,我有一个验证和创建数量的工厂类型。

Your scenario can also be applied to entities. 您的方案也可以应用于实体。 There are cases where an entity requires some dependency in order to perform some behaviour. 在某些情况下,实体需要一些依赖关系才能执行某些行为。 As far as I can tell the most popular mechanism to use is double-dispatch. 据我所知,最常用的机制是双重调度。

I'll use C# for my examples. 我将使用C#作为我的例子。

In your case you could have something like this: 在你的情况下,你可以有这样的东西:

public void Validate(IQuantityValidator validator)

As other answers have noted a value object is typically simple enough to perform its invariant checking in the constructor. 正如其他答案所指出的那样,值对象通常足够简单,可以在构造函数中执行其不变检查。 An e-mail value object would be a good example as an e-mail has a very specific structure. 电子邮件值对象就是一个很好的例子,因为电子邮件具有非常特定的结构。

Something a bit more complex could be an OrderLine where we need to determine, totally hypothetical, whether it is, say, taxable: 更复杂的东西可能是OrderLine ,我们需要确定,完全假设,是否是应税的:

public bool IsTaxable(ITaxableService service)

In the article you reference I would assert that the 'newable' relates quite a bit to the 'transient' type of life cycle that we find in DI containers as we are interested in specific instances. 在你引用的文章中,我会声称'newable'与我们在DI容器中找到的'瞬态'类型的生命周期有很大关系,因为我们对特定实例感兴趣。 However, when we need to inject specific values the transient business does not really help. 但是,当我们需要注入特定值时,瞬态业务并没有真正帮助。 This is the case for entities where each is a new instance but has very different state. 对于每个都是新实例但具有非常不同状态的实体来说就是这种情况。 A repository would hydrate the object but it could just as well use a factory. 存储库可以为对象提供水分,但它也可以使用工厂。

The 'true' dependencies typically have a 'singleton' life-cycle. “真正的”依赖关系通常具有“单身”生命周期。

So for the 'newable' instances a factory could be used if you would like to perform validation upon construction by having the factory call the relevant validation method on your value object using the injected validator dependency as Mark Seemann has mentioned. 因此,对于'newable'实例,如果要在构造时执行验证,可以使用工厂,方法是让工厂使用Mark Seemann提到的注入验证器依赖关系在您的值对象上调用相关的验证方法。

This gives you the freedom to still test in isolation without coupling to a specific implementation in your constructor. 这使您可以自由地进行单独测试,而无需耦合到构造函数中的特定实现。

Just a slightly different angle on what has already been answered. 对于已经回答的问题,只是略有不同的角度。 Hope it helps :) 希望能帮助到你 :)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM