I wanted to give the Functional Core/Imperative Shell approach a shot, especially since Swift's struct
s 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