Unit Testing with ReactiveCocoa and RubyMotion

Karlin Fox and I have been using RubyMotion and ReactiveCocoa to build an iPad application for our current project. We’ve enjoyed the different paradigm and the way we’re able to stream data through our application. Adapting TouchDB live queries to ReactiveCocoa signals has been particularly rewarding.

But what about unit testing our iOS app? Unit testing in iOS has matured a lot in the past few years, and RubyMotion brings testing along from the start of a new project. However, development paradigms like FRP have a dramatic effect on how we test, what we test, and what tools we choose to build our tests.

Signals

Mocking vs. Real Objects

We’ve been alternating between mocking out signals and creating real signals we can instrument in our unit tests. Each method has its advantages.

Using a mock is often simpler than using a real signal in a simple case. For example, we have a few methods that take a signal from one component, call takeUntil with another other signal, and return the result. We decided to use mocks here for simplicity (specifically, motion-facon).

On the other hand, method chaining can make testing using mocks a HUGE pain. See below for an example; it only gets worse with a longer method chain. You’ll find a few of our test helpers included later in this post (e.g., and_trap_block) that make both mock- and Subject-based testing easier to deal with.

class ThatThing
  attr_accessor :otherSignal
  def thatMethod(signal)
    signal.map! do |stuff|
      stuf['aThing']
    end.filter! do |thing|
      thing != :none
    end
  end
end

describe 'thatMethod' do
  before do
    @target = ThatThing.new
  end
  it 'has signal method chaining and we use mocks' do
    inSignal = mock 'first signal'
    secondSignal = mock 'second signal'
    mapBlock = inSignal.should.receive(:map!).
      and_return(secondSignal).and_trap_block
    filterBlock = secondSignal.should.receive(:filter!).
      and_return(:finalSignal).and_trap_block
    
    @target.thatMethod(inSignal).should.equal :finalSignal
    
    mapBlock.trigger({'aThing' => 'got it').should.equal 'got it'
    filterBlock.trigger('not none').should.equal true
    filterBlock.trigger(:none).should.equal false
  end
end

In these cases, it’s often easier to use a subject or two, and then test the results of pushing various values through your chain. Here is the above test re-written to use subjects instead of mocks:

describe 'thatMethod' do
  before do
    @target = ThatThing.new
  end
  it 'has signal method chaining and we use real signals' do
    inputSignal = RACSubject.subject
    values = valuesFromSignal @target.thatMethod
    inputSignal.sendNext({'aThing' => 'stuff'})
    inputSignal.sendNext({'aThing' => :none})
    
    values.should.equal ['stuff']
  end
end

Mocking can still be advantageous when the operations on the signal have a multiplicative effect and testing each case becomes prohibitively expensive.

State-based vs. Interaction-based Tests

While ReactiveCocoa delivers on its promise to turn ordinary application state into streams and signals with first-class handing of new and changing values, it does introduce a new kind of state to an application — the signals themselves.

We have many signals that are wired up at object creation time, and then are exposed as properties on that object. This has led us to pursue a mixed state-based and interaction-based testing style. We verify that the signal our object under test is subscribing to comes from the expected source using mocking, but then verify afterward that the property has the correct value.

One of the challenges of wiring up signals at object creation time is separating concerns to avoid mocking/stubbing hell. A simple solution to that problem is creating a separate method per initialization concern, and calling them all from the primary configuration method.

Our Helpers

A few of our test helpers from this project that are applicable to general iOS unit testing involving RubyMotion and ReactiveCocoa are included below with explanations

Fork of Motion-facon

We wanted a way to trap blocks (see and_trap_block in earlier example) that get passed to mocked methods. This fork allows us to do that. The blocks can then be executed to test their specific effects on the state and interactions under test. When we get a little time, we’d like to finish tests and submit a pull request for this.

motion-facon at github.

Bacon Assertion Fix

Bacon considers a test containing no bacon assertions empty and wrong, and whines about there being no assertions. Some of our tests only do mock assertions, so that behavior was not acceptable. We replace Bacon’s it with our own version to avoid the problem.

# Monkey patch Bacon. Mmmmmm.
module Bacon
  class Context
    alias_method :old_it, :it
    def it(description, &testBlock)
      if !testBlock.nil?
        old_it description do
          testBlock.call
          true.should.be.true # true dat
        end
      end
    end
  end
end

Getting Values from a Signal

Often, using real signals during a test to instrument the filtering, mapping, or other signal operations within a method under test, we want to know what sequence of values appeared on a signal. We created this helper to make that simple. It returns an array that will contain all the values that appear on a signal.

def valuesFromSignal(signal)
  values = []
  signal.subscribeNext ->(val) do
    values << val
  end
  values
end