Using RSpec with MacRuby, Part 2

Update 11/Dec/2009: The described solution doesn't work with MacRuby 0.5 as noted by Matt Aimonetti in the comments of the first part.

This is the second part of our small series about MacRuby and RSpec. In the first part, we have added RSpec to the example project MacRubyTwitterMG, did a small patch to make it run and defined the first two specs. Now we want to define some more specs and learn how to use mock objects:

  1. The next spec shall test the method refresh and looks like this:
    describe "#refresh" do
      it 'should refresh the tweets when username and password are given' do
        @main_controller.username = "ashtom" 
        @main_controller.password = "secret" 
        @twitterEngine.should_receive(:setUsername).with("ashtom", password:"secret")
        @twitterEngine.should_receive(:getFollowedTimelineSinceID)
        @main_controller.refresh(nil)
      end
    end
    
    For several reasons we do not really want to connect to Twitter here, so we try to mock the object @twitterEngine.
    before :each do
      @twitterEngine = mock("MGTwitterEngine")
      @twitterEngine.stub!(:initWithDelegate).and_return(@twitterEngine)
      MGTwitterEngine.stub!(:alloc).and_return(@twitterEngine)
    end
    
    The problem with this code is that MacRuby does not know the class MGTwitterEngine while running the specs from the command-line, because we have added all of its Objective-C source files directly to our Xcode project. A possible solution might be to create a framework for MGTwitterEngine, so let's try this.
  2. Select File > New Project..., then choose Cocoa Framework and continue with Choose.... Enter MGTwitterEngineFramework as the name of the new framework and put it into the Vendor directory which we've already created.
  3. Drag the group Vendor > MGTwitterEngine from the project MacRubyTwitterMG to the newly created project.
  4. Right-click the target MGTwitterEngineFramework, choose Get Info and change the following settings:
    • Architectures: 32/64-bit Universal
    • Objective-C Garbage Collection: Supported [-fobjc-gc]
  5. Close the inspector.
  6. Select x86_64 as the active architecture, then hit Build & Go.
  7. Now we change the file spec_helper.rb in the MacRubyTwitterMG project to include the new framework:
    framework File.expand_path(File.dirname(__FILE__) + '/../Vendor/MGTwitterEngineFramework/build/Debug/MGTwitterEngineFramework.framework')
    
  8. Back to the spec, we need to call the method awakeFromNib to initialize the instance of MGTwitterEngine:
    before :each do
      # ...
    
      @main_controller.awakeFromNib
    end
    
    As awakeFromNib is accessing the member @timelineTableView (which is an IBOutlet connected to the table view) we need to mock it as well:
    before :each do
      # ...
    
      @timelineTableView = mock("NSTableView")
      @timelineTableView.stub!(:dataSource=)
      @main_controller.timelineTableView = @timelineTableView
    
      @main_controller.awakeFromNib
    end
    
  9. We run the specs:
    $ macruby Spec/Controllers/MainControllerSpec.rb 
    ...
    
    Finished in 0.078637 seconds
    
    3 examples, 0 failures
    
  10. Done.

The complete code is (as always) available at GitHub.

Aftermath

After having defined some more specs I now get the following warning:

