单元测试对象与其依赖性隔离 [英] Unit testing value objects in isolation from its dependencies

查看:149
本文介绍了单元测试对象与其依赖性隔离的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

TL; DR

如何检测值对象与其依赖关系不分离或注入?






在Misko Hevery的博客文章到新或不是新... 他主张以下(从博客文章引用):



  • 一个可注入类可以在其构造函数中请求其他注射器(有时我将可注册方式称为服务对象,但是
    这个术语是重载的)。可注入的人不能在其构造函数中要求不可注入(Newable)。

  • Newables可以在其构造函数中要求其他Newables,但不能用于Injectables(有时我将Newables称为值对象,但
    再次,该术语重载)


现在如果我有一个数量这样的值对象:

 类数量{

$ quantity = 0;

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

$ gtValidator = new Zend_Validate_GreaterThan(0);
如果(!$ gtvalidator-> isValid($ quantity)){
抛出新的异常(数量必须大于零);
}

$ this-> quantity = $ quantity;
}
}

我的数量 value对象至少需要2个验证器才能正确构建。通常我会通过构造函数注入这些验证器,以便我们在测试期间可以存根



然而,根据Misko,一个新的不应该在其构造函数中要求注入。坦白地说,一个数量对象,看起来像这样

$ quantity = new数量(1,$ intValidator,$ gtValidator); 看起来真的很尴尬。



使用依赖注入框架创建一个值对象甚至更加尴尬。然而,现在我的依赖关系在 Quantity 构造函数中是硬编码的,如果业务逻辑发生变化,我无法改变它们。



如何正确设计价值对象以测试和遵守注射剂和新产品之间的分离?



注意:


  1. 这只是一个很简单的例子。我的真实对象,我有严重的逻辑,可能会使用其他依赖关系。

  2. 我使用一个PHP示例只是为了说明。


解决方案

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



通常,最好让Value Object本身保护其不变量。在您提供的数量示例中,它可以轻松地通过检查传入值而不依赖于外部依赖关系。但是,我意识到你写了


这只是一个很简单的例子。我的真实对象我有严肃的逻辑,可能会使用其他依赖关系。


所以,当我要概述一下解决方案基于数量示例,请记住,它看起来过于复杂,因为验证逻辑在这里很简单。



既然你也写了


我使用了一个PHP示例,仅供参考。


我将在F#中回答。



如果您有外部验证依赖关系,但仍希望将Quantity保留为值对象,则需要从Value Object中解析验证逻辑。



一种方法是定义验证接口:

  type IQuantityValidator = 
abstract验证:decimal - >单位

在这种情况下,我设计了验证方法,在验证失败时抛出异常。这意味着如果 Validate 方法没有抛出异常,一切都是好的。这是方法返回单元的原因。



(如果我没有决定在OP,我更喜欢使用规格模式;如果是,我会改为将验证方法声明为 decimal - > bool 。)



IQuantityValidator 界面可让您引入复合

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

此Composite简单地遍历其他 IQuantityValidator 实例,并调用其验证方法。一个叶验证器可以是:

 



code> type IntegerValidator()=
interface IQuantityValidator with
member this.Validate value =
如果值%1m<> 0m
然后
raise(
ArgumentOutOfRangeException(
value,
数量必须是整数。))

另一个可能是:

  type GreaterThanValidator (边界)= 
接口IQuantityValidator与
成员this.Validate值=
如果值<=边界
然后
raise(
ArgumentOutOfRangeException(
value,
数量必须大于零。))

请注意, GreaterThanValidator 类通过其构造函数获取依赖关系。在这种情况下, border 只是一个 decimal ,所以它是一个原始依赖关系,但它也可以是多态依赖关系(AKA a Service )。 / p>

现在,您可以从这些构建块中编写自己的验证器:

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

你调用 myValidator 9m 42m ,它返回没有错误,但如果您使用例如 9.8m 0m -1m 它会抛出适当的



如果您想要构建比 decimal 更复杂的东西,您可以介绍一个工厂,并由适当的验证器组成工厂。



由于数量非常简单,我们可以将其定义为 decimal

  type Quantity = decimal 

工厂可能如下所示:

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

现在,您可以使用选择的验证器来编写一个 QuantityFactory 实例:

  let factory = QuantityFactory(myValidator)

这将让您提供 decimal 作为输入值,get(验证)数量值作为输出。



这些电话成功:

  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

现在,鉴于示例域的简单性质,所有这些都非常复杂,但是随着问题领域的变得越来越复杂,复杂比复杂的更好


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):

  • 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)

Now if I have a Quantity value object like this:

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. 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. Frankly a Quantity object that looks like this
$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.

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. 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. In the Quantity example you supply, it could easily do that by checking the incoming value without relying on external dependencies. 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.

Since you also write

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

I'm going to answer in 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.

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. This means that if the Validate method doesn't throw an exception, all is good. This is the reason the method returns 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.)

The IQuantityValidator interface enables you to introduce a 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. 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. 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 (A.K.A a Service).

You can now compose your own validator from these building blocks:

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

When you invoke myValidator with e.g. 9m or 42m, it returns without errors, but if you invoke it with e.g. 9.8m, 0m or -1m it throws the appropriate exception.

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.

Since Quantity is very simple here, we can just define it as a type alias on 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:

let factory = QuantityFactory(myValidator)

which will let you supply decimal values as input, and get (validated) Quantity values as output.

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.

这篇关于单元测试对象与其依赖性隔离的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