Stubbing Rubymotion Http

5 minute read

This is a seriously old post. I’d probably not bother reading it, but I keep it here for nostalgia.

Stubbing RubyMotion HTTP

Coming from the Ruby and Rails world, I’m anchored in the paradigm that we should test our code. Our code should not only be tested, but it should be sure not to be hitting external services during those specs. I’ve had a hard time figuring out the best way to stub out HTTP calls effectively and easily in RubyMotion. I’m also not the best Objective-C programmer, so porting libraries already built was not on the cards in the timeframe I wanted.

However, I came across the WebStub library by Matt Green in my search for answers, and played around with it over the weekend. It’s fantastic. Coupled with Clay Allsop’s AFMotion library that creates a ruby bridge to the AFNetworking library.

Let’s run through how I went about testing a little API driven app I’m writing. I assume you have RubyMotion, and that you’re not new to using it, but you’re unfamiliar with how to test HTTP calls. It uses BubbleWraps HTTP calls, not AFMotion, however it’s the same testing method.

Create a RubyMotion Project

We’re going to clone the brilliant API Driven Example written by Clay Allsop. Did I mention he wrote the RubyMotion Book? It’s good. Buy it. The API Driven example on the RubyMotion Tutorial is ripe for a little sprinkle of test. So go ahead and run through that example if you want to play along with me adding specs to it.

Once you have that, we’ll start adding our specs. I’ve cloned the git repository that Clay has kindly placed this in, and taken the Colr application straight out of it.

In the Color class, we have 2 HTTP calls, one is a GET and the other is a POST. Let’s wrap a test around that GET request first. The find method looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def self.find(hex, &block)
 BW::HTTP.get("http://www.colr.org/json/color/#{hex}") do |response|
 result_data = BW::JSON.parse(response.body.to_str)
 color_data = result_data["colors"][0]

 # Colr will return a color with id == -1 if no color was found
 color = Color.new(color_data)
 if color.id.to_i == -1
 block.call(nil)
 else
 block.call(color)
 end
 end
end

Add WebStub to your Rakefile.

1
2
3
4
5
6
7
8
9
10
# -*- coding: utf-8 -*-
$:.unshift("/Library/RubyMotion/lib")
require 'motion/project'
require 'bubble-wrap'
require 'webstub'

Motion::Project::App.setup do |app|
 # Use `rake config' to see complete project settings.
 app.name = 'Colr'
end