~/Projects/MacRubyTwitterMG/Vendor/RSpec/lib/spec/mocks/proxy.rb:201: 
warning: removing pure Objective-C method `proxied_by_rspec__alloc' may cause 
serious problem
This seems to be related to the stub of MGTwitterEngine.alloc, but so far I've found no way to prevent the warning. If you have a solution, please post a comment.

Keywords: mac, programming, ruby

Added by Thomas Dohmke 279 days ago (0 Comments)

Using RSpec with MacRuby, Part 1

Update 11/Dec/2009: The described solution doesn't work with MacRuby 0.5 as noted by Matt Aimonetti in the comments.

After playing with MacRuby for a while, I was definitely missing something: RSpec. I got so used to writing tests or specs with various Rails projects that I am feeling somewhat lost, lonely and uncertain when no testing framework is available... ok, maybe it's not that bad. :) Anyway, this article explains my first steps to run RSpec with MacRuby 0.4.

The starting point is my last post about MGTwitterEngine and MacRuby. If you haven't already read it, you might want to give it a look.

  1. Open the console and checkout MacRubyTwitterMG from GitHub:
    cd ~/Project
    git clone git@github.com:ashtom/macruby_twitter_mg.git MacRubyTwitterMG
  2. Switch to the Vendor subdirectory:
    cd Vendor
  3. Checkout RSpec:
    git clone git://github.com/dchelimsky/rspec.git RSpec
  4. Create a directory for your specs (I choose Spec with a capital letter only because Classes and Vendor also start with a capital letter):
    cd ..
    mkdir Spec
  5. Create a file named spec_helper.rb:
    $LOAD_PATH << File.expand_path(File.dirname(__FILE__) + '/../Vendor/RSpec/lib/')
    require 'spec/autorun'
    
    framework 'Cocoa'
    
    # Loading all the Ruby project files.
    dir_path = File.expand_path(File.dirname(__FILE__) + '/../Classes/')
    Dir.entries(dir_path).each do |path|
      if path != File.basename(__FILE__) and path[-3..-1] == '.rb'
        require(dir_path + '/' + path)
      end
    end
    In the first line we add RSpec to the load path of MacRuby, in the second line we require the RSpec runner. The rest of the file is more or less a copy of the rb_main.rb and loads all MacRuby project files.
  6. Create a very simple test spec in test_spec.rb to test whether RSpec actually works:
    require File.expand_path(File.dirname(__FILE__) + '/spec_helper.rb')
    
    describe "RSpec" do
      it "should work" do
        "Hello World".length.should == 11
      end
    end
  7. Back on the console, we try to run the spec:
    $ macruby Spec/test_spec.rb 
    F
    
    1)
    NameError in 'RSpec should work'
    undefined local variable or method `setup_mocks_for_rspec' for #<Spec::Example::ExampleGroup::Subclass_1:0x80064ec00>
    
    Finished in 0.0821 seconds
    
    1 example, 1 failure
    
  8. Hm... I haven't found any solution or hints for this error at Google, so I tried to fix this by myself. Here's my approach: Open the file /Vendor/RSpec/lib/spec/runner/options.rb and search for the following line (at the time of writing this post, it was line 273):
    Spec::Example::ExampleMethods.__send__ :include, Spec::Adapters::MockFramework
    Change the line to
    Spec::Example::ExampleGroup.__send__ :include, Spec::Adapters::MockFramework
    and save the file. If you have a better solution, please leave a comment below this post. Thanks!
  9. Try to run the spec again:
    $ macruby Spec/test_spec.rb 
    .
    
    Finished in 0.077641 seconds
    
    1 example, 0 failures
  10. Yay! :)
  11. Finally we can write our first real spec for the MainController.
    require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
    
    describe "MainController" do
      describe "#numberOfRowsInTableView" do
        before :each do
          @main_controller = MainController.new
        end
    
        it "should return zero if timeline is empty" do
          @main_controller.timeline = []
          @main_controller.numberOfRowsInTableView(nil).should == 0
        end
    
        it "should return the number of entries in the timeline" do
          @main_controller.timeline = ["one", "two", "three"]
          @main_controller.numberOfRowsInTableView(nil).should == 3
        end
      end
    end
    I've put this stuff under Spec/Controllers/MainControllerSpec.rb although the spelling looks a bit awkward for now. As before, the spec is executed with a simple shell command:
    $ macruby Spec/Controllers/MainControllerSpec.rb 
    ..
    
    Finished in 0.082415 seconds
    
    2 examples, 0 failures

That's it with this post, still pretty simple, but it works. In the next post (coming soon) we'll do some more testing and see how to use mocks. In the meantime, your suggestions and comments are very welcome.

Git Repository

I've put all code of this post into the branch spec of MacRubyTwitterMG's git repository at GitHub. Feel free to fork. :)

Keywords: mac, programming, ruby

Added by Thomas Dohmke 288 days ago (4 Comments)

Using MGTwitterEngine with MacRuby (or How to Build a Twitter Client with 37 Lines of Code)

Last week, the guys at PeepCode published their first screencast about MacRuby: Meet MacRuby. In it the presenter develops a simple Twitter client, which he actually calls "the world's simplest Mac Twitter client in Ruby" (at 21:05min in the screencast). While the screencast is great and the client itself is really quite simple, I was curious whether this is also the simplest way to do it. Looking at the accompanying code of the screencast, a lot of effort goes into connecting to the Twitter API and parsing of the returned XML. Twitter clients have been developed all over the place, so there should be a way to use some open source library and avoid most of this networking stuff.

