Skip to content

Instantly share code, notes, and snippets.

@lawrencelomax
Created April 30, 2014 15:38
Show Gist options
  • Save lawrencelomax/6654f51056c2d15ad205 to your computer and use it in GitHub Desktop.
Save lawrencelomax/6654f51056c2d15ad205 to your computer and use it in GitHub Desktop.
RAC Testing abortive blogpost
layout title date comments categories published
post
ReactiveCocoa Unit Testing Tips
2013-09-22 20:45
true
Testing
iOS
Patterns
Xcode
Pragmatism
true

ReactiveCocoa is a powerful Functional Reactive Programming library for Objective-C that can vastly improve the predictability and reliability of your Applications by transforming and composing streams of values. By being more declaritive), implementations are clearer and more concise.

One of the main criticisms of ReactiveCocoa is that there is a steep learning curve and conceptual barrier that must be overcome. While I don't agree adoption of programming paradigms should be based on suitability rather than tradition, there are some suggestions and I think that I would have been of benefit when getting familiar with the Framework.

There are a number of great guides, talks and books on the ReactiveCocoa. Rather than giving an overview of the Framework and its uses, I've chosen to talk about ReactiveCocoa in the context of Unit Testing. To illustrate the writing of some Unit Tests, I’m using OCMock and specta/expecta.

Partial Mocks

Mock Objects are a great way of minimising the side effects and overhead of setting up object state when using real instances of your classes. They are also used to vend data directly to the Objects we are testing, providing values to test against.

Partial Mocks (often regarded as a code 💩) are a lot sketchier. They are used to modify the behaviour of existing Objects, or return canned values (commonly known as stubbing). They are more commonly associated with preventing behaviours from occurring in a test environment, but also for vending test values. Altering the execution of methods at runtime in this way may be implemented with isa swizzling

The RACObserve macro is one of the most commonly used ways of bringing Objects into the Reactive world and it's implementation relies on Key-Value Observing. As KVO injects its behaviour into objects by using isa swizzling, the swizzling from Partial Mocking and KVO can conflict with crashy results.

When in a Unit Test environment we may need to vend test objects from readonly accessors of instantiated Objects. The bad way is to stub methods on a Partial Mock:

ClassA *testObject = [[ClassA alloc] init];
NSNumber *fixedNumber = @1;
OCMockObject *testObjectMock = [OCMockObject partialMockForObject:testObject];
[[[testObjectMock stub] andReturn:fixedNumber] number];
testObject = (ClassA *)testObjectMock

This screws up KVO and KVC and therefore RACObserve() and RAC(). We can just include a category header in the Unit Test implementation, telling the compiler that these properties are indeed writable.

There is a better strategy that avoids the need for Mocks. The following shows a Class1 that has immutable properties externally, but can take advantage of all the benefits writable properties in the Class's implementation.

@interface ClassA

@property(nonatomic, readonly) NSNumber *number;
@property(nonatomic, readonly) NSString *string;

@end

@interface ClassA()

@property(nonatomic, strong) NSNumber *number;
@property(nonatomic, strong) NSString *string;

@end

@implementation ClassA
// Rest of the implementation goes here
@end

Inside the Unit Test source:

@interface ClassA (Spec)
// Redeclaring properties as writable for the purpose of testing.
@property(nonatomic, strong) NSNumber *number;
@property(nonatomic, strong) NSString *string;
@end

// Inside a test case
ClassA *testObject = [[ClassA alloc] init];
NSNumber *fixedNumber = @1;
testObject.number = fixedNumber;

Overstubbing Side Effects

Side Effects describe are something that we want to minimise as they often bring unexpected behaviour with them. They are an inevitability in any nearly all Applications. One common misconception of functional languages its that they are incapable of working in the ’real world’ because they do not allow side effects. In reality, FP has a mechanism for making Side Effects explicit, this aids in minimising of their usage.

Stubbing a large number of methods in classes that use ReactiveCocoa in order to get tests working is a code smell 💩. This could be indicative of an underlying design problem. It is especially worth looking out for zero-argument methods as they are used purely for their side effects.

