How can i unit test an object internal to a method in Objective-C?
Asked Answered
N

4

6

I'm wondering how to go about testing this. I have a method that takes a parameter, and based on some properties of that parameter it creates another object and operates on it. The code looks something like this:

- (void) navigate:(NavContext *)context {
  Destination * dest = [[Destination alloc] initWithContext:context];
  if (context.isValid) {
    [dest doSomething];
  } else {
    // something else
  }
  [dest release];
}

What i want to verify is that if context.isValid is true, that doSomething is called on dest, but i don't know how to test that (or if that's even possible) using OCMock or any other traditional testing methods since that object is created entirely within the scope of the method. Am i going about this the wrong way?

Nonunionism answered 6/7, 2009 at 20:56 Comment(0)
H
5

You could use OCMock, but you'd have to modify the code to either take a Destination object or to use a singleton object which you could replace with your mock object first.

The cleanest way to do this would probably be to implement a

-(void) navigate:(NavContext *)context destination:(Destination *)dest;

method. Change the implementation of -(void) navigate:(NavContext *)context to the following:

- (void) navigate:(NavContext *)context {
    Destination * dest = [[Destination alloc] initWithContext:context];
    [self navigate:context destination:dest];
    [dest release];
}

This will allow your tests to call the method with an extra parameter directly. (In other languages, you would implement this simply by providing a default value for the destination parameter, but Objective-C does not support default parameters.)

Hilar answered 6/7, 2009 at 21:22 Comment(2)
This seems to be the easiest answer, but it seems a little messy to have the destination object live in multiple scopes when it isn't really needed elsewhere, just to support easy testing. I guess compromises need to be made somewhere :)Nonunionism
Yep; if you want to be able to substitute a separate Destination object, then you have to have some way of passing it in. This is the cleanest way I know of to do that.Hilar
B
1

What i want to verify is that if context.isValid is true, that doSomething is called on dest

I think you may be testing the wrong thing here. You can safely assume (I hope) that boolean statements work correctly in ObjC. Wouldn't you want to test the Context object instead? If context.isValid then you're guaranteed that the [dest doSomething] branch gets executed.

Brenza answered 6/7, 2009 at 21:27 Comment(3)
I'm mocking the context object since in this test i don't care how it gets created, i care that the method does the right thing based on the context's properties.Nonunionism
Further, if the implementation of navigate: ever changes, he may still want to verify that doSomething is called.Hilar
Ahh fair enough, I was being a little short sighted. I like BJ Homer's approach then.Brenza
R
0

It's completely possible, using such interesting techniques as method swizzling, but it's probably going about it the wrong way. If there's absolutely no way to observe the effects of invoking doSomething from a unit test, isn't the fact that it invokes doSomething an implementation detail?

(If you were to do this test, one way to accomplish your aims would be replacing the doSomething method of Destination with one that notifies your unit test and then passes on the call to doSomething.)

Relay answered 6/7, 2009 at 21:4 Comment(1)
I suppose it's not a big deal if i don't test that the method is called. I'm still somewhat new to TDD/unit testing so i don't know all of the best practices yet :)Nonunionism
M
0

I like to use factory methods in this situation.

@interface Destination(Factory)
+ (Destination *)destinationWithContext:(NavContext *)context;
@end

@implementation Destination(Factory)
+ (Destination *)destinationWithContext:(NavContext *)context
{
    return [[Destination alloc] initWithContext:context];
}
@end

I then make a FakeClass:

#import "Destination+Factory.h"

@interface FakeDestination : Destination
+ (id)sharedInstance;
+ (void)setSharedInstance:(id)sharedInstance;
// Note! Instance method!
- (Destination *)destinationWithContext:(NavContext *)context;
@end

@implementation FakeDestination
+ (id)sharedInstance
{
    static id _sharedInstance = nil;
    if (!_sharedInstance)
    {
        _sharedInstance = [[FakeDestination alloc] init];
    }
    return _sharedInstance;
}
+ (void)setSharedInstance:(id)sharedInstance
{
    _sharedInstance = sharedInstance;
}
// Overrides
+ (Destination *)destinationWithContext:(NavContext *)context { [FakeDestination.sharedInstance destinationWithContext:context]; }
// Instance
- (Destination *)destinationWithContext:(NavContext *)context { return nil; }
@end

Once you set this up, you just need to swizzle the class methods for + (Destination *)destinationWithContext:(NavContext *)context;

Now you're set to:

id destinationMock = [OCMock mockForClass:FakeDestination.class];
// do the swizzle
[FakeDestination setSharedInstance:destinationMock];
[[destinationMock expect] doSomething];
// Call your method
[destinationMock verify];

This is a fair amount of coding up front, but it's very reusable.

Merrow answered 31/3, 2013 at 4:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.