5

I'm writing some Objective-C apps (for OS X/iOS) and I'm currently implementing a service to be shared across them. The service is intended to be fairly self-contained.

For the current functionality I'm envisioning there will be only one method that clients will call to do a fairly complicated series of steps both using private methods on the class, and passing data through a bunch of "data mangling classes" to arrive at an end result.

The gist of the code is to fetch a log of changes, stored in a service-internal data store, that has occurred since a particular time, simplify the log to only include the last applicable change for each object, attach the serialized values for the affected objects and return this all to the client.

My question then is, how do I unit-test this entry point method? Obviously, each class would have thorough unit tests to ensure that their functionality works as expected, but the entry point seems harder to "disconnect" from the rest of the world. I would rather not send in each of these internal classes IoC-style, because they're small and are only made classes to satisfy the single-responsibility principle.

I see a couple possibilities:

  1. Create a "private" interface header for the tests with methods that call the internal classes and test each of these methods separately. Then, to test the entry point, make a partial mock of the service class with these private methods mocked out and just test that the methods are called with the right arguments.
  2. Write a series of fatter tests for the entry point without mocking out anything, testing the entire functionality in one go. This looks, to me, more like "integration testing" and seems brittle, but it does satisfy the "only test via the public interface" principle.
  3. Write a factory that returns these internal services and take that in the initializer, then write a factory that returns mocked versions of them to use in tests. This has the downside of making the construction of the service annoying, and leaks internal details to the client.
  4. Write a "private" initializer that take these services as extra parameters, use that to provide mocked services, and have the public initializer back-end to this one. This would ensure that the client code still sees the easy/pretty initializer and no internals are leaked.

I'm sure there's more ways to solve this problem that I haven't thought of yet, but my question is: what's the most appropriate approach according to unit testing best practices? Especially considering I would prefer to write this test-first, meaning I should preferably only create these services as the code indicates a need for them.

2 Answers 2

2

Is there any important logic in your "entry point" method? It sounds like its only job is to instantiate some other classes and wire them together. If that's right then I'd say this is trivial code, it's not business critical, you'd notice immediately if it was broken (generally if you get this sort of 'composition' code wrong it won't even compile), so - I know this seems blasphemous - it doesn't need unit tests. "The best code is no code" also applies to tests: if a test isn't testing something important, difficult, or liable to break without detection, then that test is not worth the effort of writing or maintaining.

If the main method is more than just trivial code (try...catch blocks are common in this sort of method, as are if statements for configuration) then you should probably consider extracting some logic. For example, if you have lots of if statements in your app maybe you could extract a Configuration class which handles arbitrary configurations and can easily be unit tested or extended.

1

I would do something like your version 4 but make that initialiser public.

@interface Frobulator : NSObject

- (instancetype)init; //uses default collaborators
- (instancetype)initWithThingifier: (id <Thingifiying>)thingifier doohicker: (id <Doohicking>)doohicker; //designated initialiser

- (NSData *)frobulate: (NSData *)unfrobulatedData;

@property (nonatomic, readonly) id <Thingifying>thingifier;
@property (nonatomic, readonly) id <Doohicking>doohicker;

@end

Now your unit tests:

  • test that -frobulate: interacts with the collaborators set via the initialiser. These can be mocks.
  • test that -init sets up the real collaborators.

You've then got the option in your app of either using the -init method to get the default collaborators, or supplying different collaborators should your needs change.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.