Testing Signal output and Side Effects separately

By convention, side effects should be explicitly declared in RAC. By expressing side effects in a more formal way, the division for constructing distinct test cases becomes clearer.

In this example, a Signal called playbackSignal represents the playback state of an audio player in response to UI events:

- (RACSignal *) playbackStateSignal {
    @weakify(self)
    
    return [[RACSignal merge:@[
        [[self.playButton rac_signalForControlEvents:UIControlEventTouchUpInside] mapReplace:@(PlaybackStatePlaying)],
        [[self.stopButton rac_signalForControlEvents:UIControlEventTouchUpInside] mapReplace:@(PlaybackStateStopped)]
    ]] doNext:(NSNumber *playbackStateNumber){
        @strongify(self)
        
        PlaybackState state = [playbackStateNumber unsignedIntegerValue];
        if (state == PlaybackStatePlaying) {
            [self.audioPlayer play];
        } else if (state == PlaybackStatePaused) {
            [self.audioPlayer pause];
        }
    }];
}

This Signal returns a enumerated state value and additionally has the side effect of telling the audio player to play. Since these are related but separate behaviours, each of them should be their own Test Case.

it(@"the playback state signal should send play events when the playback controller plays", ^{
    __block PlaybackState state = 0;
    [[playbackController playbackStateSignal] subscribeNext:^(NSNumber *stateNumber){
        state = [stateNumber unsignedIntegerValue];
    }];
    
    // Like sending a touch event to the button
    [playbackController.playButton sendActionsForControlEvents:UIControlEventTouchUpInside];
    
    expect(state).to.equal(PlaybackStatePlaying);
});

it(@"when the state changes to playing, it should tell the audio player to play", ^{
    id playbackControllerMock = [OCMockObject mockForClass:AnalyticsController.class];
    [[playbackControllerMock expect] trackEvent:@"Play"];
    playbackController.analyticsController = playbackControllerMock;
    
    // Only using the Signal for its side effect in this test, don't need the values
    [[playbackController playbackStateSignal] subscribeCompleted^{}];
    
    [playbackController.playButton sendActionsForControlEvents:UIControlEventTouchUpInside];
    
    // This will assert that the tracking manager received our event
    [playbackControllerMock verify];
});    

Bound State is easily testable

ReactiveCocoa aims to solve a lot of the problems of excessive mutable state. However, ReactiveCocoa does not operate in a vaccum and will always exist within the context of Objective-C Applications. RAC may also be an implementation detail of a subsystem rather than the exclusive way to interface with the subsystem.

There are macros for moving the values held by Objective-C properties to (RACObserve()) and from (RAC()) the Reactive world.

The header has a simple property declaration:

@property(nonatomic, readonly) BOOL readyToPlay;

Which can be bound with only one RAC macro and one Signal in the implementation.

RAC(self, readyToPlay) = [RACSignal combineLatest:@[
    RACObserve(self, hasItemsInQueue), RACObserve(self, stopped).not, RACObserve(self, supendedByUser).not
]] and];

That's it. Only one place where the readyToPlay property is changed. A client of this class doesn’t have to know or care about RAC and can still use the class in a procedural way. Clients that want to use RAC can RACObserve the readyToPlay property. After all, a (KVO Compliant) property is just a Signal waiting to happen. The non Reactive implementation of this would probably require the readyToPlay status recalculated and set whenever any dependent (hasItemsInQueue, stopped, suspendedByUser) values changes, perhaps by overriding each of the setters.

This can also be applied to great effect with the usage of ViewModels. In Cocoa Land, ViewModels may be associated with and improved by ReactiveCocoa, but they are not dependent on each other. Properties exposed in a ViewModel class may be set based on Signal chains in the implementation, but the usage of Signals doesn't need to be exposed.

Asserting the equality of some property in an Object is trivial when testing. The Unit Tests themselves may also be more familiar to developers who have not learned ReactiveCocoa, even if the implementation is not.

Bound State prevents repeated Side-Effects

Bound state can also be handy when subscribing to side-effectful Signals multiple times. The User Interface may subscribe to the values of the playbackStateSignal

