Unit testing value objects in isolation from its dependencies

前端 未结 3 2029
刺人心
刺人心 2021-01-14 01:39

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 T

3条回答
  •  说谎
    说谎 (楼主)
    2021-01-14 02:02

    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.

提交回复
热议问题