Test-first Ruby Swing apps

My favourite language is Ruby. So I start with Ruby. I want to test-first the UI without instantiating the GUI, so I use the Presenter First pattern. This example is a direct port of the Presenter First Java sample. I want to do BDD, so I use the Ruby test framework RSpec.

First, I will need to install Ruby. Next, I use RubyGems, which is included with Ruby, to install RSpec by executing ‘gem install -r rspec’. I install the RDT Eclipse plugin, but this is optional.

Let’s start with the test:

# TestPasswordPresenter.rb
require 'PasswordPresenter'
context "login dialog"  do
  setup do
    @viewMock = mock("view")
    @modelMock = mock("model")
    @viewMock.should_receive(:addPasswordSubmittedListener) do |a|
      @listener = a
    end
    PasswordPresenter.new(@viewMock, @modelMock)
  end
  specify "should display success for correct password" do
    setupValidation(true)
    @viewMock.should_receive(:displaySuccess).once
    @listener.call
  end
  specify "should display try again for incorrect password" do
    setupValidation(false)
    @viewMock.should_receive(:displayTryAgain).once
    @listener.call
  end
  def setupValidation(validationSuccess)
    password = "password"
    @viewMock.should_receive(:getSubmittedPassword)
      .once_and_return(password)
    @modelMock.should_receive(:validatePassword)
      .with(password)
      .once_and_return(validationSuccess)
  end
end

To run the test, I use ‘spec TestPasswordPresenter.rb’. To get this to pass, I need the presenter class:

# PasswordPresenter.rb
class PasswordPresenter
  def initialize(view, model)
    @view = view
    @model = model
    @view.addPasswordSubmittedListener(lambda { handlePasswordBusinessRule })
  end
  def handlePasswordBusinessRule
    if @model.validatePassword(@view.getSubmittedPassword) then
      @view.displaySuccess
    else
      @view.displayTryAgain
    end
  end
end

An advantage of this approach is that I don’t need to incur the JVM loadtime to run the tests (i.e. I can use Ruby rather than JRuby), since all the Swing references are isolated to the view class.

Alright, now that I’ve tested the presenter, it’s time to create the Swing app. Why a Swing app? Because I haven’t found a standard Ruby UI library, and I already understand the Swing API. A couple years ago I evaluated JRuby, which wasn’t feasible for Swing development, since it would take minutes to load a Swing app. Now that they’ve implemented lazy loading of the Java classes, loadtime is no longer an issue.

I’ll need to install JRuby at this point to access Swing from Ruby code.

Below, you’ll find the rest of the code to run the application.

# PasswordApp.rb
require 'PasswordView'
require 'PasswordModel'
require 'PasswordPresenter'
PasswordPresenter.new(PasswordView.new, PasswordModel.new)
# PasswordModel.rb
class PasswordModel
  def validatePassword(password)
    "password" == password
  end
end
# PasswordView.rb
require 'java'
require 'LambdaActionListener'
include_class ["JButton", "JFrame", "JLabel", "JOptionPane", "JPanel", "JTextField"]
  .map {|e| "javax.swing." + e}
class PasswordView
  def initialize
    panel = JPanel.new
    panel.add(JLabel.new("Password:"))
    @password = JTextField.new(20)
    panel.add(@password)
    @button = JButton.new("Submit Password")
    panel.add(@button)
    frame = JFrame.new("Login")
    frame.setContentPane(panel)
    frame.setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE)
    frame.pack()
    frame.setVisible(true)
  end
  def addPasswordSubmittedListener(listener)
    @button.addActionListener(LambdaActionListener.new(listener))
  end
  def displaySuccess
    JOptionPane.showMessageDialog(nil, "Success. Valid password.")
  end
  def displayTryAgain
    JOptionPane.showMessageDialog(nil, "Password incorrect. Try again.")
  end
  def getSubmittedPassword
    @password.getText
  end
end

This utility class adapts a Ruby Proc object to a Swing ActionListener:

# LambdaActionListener.rb
require 'java'
include_class "java.awt.event.ActionListener"
class LambdaActionListener < ActionListener
  def initialize(listener)
    super()
    @listener = listener
  end
  def actionPerformed(evt)
    @listener.call
  end
end

You can run the main app with ‘jruby PasswordApp.rb’.

Download jruby_presenter-first_rspec.

It's only fair to share...
Share on FacebookGoogle+Tweet about this on TwitterShare on LinkedIn

Leave a Reply