RAC(self.playButton, hidden) = [[self playbackStateSignal] map:^(NSNumber *state){
    return state.unsignedIntegerValue != Playing; 
}];

RAC(self.pauseButton, hidden) = [[self playbackStateSignal] map:^(NSNumber *state){
    return state.unsignedIntegerValue != Paused; 
}];

As previously mentioned, the playbackStateSignal has a side effect it tells the audio player to play and pause. By subscribing to the Signal once per button, the Signal would be subscribed to twice, causing the side-effect to occur twice. We could guarantee that this happens only once by multicasting playbackStateSignal.

One simple alternative is to bind the Signal to a property, then derive the visibility of the buttons from the bound state.

RAC(self, playbackState) = [self playbackStateSignal];

RAC(self.playButton, hidden) = [RACObserve(self, playbackState) 
 map:^(NSNumber *state){
    return state.unsignedIntegerValue != Playing; 
}];

RAC(self.pauseButton, hidden) = [RACObserve(self, playbackState) 
 map:^(NSNumber *state){
    return state.unsignedIntegerValue != Paused; 
}];

```playbackStateSignal``` is subscribed to only once, causing only one side-effect per value sent. The change in visibility of ```playButton``` and ```pauseButton``` are essentially part of the same Signal chain, but do not propogate the side effect in ```playbackStateSignal```.

Subscribing In Tests

I've previously mentioned that binding signals to Objective-C properties is helpful for Unit Testing. That's a lie by omission, subscribing directly to Signals for testing purposes is significantly easier than it may first appear.

In RAC, the RACSubscriber protocol is the means for recieving values from Signals. With the exception of Side-Effects, Unit Tests care about the values that are sent by Signals. The entry point into RACSubscriber are the subscribeNext:error:completed: methods. This is an example of an Unit Test, asserting on the last value that a Signal sends:

id expectedValue = nil;
__block sentValue = nil;
__block BOOL hasSentAValue = NO;
[someSignal subscribeNext:^(id value){
    someValue = value;
    hasSentAValue = YES;
}];

expect(hasSentAValue).to.beEqual();
expect(sentValue).to.equal(someOtherValue);

However, this is not desirable for a number of reasons

  1. Unless you want to observe the last value of the Signal only previous values sent in the Signal will be overwritten every time a value is sent.
  2. We have to assert that a value is sent, and the value that is sent separately. Otherwise asserting that a nil value is sent will result in a false passing of the equality assertion.
  3. If someSignal sends its values asynchronously, then the tests will fail.

Turns out, there a bunch of handy operators on RACSignal that enable us to get values out from Signals that don't require us to subscribe to the Signal directly:

  • firstOrDefault:success:error is a synchronous method that returns first value sent in a sequence. If you only care about getting the first object in a Signal, this beats using subscribeNext: or binding to a keypath on an TestSubsciber Object with the RAC() Macro. It will also tell you if a signal has sent an error event, again handy for asserting against.
  • asynchronousFirstOrDefault:success:error: Is like the previous method except it will work for Signals that do not do their work on the calling thread. If suitable, this can be a better solution to testing asynchronous Signals than the previously mentioned solution of stubbing all schedulers with the RACImmediateScheduler long time will hang your tests otherwise.
  • toArray/array can be very helpful in conjunction with something such as the OCHamcrest matchers. With the Hamcrest matchers, you can assert that a range of values have been sent, or that number of values have been sent, all in one line of code. This sure beats subscribing to a signal and asserting that certain values exist or don’t exist by creating a bunch of boolean expressions.

Applying the firstOrDefault:success:error operator we can refactor the test:

NSArray *sentValues = someSignal.array;
expect(sentValue.count).to.beGreaterThanOrEqualTo(0);
expect(sentValues.lastObject).to.equal([NSNull null]);  //Nil event args are sent as NSNull so they can be added to a collection

Much better.

Building up from Unit to Integration

An often cited benefit of using RAC is the simplification of asynchronous code. A common criticism of JavaScript is the nesting of callbacks, where execution flow can be very difficult to follow. In the few years between the availability of blocks in Objective-C and the increased visibility of RAC, there was a tendency to abuse nested blocks to perform Continuations.