My first approach was to install one of the Ruby libraries for Twitter such as Twitter4R or Twitter, but both seem not to work with version 0.4 of MacRuby. The cool thing with MacRuby is that it combines two worlds: Ruby and Cocoa. This brings in the broad range of Cocoa frameworks and with it MGTwitterEngine. MGTwitterEngine is an open source library to integrate Twitter into any Cocoa app (including those on the iPhone). So let's try to build a MacRuby app with it. As before, we assume that you have already installed Xcode 3.1 and MacRuby 0.4.

  1. Create a new MacRuby project in Xcode called "MacRubyTwitterMG".
  2. Checkout MGTwitterEngine:
    svn co http://svn.cocoasourcecode.com/MGTwitterEngine/
  3. Open MGTwitterEngine.xcodeproj in Xcode.
  4. Open the group Classes > MATwitterEngine in the source list. Then drag and drop all files beginning with MGTwitter as well as the complete groups Twitter NSXML Parsers and Categories to your MacRuby project (I created a group Vendor > MGTwitterEngine for it).
  5. Next we add a new Ruby class to the project. Right-click Classes and select New File.... Choose the template Ruby class, click Next, enter MainController.rb as the File Name and hit Finish.
  6. Add four attributes for username, password and status as well as the table view, in which we will show the tweets. Also add two methods for refreshing the timeline and sending updates.
    class MainController
      attr_accessor :username, :password, :status
      attr_writer :timelineTableView
    
      def refresh(sender)
      end
    
      def sendUpdate(sender)
      end
    end
    
  7. Open the file MainMenu.nib in Interface Builder. Put two text fields, one push button, one table view and a multi-line text field to the main window. The layout doesn't matter, mine looks like this:
  8. Drag and drop an instance of NSObject (the blue cube) from the IB library to your MainMenu.nib. Set its class to MainController.
  9. Ctrl-click the button, drag to Main Controller and connect it with the method refresh.
  10. Ctrl-click the Main Controller, drag to the table view and connect it with the attribute timelineTableView. The table view should have only one column (see above screenshot).
  11. Ctrl-click the multi-line text field, drag to Main Controller and connect it with the method sendUpdate.
  12. Select the multi-line text field, open the Attributes inspector (Cmd+1) and choose Sent On Enter Only for the attribute Action.
  13. Select the first text field, open the Bindings tab in the inspector (Cmd+4), activate Bind to and select Main Controller. Enter self.username in the Model Key Path field.
  14. Repeat the previous step for the second text field and set the binding to self.password.
  15. Repeat the previous step for the multi-line text field and set the binding to self.status.
  16. Save the nib file. If you get the following warning, click Review Issues, then select Mac OS X 10.5.x as the Deployment Target. This should "solve" the issues and you can save the file.

  17. Back in MainController.rb, we add the following method:
     def awakeFromNib
        @timeline = []
        @timelineTableView.dataSource = self
        @twitterEngine = MGTwitterEngine.alloc.initWithDelegate(self)
      end
    

    The method initializes an empty array for our timeline. It also sets the attribute datasource of the table view to self and initializes the MGTwitterEngine with self as the delegate.
  18. For the table view we need two simple methods:
     def numberOfRowsInTableView(view)
        @timeline.size
      end
    
      def tableView(view, objectValueForTableColumn:column, row:index) 
        @timeline[index]
      end
    

    The first method returns the number of rows in the table view, which we can get from the number of entries in our array. The second method returns the corresponding entry for each row.
  19. Next, we implement the method refresh:
      def refresh(sender)
        @twitterEngine.setUsername(username, password:password)
        @twitterEngine.getFollowedTimelineSinceID(0, startingAtPage:0, count:20)
      end
    

    We set username and password, then ask the engine to get our current timeline.
  20. MGTwitterEngine has five delegate methods (see below), but for now we're only interested in the tweets so we go with an implementation for statusesReceived:
     def statusesReceived(statuses, forRequest:identifier)
        @timeline = []
        statuses.each do |status|
          @timeline << status["user"]["name"] + ": " + status["text"]
        end
        @timelineTableView.reloadData
      end
    

    The parameter statuses is an array with a hash for each tweet. We iterate over it, take the user's name and the status text, concat it to a string and append it to our timeline array. Then we reload the data in the table view.
  21. The last thing is to send an update to Twitter. When the user hits enter in the multi-line text field, the method sendUpdate is invoked. We implement it as follows:
     def sendUpdate(sender)
        @twitterEngine.sendUpdate(sender.stringValue)
        sender.setTitleWithMnemonic("")
      end
    

    The parameter sender points to our text field, so we send its stringValue, i.e. the entered text, to Twitter and clear the text.
  22. In addition we need a small change to statusesReceived:
     def statusesReceived(statuses, forRequest:identifier)
        return if statuses.count == 1 and statuses.first["source_api_request_type"] == 5
    
        @timeline = [] 
        # ...
      end
    

    The reason for this is that the MGTwitterEngine calls this method after it has sent the status update. In this case, the parameter statuses contains only the new tweet, so that the initialization of @timeline in the next line would unintentionally clear the whole timeline. We therefore ignore this update and wait for the next click on Refresh to get the newest timeline.
  23. Finally, build the app and run it. Enter your credentials, click on Refresh and wait for your tweets to appear.

