4

I'm finally imposing some TDD on a project I'm working on, and running into the edges... I know the code I want but not how to test for it :)

The implementation I'm looking for is:

- (void) doSomething
{
    FooBuilder *foo = [[FooBuilder alloc] init];
    [foo doSomethingElseWithCompletionBlock:^{
        [self somethingDone];
    }];
}

So I want my test to verify that a) the method under test allocates a new FooBuilder and b) that method then calls a method on the new object.

How do I go about this? I started down the path of trying to mock the alloc class method but quickly determined that down that path lies madness.

Note I'm not testing FooBuilder itself with this test, just that the collaboration is there.

Jon Reid
  • 20,545
  • 2
  • 64
  • 95
dpassage
  • 5,423
  • 3
  • 25
  • 53

2 Answers2

1

Normally, dependency injection is used to provide a fully-formed object, saying "instead of asking for this object, here you go, use this." But in this case, we want the ability to instantiate a new object. So instead of injecting an object, all we have to do is inject the class. "Instead of creating a specific class, here you go, instantiate one of these."

There are two main forms of dependency injection: "constructor injection" (I'll stick with the term "constructor" even though Objective-C separates this into allocation and initialization) and "property injection".

For constructor injection, specify the class in the initializer:

- (instancetype)initWithFooBuilderClass:(Class)fooBuilderClass;

For property injection, specify the class in a property:

@property (nonatomic, strong) Class fooBuilderClass;

Constructor injection is clearer, because it makes the dependency obvious. But you may prefer property injection. Sometimes I start one way and refactor toward the other, changing my mind.

Either way, you can have a default initializer that either calls -initWithFooBuilderClass: , or sets the property, to [FooBuilderClass class].

Then doSomething would start like this:

- (void)doSomething
{
    id foo = [[self.fooBuilderClass alloc] init];
    ...
Jon Reid
  • 20,545
  • 2
  • 64
  • 95
  • Jon, thanks for your attention and detailed response - but I think I'm missing something here. You diagnose the coupling between the class under test and the `FooBuilder` class as the problem. In doing so, I feel like you're making something simple a lot more complicated. I have an object Bar that displays Foos; to do this, it needs to build a Foo with a FooBuilder. I feel like you're asking me to introduce generality I don't need solely to make testing easier. – dpassage Jul 29 '13 at 06:51
  • I woke up this morning realizing that the problem is actually just dependency injection of a class. So I completely rewrote my answer; try it on for size. …If it still feels overly complicated, let me know and I'll try to explain further. – Jon Reid Jul 29 '13 at 16:27
  • I still think you're not getting what I'm asking. My question is really about how to drive the OCMock API to ensure that my object-under-test is making the right outbound calls. You're still trying to inject generality into a class that doesn't need it. – dpassage Jul 29 '13 at 17:05
  • OCMock isn't the problem. And in fact, I recommend not using _any_ mocking framework (make your own test doubles by hand instead) until you become familiar with the patterns. The pattern here is Dependency Injection; it's the mechanism for supplying a double. Without DI, your TDD will be restricted to state verification (what is the state of the class under test). By adding DI and test doubles, you can do behavior verification (how does the class under test interact with this other class). – Jon Reid Jul 29 '13 at 17:44
  • 1
    @JonReid is correct. If you want to write the method this way, you can't use `OCMock` to verify your use of `FooBuilder`. Another alternative would be to change your method to take a `FooBuilder` argument: `-(void)doSomethingWithFooBuilder:(FooBuilder *)fooBuilder`. Then your test can pass in a mocked instance of `FooBuilder`. – Christopher Pickslay Jul 29 '13 at 18:15
  • If the answer is "OCMock can't do that", fair enough. But help me understand what benefit I get from rewriting my code in this way *other than testability*. If it's generality, that's generality I don't need. (Sure, I might want it later, but if so I can add it at that point.) – dpassage Jul 29 '13 at 22:16
  • Testability is enough of a reason for me to change the design of my code. – Jon Reid Jul 30 '13 at 02:57
0

I ended up addressing this by adding a new class method to FooBuilder which takes a completion block as an argument. So I've effectively moved the instantiation and method call out of my object-under-test into the collaborator object. Now I can mock that single class method call.

I think this ends up being slightly better design than what I started with; the detail that there needs to be a new FooBuilder instantiated is hidden from users of the class now. It's also pretty simple.

It does have the property that it maintains the strong coupling between my object-under-test and the FooBuilder class. Maybe that'll bite me down the road - but I'm making the YAGNI bet that it won't.

dpassage
  • 5,423
  • 3
  • 25
  • 53
  • I'm glad you found something that works. But you will face the same problem elsewhere, and will need to draw on Dependency Injection. I give another DI example here: http://stackoverflow.com/questions/13711911/unit-testing-example-with-ocunit/ – Jon Reid Jul 30 '13 at 03:15
  • I'm sure I will, and it's a good technique to know. In this case, though, it really felt like adding that generality would make the code harder to understand. Thanks for your help - I'm already using some hints from your website in my app as well :) – dpassage Jul 30 '13 at 15:48