Here’s the color_spec.rb spec I built to demonstrate testing that call.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
describe Color do
 extend WebStub::SpecHelpers

 before do
 disable_network_access!
 end

 describe "#find" do
 before do
 @successful_color_response = '{"colors": [{"timestamp": 1348628320, "hex": "ffba13", "id": 5490, "tags": [{"timestamp": 1129240205, "id": 14386, "name": "cheesy"}, {"timestamp": 1108442341, "id": 5076, "name": "fries"}, {"timestamp": 1108442340, "id": 5075, "name": "cheese"}, {"timestamp": 1108442341, "id": 5076, "name": "fries"}, {"timestamp": 1344743165, "id": 26414, "name": "yellowsnow"}, {"timestamp": 1109734601, "id": 6414, "name": "bee"}, {"timestamp": 1108110850, "id": 2542, "name": "yellow"}, {"timestamp": 1346850231, "id": 26442, "name": "kahit"}, {"timestamp": 1120839389, "id": 13877, "name": "ano"}, {"timestamp": 1348492735, "id": 26461, "name": "uniqum"}, {"timestamp": 1348627829, "id": 26463, "name": "somecolor"}, {"timestamp": 1348627829, "id": 26463, "name": "somecolor"}, {"timestamp": 1348627829, "id": 26463, "name": "somecolor"}, {"timestamp": 1348627829, "id": 26463, "name": "somecolor"}]}], "schemes": [], "schemes_history": {}, "success": true, "colors_history": {"ffba13": [{"d_count": 3, "id": "5075", "a_count": 5, "name": "cheese"}, {"d_count": 2, "id": "5076", "a_count": 5, "name": "fries"}, {"d_count": 1, "id": "5077", "a_count": 2, "name": "ham"}, {"d_count": 2, "id": "5078", "a_count": 2, "name": "spazz"}, {"d_count": 1, "id": "13840", "a_count": 1, "name": "01oct05"}, {"d_count": 1, "id": "14849", "a_count": 1, "name": "998"}, {"d_count": 1, "id": "3336", "a_count": 1, "name": "love"}, {"d_count": 0, "id": "14386", "a_count": 1, "name": "cheesy"}, {"d_count": 0, "id": "26414", "a_count": 1, "name": "yellowsnow"}, {"d_count": 0, "id": "6414", "a_count": 1, "name": "bee"}, {"d_count": 1, "id": "2542", "a_count": 2, "name": "yellow"}, {"d_count": 0, "id": "26442", "a_count": 1, "name": "kahit"}, {"d_count": 0, "id": "13877", "a_count": 1, "name": "ano"}, {"d_count": 0, "id": "26461", "a_count": 1, "name": "uniqum"}, {"d_count": 0, "id": "26463", "a_count": 4, "name": "somecolor"}]}, "messages": [], "new_color": "ffba13"}'
 @unsuccessful_color_response = '{"colors": [{"timestamp": 1356329465, "hex": "ffba1", "id": -1, "tags": []}], "schemes": [], "schemes_history": {}, "success": true, "colors_history": {}, "messages": [], "new_color": "ffba1"}'
 end

 it "converts a json color into a Color object" do
 stub_request(:get, "http://www.colr.org/json/color/ffba13").
 to_return(body: @successful_color_response, content_type: "application/json")

 @color = nil
 Color.find('ffba13') do |color|
 @color = color
 resume
 end

 wait_max 1.0 do
 @color.class.should == Color
 end
 end

 it "returns a nil when no color is found" do
 stub_request(:get, "http://www.colr.org/json/color/nothing").
 to_return(body: @unsuccessful_color_response, content_type: "application/json")

 @color = nil
 Color.find('nothing') do |color|
 @color = color
 resume
 end

 wait_max 1.0 do
 @color.class.should == NilClass
 end
 end

 it "tells the Colr api to add a tag to the color" do
 stub_request(:post, "http://www.colr.org/js/color/ffba13/addtag/").
 to_return(body: @successful_color_response, content_type: "application/json")

 @color = Color.new(hex: 'ffba13')
 @color.add_tag('mustard') do |color|
 @color = color
 resume
 end

 wait_max 1.0 do
 @color.class.should == NilClass
 end
 end
 end
end

Things to Note

First, we need to engage the WebStub library, and disable any network access. WebStub will let us know of any unauthorised networked access and block it, perfect for debugging url’s.

1
2
3
4
5
extend WebStub::SpecHelpers

before do
 disable_network_access!
end

Set up the response that WebStub should give you if you call the stubbed URL. This is better done from fixtures, but for verbosity I’m putting it here for now.

1
@successful_color_response = '{"colors": [{"timestamp": 1348628320...olor": "ffba13"}'

Now, set up WebStub to listen on the URL that we’re calling for this test.

1
2
stub_request(:get, "http://www.colr.org/json/color/ffba13").
 to_return(body: @successful_color_response, content_type: "application/json")

Here’s the fun part. HTTP calls are done in a separate thread from the main thread, so catching when they’re done requires a little ‘hooking’. We put the resume method call in the block, my understanding is that this is like calling thread.join in Ruby. We want to wait for it to return in our test thread, not go running off all by itself and returning results to the abyss.

1
2
3
4
5
6
7
8
9
10
11
12
13
# Local variables can go out of scope before a block finishes in RubyMotion, so we use an instance variable that won't be garbage collected.
@color = nil
# Call our Color#find method and set the instance variable to the response
Color.find('nothing') do |color|
 @color = color
 resume
end

# Now we need to wait for the thread to join (this is to go with the resume above)
wait_max 1.0 do
 # Normal test patterns resume here
 @color.class.should == NilClass
end

That’s all there is to it. If you’re using AFMotion like me, your specs will look just the same. The only difference being I usually abstract the service layer to it’s own module/class so I can switch it out later if I have issues.

I hope that helped.