37 lines of code plus some magic in Interface Builder and we're done. Pretty simple, huh?

Aftermath

As said above, MGTwitterEngine provides five delegate methods in total:

def requestSucceeded(requestIdentifier)
end

def requestFailed(requestIdentifier, withError:error)
end

def statusesReceived(statuses, forRequest:identifier)
end

def directMessagesReceived(messages, forRequest:identifier)
end

def userInfoReceived(userInfo, forRequest:identifier)
end
For a real application, we would use at least the first three methods to do sufficient error handling, e.g. display an alert panel if the user has entered a wrong password. Furthermore we could also use these methods to show some status information, e.g. as a status label or an activity indicator like this:
  def refresh(sender)
    @indicator.startAnimation(sender)
    # Start engine
    # ...
  end

  def requestSucceeded(requestIdentifier)
    @indicator.stopAnimation(self)
  end

  def requestFailed(requestIdentifier, withError:error)
    @indicator.stopAnimation(self)
    # Show some error message
    # ...
  end

Git Repository

The complete project is available at GitHub. Feel free to fork. :)

Update 1 (29/May/2009)

If you encounter the problem that the password is not accepted although you've entered it correctly, try the following fix:

  1. Open MainMenu.nib in Interface Builder.
  2. Select the password field and open the Bindings inspector (Cmd + 4).
  3. Open Value and activate the option Continuously Updates Value.
  4. Optionally repeat the previous steps for the login field.
  5. Save. Rebuild.

Keywords: mac, programming, ruby

Added by Thomas Dohmke 292 days ago (3 Comments)

Using BGHUDAppKit with MacRuby

This is a short tutorial on how to use BGHUDAppKit together with MacRuby. We assume that you have already installed Xcode 3.1 and MacRuby 0.4.

  1. Download (or rather checkout) the current version of BGHUDAppKit:
    svn checkout http://bghudappkit.googlecode.com/svn/trunk/ bghudappkit
  2. Open the file bghudappkit/BGHUDAppKit.xcodeproj in Xcode.
  3. In the source list on the left side, search for the target BGHUDAppKit, right-click it and choose Get Info (or Cmd-I).
  4. Select the tab Build and search for the option Architectures.
  5. Change the value of Architectures to 32/64-bit Universal (otherwise you will get a "no matching architecture in universal wrapper" error when launching your MacRuby application).
  6. Switch back to the project view and build the project (menu Build > Build or Cmd-B).
  7. After the build is complete, drag the file BGHUDAppKit.framework to the Frameworks group of your MacRuby project.
  8. Select your active target, right-click it and choose Add › New Build Phase › New Copy Files Build Phase.
  9. Double-click the new "Copy Files" build phase and select Frameworks from the Destination drop down.
  10. Drag the BGHUDAppKit.framework from the Frameworks group to the newly created build phase.
  11. Open the file rb_main.rb, search for
    framework 'Cocoa'
    and add the following line after it:
    framework 'BGHUDAppKit'
  12. Build your MacRuby project. It should run normally and raise no errors.

Finally you can access the new HUD controls in Interface Builder and add them to your interfaces, configure their properties, connect them with IBOutlets and IBActions, etc.

Keywords: mac, programming, ruby

Added by Thomas Dohmke 293 days ago (0 Comments)

Download Statistics for Ruby Helper

Ruby Helper is now available in the App Store for more than 8 weeks, so we think it's time to take a look at the download statistics. With the end of the 8th week of it's approval, the app has been downloaded 1679 times including 593 upgrades from 1.0 to 1.1. Relating the number of upgrades to the number of downloads before the release of version 1.1 results in an upgrade rate of currently 65%.

After the initial peak, the average number of downloads per week seems to settle at about 80. The lowest daily value was 9 downloads, the highest 119 downloads on the first day or 215 downloads when including the number of upgrades on the first full day of version 1.1 (4th of May).

Looking at the countries, the most downloads came from the USA with about 45% of all downloads. Second is Japan with 11% followed by Germany with 7%, the UK with 5% and France with 3%. All other countries are below 3%. (It would be interesting to correlate this data with the distribution of Ruby developers as well as sold iPhones in these countries...)

Last but not least, three user wrote a review in the App Store, two of them voted with 4 stars, one with 5 stars. The average value of all ratings (including those without text) is 3.5 stars.

Keywords: iphone, statistics

Added by Thomas Dohmke 293 days ago (0 Comments)

1 2 Older posts »

Twitter

Follow us on Twitter:

Keywords