Below is a code sample for a method in an API class that downloads a JSON resource, follows on a link to another resource which returns JSON to be stored in a Database:

- (void) downloadAndStoreDataForURL:(NSURL *)url callback:(^(void)(BOOL success, id data, NSError *error))block {
    [self.httpClient get:url callback:^(BOOL success, NSDictionary *data, NSError *error){
        if(!success) {
            block(success, data, error);
            return;
        }
        
        NSURL *followOnUrl = [NSURL URLWithString:data[@“follow_on”]];
        if(!followOnUrl) {
            block(NO, data, [NSError someBadThing]);
            return;
        }
        
        [self.httpClient get:followOnUrl callback:^(BOOL success, NSDictionary *data, NSError *error){
            if(!success) {
                block(success, data, error);
            }
        
            [[self.database storeData:data forKey:someUrl callback:^(BOOL success, NSError *error) {
                if(!success) {
                    block(success, data, error);
                    return;
                }
            }];
        }];
    }];
}

This would be a nightmare to test, especially if we wanted this to be synchronous. One way could this could be to stub the get:callback: method on httpClient, capture the block argument and call it inline. The capturing of callbacks is cumbersome and hard to generalise when there are methods that have different block types. On the positive side there is some separation of responsibilities, the storage of data in the Database is separated from the HTTP Client, and the API. The block based callback has the method signature:

- (void) get:(NSURL *)url callback:(^(void)(BOOL success, id data, NSError *error))callback;

This same behaviour can be represented instead by a method that returns a Signal. We now have a consistent interface for the HTTP Client and Database to notify the method caller of completion and error. Using RACSignal as the interface for returning data from the GET request:

- (RACSignal *) get:(NSURL *)url;

We can apply this principle across other asynchronous behaviours, and chain them together with flattenMap. By repeatedly applying this operator, we can derive a new Signal that completes when the last dependent operation completes.

// Returns a signal that returns a dictionary from the URL and then completes. If the request fails for any reason an error is sent
- (RACSignal *) getDictionary:(NSURL *)url;

// Returns a signal that stores data in a local database, associated with a remote URL. Sends the original data then completes after the database write has completed, sends an error if there has been any error writing to the database
- (RACSignal *) storeDictionary:(NSDictionary *)dictionary forKey:(NSURL *)url;

- (RACSignal *) downloadAndStoreDataForURL:(NSURL *)url {
    return [[[self.httpClient get:firstUrl]
     flattenMap:^(NSDictionary *dictionary){
         NSURL *followOnUrl = [NSURL URLWithString:dictionary[@“follow_on”]];
         return followOnUrl ? [self.httpClient get:followOnUrl] : [RACSignal error:[NSError noFollowOnUrl]];
    }] 
     flattenMap:^(NSData *data){
        return [self.database storeDictionary:data forKey:first];   
    }];
}

A few things to note:

  1. The storeData:forKey: exists to alter the state of a database. This is similar to the the IO Monad that allows functional languages to read and write data from the 'outside world'.
  2. There is no reason that get: and storeData:forKey: need to be implemented from the ground-up with Signals. Its perfectly understandable to use +[RACSignal createSignal:] to wrap an existing block based method in a Signal. Thinking about the advantages that particular Signal operators give you over a traditional procedural implementation is a good way to get started with RAC.
  3. We are taking advantage of RACSignal having the behaviour of an Error Monad. This avoids the repeated guards at the start of each block in the original implementation. If any one Signal fails in this chain, the composed Signal fails.
  4. It was a good idea for the HTTP Client and the Database to have their behaviour defined by separate classes. This will surely make the task of testing our integration of these methods much easier. If the getDictionary and storeDictonary:forKey: methods existed on the API class we would have to use yucky Partial Mocks to stub methods in the API class that we are testing. Instead we can used mocked HTTP and Database classes that are injected into the API class in a Unit Test environment.

Testing downloadAndStoreDataForURL: becomes a matter of returning Signals for stubbed get:, getDictionaryFromFollowingDictionary: and storeData:forKey: methods.

it(@"should succeed when both get requests and the database operation succeed", ^{
    NSURL *startingURL = [NSURL URLWithString:@"http://bong"]
    NSDictionary *followOnDictionary = [RACSignal return:@{ @"foo" : @"bar", @"follow_on" : @"http://bat" }];
    NSDictionary *expected = @{ @"baz" : @bang };
    
    OCMockObject *httpMock = [OCMockObject mockForClass:HTTPClient.class];
    // Taking advantage of OCMock matching different method arguments and returning given data
    [[[httpMock stub] andReturn:[RACSignal return:followOnDictionary]] getDictionary:startingURL];
    [[[httpMock stub] andReturn:[RACSignal return:expected] getDictionary:[NSURL URLWithString:@"http://bat"]];
    api.http = (HTTPClient *)httpMock; //DI!!!

    OCMockObject *databaseMock = [OCMockObject mockForClass:Database.class]
    // Using strict Mocks rather than nice/null Mocks as we will get failure earlier if arguments on our mocks aren't as we expect
    [[[databaseMock stub] andReturn:[RACSignal return:expected]] storeData:expected forKey:startingURL];
    api.database = (Database *)databaseMock; //DI++!!!
    
    BOOL success = NO;
    NSError *error = nil;
    RACSignal *signal = [api downloadAndStoreDataForURL:startingURL];
    id result = [signal firstOrDefault:NSNull.null success:&success error:&error];
    
    expect(result).to.equal(expected);
    expect(success).to.beTruthy();
    expect(error).to.beNil();
});

Using stubbed methods for each of the dependent Signals isolates test failure to the chaining of the Signals rather than the dependent Signals2. Isolating failure is crucial to writing good Unit Tests. Testing the dependent Signals can be done in separate cases. Using +[RACSignal return:] is very helpful for testing the output of chained Signals synchronously.

This is only a trivial example. I’ve come across times when there are more than a handful of chained signals as part of a sign in process for a Social Network. By making each asynchronous task a Signal and using flattenMap: I have a robust and easy to test Signal chain that completes when all dependent operations are completed.

Flatten Async with Schedulers

With the same code sample there is no guarantee that these methods can be made synchronous for testing purposes. Internally, they could operate on any number of threads or queues so it would be nearly impossible to get all of these events to happen in one run-loop. The other alternative is asynchronous testing, which is less optimal for a whole host of reasons. In my experience, the best async test is no async test.

Passing where a piece of code should be executed on the completion of some asyncrhonous task is a staple of good design with GCD. It is also something that is adopted in ReactiveCocoa with the help of the RACScheduler abstraction. Asynchronous Signals in ReactiveCocoa take a Scheduler as a required argument. By passing [RACScheduler immediatedScheduler] can really help out in these instances.

There is another brute-force approach if you are unable to change the source to take a Scheduler. nces. Any work that happens in the Reactive world can be done in a scheduler. A scheduler is essentially just an abstraction on top of the C-level GCD. By using an Object based abstraction instead of C structures representing queues, we can temporarily make sure that all work occurs synchronously, on the thread of the Test Runner:

[RACScheduler stub]

This is somewhat of a brute force approach, and is a global change in the test environment (Don't forget to unstub after running the test). A better approach is to always pass through a scheduler for the queue in which the work will be executed on. It is not entirely unreasonable for all Signals that do a significant amount of work in the background to do this.

It's also noting that by changing the scheduler used between the Unit Test and Application environment, the execution environment had changed. However, tests that fail in particular execution environments may suggest a dependence on shared state such as thread locals.

Name all the Signals

Naming signals may seem like a waste of time at first but is totally worth the effort. Just as Unit Tests will help track down the source of regressions with future code changes, named signals can help enormously when debugging the source of regressions. The Instruments templates are particularly awesome when combined with named Signals.

Footnotes

  1. Of course all classnames should be prefixed with 3 characters.

  2. This of course assumes that the Unit Tests are correct to begin with. Stubbing methods is particularly prone to typos.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment