Last year I wrote a RubyCocoa tutorial that’s been aggregated in a few places and referenced a couple times. By my standards it was an overwhelming response. A few things have changed since then, most notably Mac has embraced Ruby.
It’s installed with Leopard, RubyCocoa has been put on it as well, all of which is great news. The drawback is that my tutorial has gone out of date, as Xcode has changed too. Guess I’m gonna have to write a new one.
Naturally my first thought was to update the original tutorial, with new pictures, but I’ve got two problems with that. One, it’s boring. Not for you—for me! Large portions of it haven’t changed aside from some differences in the screenshots and installation.
Why should I explain what Cocoa is again? I just couldn’t get motivated. The second is that I have an issue with the way programming APIs are taught. Why is it that developers, such as myself, who have embraced testing abandon it in example code?
This is especially true in GUI code, where often the simple examples have more complicated tests. Unit testing is so important we should be able to teach APIs Test-First, and in that spirit we’re going to a classic example of TDD using RubyCocoa.
This should teach you how to get up and running and well as make the tutorial a heck of a lot more interesting. For me anyway.
If you’d like to see about 4000000 examples of the bowling game head over to Ron Jefferies website at xprogramming.com. It’s a typical example of solving an algorithmic problem using TDD, and often used as a kata or practice session. We’ll do this following Uncle Bob’s three rules of TDD:
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
We’ll use RSpec for this exercise.
If you aren’t familiar with Cocoa at all, take a peek at the “What the Heck is Cocoa” section here.
If we want to make an awesome bowling game using RubyCocoa we’re going to need to install RubyCocoa. This tutorial is assuming you’re using Leopard, which means you have two steps. The first is to install Xcode from the disc that came with the operating system.
I’m not going to explain that here, I assume you can open a disc and click an installation. For adding RubyCocoa so we can write this slick Mac App you have three options:
- Install from source. A big part of the reason you haven’t seen this doc earlier is because I’ve repeatedly attempted to install from source, and failed miserably before getting it right.
- Install from the latest binary release.
- Do absolutely nothing, say it again. If you’re on Leopard, it’s already there for you.
Step three sounds tempting but there’s a catch. If you’ve used MacPorts to update Ruby, you may find that your Ruby and RubyCocoa installations don’t match up. See Verifying your installation below. Because of this I highly recommend the “do nothing” choice if you can get away with it.
The latest Apple Software Update even updated the bridge, so you won’t be very behind anyway. We also need RSpec as we’ll be using it for our unit testing.
1 sudo gem install rspec.
The great thing about RSpec is it reads extremely well to the uninitiated, so I won’t be explaining RSpec as I go.
Verifying your installation
After installing however you want to install, go to
require ‘osx/cocoa’. If you get true then your system is
setup properly for rubycocoa development. If not you may need to tweak some
environment variables, or worse install from source. I’ll have a blog post
on that soon.
Your first project
We’ll need to create a new RubyCocoa project. Start up Xcode and create a new Cocoa-Ruby project.
Your screen will probably look different, unless you have the iPhone development kit installed too. The important thing is that you select Cocoa-Ruby application. Create the project with whatever name you like, as long as you call it bowling.
This will generate an application skeleton in Xcode. Let’s create our super slick bowling game interface in Interface Builder. Double Click Main Menu.nib in your Xcode project file. From there you should get windows like the following (I’ve arranged them to fit the window better):
Your Window won’t be there yet. In the Nib Document window, that’s the
MainMenu.nib on top, double-click the Window icon. That Window
represents your dialog box.
You can see it on the screen in the picture above. Now select the
Window icon again and hit
Apple-Shift-I to bring up the Inspector for
This shows the properties for the Window at the moment, but will change whenever you select a different object. Change the name of the window to bowling now. Our bowling game is on the honor system.
You enter your score, press roll, and it tells you the running tally of your current score and your current frame. This is a little different than the typical implementation, since those simply spit out a total score after all your rolls.
First you need a button so lets select a push button from the Library and drag it to the window. The easiest thing to do is in the bottom spotlight window type Push Button and it will find that button for you.
Drag the push button onto your window. You can rename it to roll simply by double-clicking on the button and changing the text. Let’s make a dialog that looks like this:
I admit not my best work as designs go, but the focus of this tutorial is test-driven GUI code not Interface Builder. This dialog is made up of one Text Field and five Labels. You should be able to search for them just like you did for the Push Button.
The important thing to remember is that there are four labels on the bottom. Two for the headings (Score and Frame) and two for the actual totals (currently 0 and 0). Congratulations we have our dialog box.
If you go back to the Xcode view and click Build and Go you’ll see the exact same view. Of course nothing does…anything. That’s why we need to write some code…er tests. Remember rule #1:
- You are not allowed to write any production code unless it is to make a failing unit test pass.
Normally at this point we’d start making outlets, actions, and connections but we’re not going to do that here. The reason? We don’t have tests. Have I said that enough yet?
To get up and running with RSpec it’s really best to open up your project in something like Textmate but since this is a tutorial and I can’t make you use my machine I’m going to assume you’ll be creating your files in Xcode and running the specs with the terminal.
Given that we have a view I think it’s safe to assume we’ll have a
controller. Create a file called
the root of your project.
As a rule you’ll want have a separate location for your specs, but this will work just fine for our project. If you’re using the Xcode interface you can use the Ruby test case class, we’ll be removing all the code anyway so it doesn’t really matter, the important thing is to create the file.
Do not add it to the Bowling target, this isn’t shipped as part of the product. Our first spec is going to have well—nothing. Remove any generated code in that spec and replace it with this:
1 describe BowlingController do 2 end
To run it you can use the terminal and type
bowling_controller_spec in your Bowling directory. You should
1 $ spec bowling_controller_spec.rb 2 ./bowling_controller_spec.rb:9: uninitialized constant BowlingController (NameError) 3 from /opt/local/lib/ruby/gems/1.8/gems/rspec-1.1.3/lib/spec/runner/example_group_runner.rb:14:in `load' 4 from /opt/local/lib/ruby/gems/1.8/gems/rspec-1.1.3/lib/spec/runner/example_group_runner.rb:14:in `load_files' 5 from /opt/local/lib/ruby/gems/1.8/gems/rspec-1.1.3/lib/spec/runner/example_group_runner.rb:13:in `each' 6 from /opt/local/lib/ruby/gems/1.8/gems/rspec-1.1.3/lib/spec/runner/example_group_runner.rb:13:in `load_files' 7 from /opt/local/lib/ruby/gems/1.8/gems/rspec-1.1.3/lib/spec/runner/options.rb:85:in `run_examples' 8 from /opt/local/lib/ruby/gems/1.8/gems/rspec-1.1.3/lib/spec/runner/command_line.rb:19:in `run' 9 from /opt/local/lib/ruby/gems/1.8/gems/rspec-1.1.3/bin/spec:4 10 from /opt/local/bin/spec:16:in `load' 11 from /opt/local/bin/spec:16
Congrats! You’ve got a failing test. What do we need to do to make that
pass? We need a controller of course. Create a new class and call it
You need to use Xcode this time and make sure you add it to the Bowling Target, otherwise it won’t be executed later.
The class should look like this:
1 class BowlingController 2 end
Run the test—we…still don’t pass. We’ll need to require that bowling controller in our spec.
1 require 'bowling_controller' 2 3 describe BowlingController do 4 end
Well man we’re cooking with gas now. What does a
BowlingController do? Well let’s go over some Cocoa concepts.
In Cocoa objects have actions and outlets. An action is a method in your
object that can be triggered by another Cocoa object.
An outlet is a reference to another object that was instantiated by Cocoa. Put more simply actions are methods, objects are things. So let’s look back at our glorious UI design.
Ahhhh. I could look at it all day. There is one action here, the pushing of the roll button. There’s three outlets: the score, the frame, and the number of pins. Can you see the difference yet?
Since we’re test driving the app we’re going to start with the action. First let’s write a test. Put this between the describe … end block in your spec:
1 before(:each) do 2 @controller = BowlingController.new 3 end 4 5 it "should roll a ball" do 6 @controller.roll 7 end
There’s a couple things worth paying attention to here. The before(:each) block will create the controller for each test. I think it’s a safe assumption we’ll have more than one. Second notice that our it block isn’t a coherent test yet. So why do we stop?
Because when we run that test it fails—roll is not a method. That’s enough to make us write some code to make the test pass. These cycles should typically take less time than it does for me to explain them.
Now I know this seems stupid, and I almost certainly wouldn’t write my tests at this granular a level if this wasn’t an example, but when I write my tests this small a few things happen. Firstly my code is almost always better. The larger my tests the more room for error.
The second is my tests tell a story, which can be followed by the next guy through my code to see what I was doing. Finally whenever I mess up (quite frequently) I can always go back with a couple undos. Now quit you’re whining and let’s make the test pass.
1 class BowlingController 2 3 def roll 4 5 end 6 end
See that wasn’t so hard. Now let’s make that test more useful. When we roll a ball we get the value from the Text Field on the screen. We use outlets to reference other Cocoa objects, so we’ll need an outlet that references that Text Field. Let’s call it pins.
To get a value from a Text Field we can call one of many methods on the
NSTextField class, in this case we know it is going to be an integer so
let’s use the intValue method. This is demonstrated with a mock object,
which is built into RSpec, and we’ll be adding this to our test.
1 it "should roll a ball and get the value from the pins outlet" do 2 mock_text_field = mock('Pins') 3 @controller.pins = mock_text_field 4 5 mock_text_field.should_receive(:intValue).and_return(0) 6 @controller.roll 7 end
I’m using the same test I did before, only I’ve extended it. I’ve said
I should have a mock that receives the value
intValue when the roll
method has been called. What I love about this test is that it explains
just how data is retrieved from the pins outlet, in an executable way.
Now lets run it…kaboom! There’s no
pins= method. Broke my own rule
didn’t I? We don’t have a method
pins=, but we also know that mock
object is going to fail. That’s two failures not one.
1 class BowlingController 2 attr_accessor :pins 3 4 def roll 5 6 end 7 end
Okay we’ve got a new error:
1 ‘Pins’ expected :intValue with (any args) once, but received it 0 times.
This is great, let’s call it and get it to pass.
1 class BowlingController 2 attr_accessor :pins 3 4 def roll 5 @pins.intValue 6 end 7 end
Okay we’ve got a passing test, but do we have a useful controller? I should say not. None of this code is aware of Cocoa, and we can fake this out all day but at some point we have to make this accessor a true outlet. The question is how do we make this object cocoa aware?
A quick lesson on the Cocoa class hierarchy. The base class is
NSObject, which is represented in RubyCocoa as
OSX::NSObject. In order to know about outlets, actions, and
any grand Cocoa thing we’ll need to make our class a Cocoa object.
So before continuing to make roll more useful, let’s write a test for that.
1 it "should be an OSX::NSObject" do 2 @controller.is_a?(OSX::NSObject).should == true 3 end
Run that spec and you get…hmm, uninitialized constant OSX. We’re going to need to require the files because even though when you’re running a RubyCocoa application everything is included for you we’re running our tests independent of the application.
Require ‘osx/cocoa’ at the top of bowling controller. Ah here it is:
1 'BowlingController should be an OSX::NSObject' FAILED 2 expected: true, 3 got: false (using ==) 4 ./bowling_controller_spec.rb:18:
Make it inherit from
OSX::NSObject and we’re good to go. See
that? We just made a Ruby object inherit from a Cocoa object. Pretty cool
eh? Now about that accessor—pins. I mentioned earlier that Cocoa uses
outlets to communicate with the other objects in it’s world.
An accessor may make our test pass, but it won’t make our application work. We need it to be an outlet, and there we’ve got ourselves a problem. You see outlets aren’t really anything special at all from a programmatic perspective.
We wire those parts together in Interface Builder, save it to the Nib file, and when the Cocoa application starts up it figures out everything it needs to instantiate.
In fact in Objective-C they are declared with an empty macro, in
RubyCocoa we use the command
ib_outlets which just makes a
writer attribute on the class. So how do we test this? Unfortunately,
we don’t, breaking rule #3.
You see we could write everything without the tags, but Interface Builder wouldn’t magically find the attributes so we’d have to add them manually. Since tagging these objects as outlets doesn’t actually change behavior you can grind your teeth to tiny nubs and change the pins to an outlet:
1 class BowlingController < OSX::NSObject 2 ib_outlets :pins 3 4 def roll 5 @pins.intValue 6 end 7 end
We’ve got one other issue here, roll isn’t marked as an action. In the
same vein as ib_outlets we’re going to mark this as an action with the
1 ib_action :roll 2 def roll 3 @pins.intValue 4 end
Also worth noting is that every Cocoa action has a sender, sent as a parameter, but I’ve omitted that here as we won’t be using it so it’s not necessary.
The typical Cocoa style is to keep it in as a parameter, because standard Objective-C Cocoa needs it, but I don’t understand why we need the clutter. From this point forward we need to implement the rest of the controller.
This blog is getting exceedingly long, so I’m going to just put the tests and the code, one at a time, until our controller is implemented.
Note I’ve added the mocks to the
before(:each) because I’ll need them
more than once.
1 test 2 3 before(:each) do 4 @controller = BowlingController.new 5 @bowling = mock('Bowling') 6 @text_field = mock('Pins') 7 @text_field.stub!(:intValue).and_return(10) 8 @controller.pins = @text_field 9 end 10 11 ... 12 13 it "should have an outlet to a bowling object" do 14 @controller.bowling = @bowling 15 end 16 code 17 18 class BowlingController < OSX::NSObject 19 ib_outlets :pins, :bowling 20 test 21 22 it "should send the pin value to the bowling object" do 23 @bowling.should_receive(:roll).with(10) 24 25 @controller.roll 26 end 27 code 28 29 ib_action :roll 30 def roll 31 @bowling.roll(@pins.intValue) 32 end
Oops! Better setup the bowling object in the before each.
1 test 2 3 before(:each) do 4 @controller = BowlingController.new 5 @bowling = mock('Bowling') 6 @text_field = mock('Pins') 7 @text_field.stub!(:intValue).and_return(10) 8 @controller.pins = @text_field 9 @controller.bowling = @bowling 10 @bowling.stub!(:roll) 11 end 12 it "should have an outlet to the score" do 13 @score = mock('Score') 14 @controller.score = @score 15 end 16 code 17 18 class BowlingController < OSX::NSObject 19 ib_outlets :pins, :bowling, :score 20 test 21 22 before(:each) do 23 ... 24 @bowling.stub!(:roll) 25 @bowling.stub!(:score).and_return(100) 26 @score = mock('Score') 27 @controller.score = @score 28 @score.stub!(:setIntValue) 29 end 30 31 ... 32 33 it "should update the score with the new score from the roll" do 34 @bowling.should_receive(:roll).ordered 35 @bowling.should_receive(:score).ordered.and_return(100) 36 @score.should_receive(:setIntValue).ordered.with(100) 37 38 @controller.roll 39 end
I want to pause and point out we’ve got a new method here. This is
where our tests come in handy. We can see from looking at the tests
intValue are called on
outlets to get and set values from them.
We’re using mocks to demonstrate how we use that API, and by the way we’re building our tests we can see our tests tell the story of how this controller was built. You should also see I’ve made a bowling object, the Model in the MVC setup, that is also an outlet.
I’ve done that because once we’ve setup this controller and that object in Interface Builder, the Cocoa framework will properly handle the creation of the objects if they are outlets. Back to the code…
1 code 2 3 ib_action :roll 4 def roll 5 @bowling.roll(@pins.intValue) 6 7 @score.setIntValue @bowling.score 8 end 9 test 10 11 it "should have an outlet to the frame" do 12 @frame = mock('Frame') 13 @controller.frame = @frame 14 end 15 code 16 17 class BowlingController < OSX::NSObject 18 ib_outlets :pins, :bowling, :score, :frame 19 test 20 21 before(:each) do 22 ... 23 @frame = mock('Frame') 24 @controller.frame = @frame 25 @frame.stub!(:setIntValue) 26 @bowling.stub!(:frame).and_return(9) 27 end 28 29 ... 30 31 it "should update the current frame with the frame from the bowling object" do 32 @bowling.should_receive(:roll).ordered 33 @bowling.should_receive(:frame).ordered.and_return(9) 34 @frame.should_receive(:setIntValue).ordered.with(9) 35 36 @controller.roll 37 end 38 code 39 40 def roll 41 @bowling.roll(@pins.intValue) 42 43 @score.setIntValue @bowling.score 44 @frame.setIntValue @bowling.frame 45 end
Great! We’ve done it. We’ve implemented a controller that plays bowling. Unfortunately since it uses a bowling object we haven’t created it doesn’t actually…do…anything yet.
This blog post is really about RubyCocoa and not about bowling, which is an article on it’s own. So I’m going to just write a copy of bowling here and if you want to use it just save it into the same directory as your controller, although I recommend writing your own.
So we’ve got a controller and a model, now we must make Interface Builder aware of it and build our wicked awesome application. That’s right, it’s wicked awesome.
Open up Interface Builder and make sure you’ve got the Library window
open. Go to Objects and Controllers and choose the
You’ll need to create two of these, one for each class in your
application. Remember how they inherited from
OSX::NSObject? Well drag
NSObject over to the Nib Document window.
Using the inspector name it
BowlingController. Then, and this is the
cool part, open finder and drag the
into the Nib Document window. There will be a brief pause then open the
Inspector again. You should see something like this:
See those outlets and actions? Previously we had to do all that in Interface Builder, and get it right the first time, otherwise regenerate it. There were ways around it, but I much prefer this way. Anytime you’ve changed the outlets and actions you simply drag the new class.
Now do the same thing with the bowling class. The outlets and actions are are in the Nib File, along with their associated implementation, but your Cocoa application still doesn’t know how are they connected.
Pause and take a minute to think about this, how does
know what pins points to? How about bowling? The answer is it doesn’t,
we’ll be embedding that information into the Nib file here.
To do that you drag from the item that needs to know something to the
item it needs to know about. Let’s try this with our famous action,
the Roll button. Hold the
ctrl key and drag to the bowling controller.
Unfortunately I can’t get a screenshot since I can’t
ctrl-drag and take
the nice shot at the same time, but what you should see is a blue line
from the Roll button that you’ll drag to the bowling controller.
When you let go the screen should flash Received Actions roll:, click
the roll. To verify completeness open
BowlingController in the
inspector and click the Connections tab, that’s the blue circle with
the error in it, and you should see something like this:
Under received actions it shows that roll: is connected to Push Button
(Roll). Embedded in the Nib file is the information Cocoa needs to send
the message roll to
BowlingController when the button is pushed. Now we
need to connect the rest of the conections.
Connect from the
BowlingController to the pins Text Field, the score,
and the frame. Finally connect from
BowlingController to the Bowling
Remember you’re going from
BowlingController to the other objects
BowlingController is using those outlets.
Each time you’ll be asked to pick the proper outlet, and if you named
your outlet properly this should be obvious. You should see this in
your inspector for
BowlingController when you are done.
Well build that puppy. Holy cow it works!
Well it should. If it doesn’t you may have made one of the classic blunders:
Did you make sure that
bowling.rbare in your build target?
- Do all your tests pass?
- Did you save your Nib file?
- Are all you connections drawn?
Well that about does it for me, I hope this helped you learn a bit about TDD and a bit about RubyCocoa. Here’s a quick review:
- What is Cocoa? What is RubyCocoa?
- Installing RubyCocoa
- Test-Driving your first controller
- Basics of Outlets
- Basics of Actions
- Connecting Outlets and Actions
- Instantiating Cocoa Outlets
- Importing Ruby classes into a Nib file
- Inheriting from
- Getting a value from an outlet
Here’s hoping this helped put to bed the myth that GUIs can’t be tested as well.