vendredi 29 mai 2015

Unit-testing a functional core of value objects: how to verify contracts without mocking?

I wanted to give the Functional Core/Imperative Shell approach a shot, especially since Swift's structs are so easy to create.

Now they drive me nuts in unit tests.

How do you unit test a net of value objects?


Maybe I'm too stupid to search for the right terms, but in the end, the more unit tests I write, the more I think I should delete all the old ones. In other words my tests seem to bubble up the call chain as long as I don't provide mock objects. Which is impossible for (struct) value objects, because there's no way to replace them with a fake double unless I introduce a protocol, which doesn't work well in production code when methods return Self or have associated types.

Here's a very simple example:

struct Foo {
    func obtainBar() -> Bar? { 
      // ... 
    }
}

struct FooManager {
    let foos: [Foo]

    func obtainFirstBar() -> Bar {
        // try to get a `Bar` from every `Foo` in `foos`; pass the first
        // one which isn't nil down
    }
}

This works well with a concrete Foo class or struct. Now how am I supposed to test what obtainFirstBar() does? -- I plug in one and two and zero Foo instances and see what happens.

But then I'm replicating my knowledge and assertions about Foo's obtainBar(). Well, either I move the few FooTests into FooManagerTests, which sounds stupid, or I use mocks to verify the incoming call instead of merely asserting a certain return value. But you can't subclass a struct, so you create a protocol:

protocol FooType {
    func obtainBar() -> Bar
}

struct Foo: FooType { /* ... */ }

class TestFoo: FooType { /* do some mocking/stubbing */}

When Bar is complex enough to warrant its own unit tests and it seems it should be mocked, you end up with this instead:

protocol FooType {
    typealias B: BarType

    func obtainBar() -> B
}

But then the compiler will complain that FooManager's collection of foos doesn't work this way. Thanks, generics.

struct FooManager<F: FooType where F.B == Bar> {
    let foos: [F]         // ^^^^^^^^^^^^^^^^ added for clarity 

    func obtainFirstBar() -> Bar { /* ... */ }
}

You can't pass in different kinds of Foo, though. Only one concrete FooType is allowed, no weird mixes of BlueFoo and RedFoo, even if both return the same Bar and seem to realize the FooType protocol in the same way. This isn't Objective-C and duck typing isn't possible. Protocols don't seem to add much benefit in terms of abstraction here anyway unless they're not depending on anything else which is a protocol or Self.

If protocols lead to confusion and not much benefit at all, I'd rather stop using them. But how do I write unit tests if one object is mostly a convoluted net of dependent objects, all being value types, seldomly optional, without moving all tests for every permutation in the test suite of the root object(s)?

Aucun commentaire:

Enregistrer un commentaire