Twitter GitHub Facebook Instagram dirv.me

Daniel Irvine on building software

Walkthrough: Test-driven development of a Qt app in Ruby, part 1

14 June 2014

This is the first in a series of posts on building a desktop GUI using test-driven development. I’ll be using Ruby and Qt, a widely-used cross-platform application framework. Tests will be written using RSpec and 100% coverage will be maintained at all times using SimpleCov.

The purpose of these posts is not to build anything useful, but rather to show the TDD process and how to achieve complete test coverage of desktop GUI code. We’ll be given requirements by an imaginary customer (let’s call her Barbara).

Browsing the code

All of the latest code for this walkthrough can be found in the corresponding Github repository.

If you’re new to Qt (like I was when I started writing this), the Qt 4.8 documentation will be your friend. These refer directly to the C++ version so if you’re not familiar with C++ this can be challenging, but stick with it and you’ll quickly get past that. There are two major differences between the C++ and Ruby APIs:

  1. Class names in Ruby look like Qt::ClassName and in C++ like QClassName. For example, Qt::Application and QApplication.
  2. Many methods have special Ruby setters, so for example rather than calling widget.setWindowTitle(title) you can instead call widget.window_title = title.

Getting started

I’m using Ruby 2.1 with qtbindings 4.8.5.2 which gives access to Qt 4.x. I’m assuming you have familiarity with the following:

  • Installing packages using bundle
  • Basic use of RSpec
  • Using a spec_helper.rb file for RSpec
  • A project structure with bin/, lib/ and spec/ directories

Even if this is new to you, you can figure it out by looking at the commit history for the repository.

Installing Qt

If you’re on Windows, you can skip this step as the qtbindings package contains the necessary binaries. If you’re on Mac, you can run brew install qt. If you’re using Linux, you probably already have Qt installed but if not then it should be available in your package manager.

Setting up your project

To get started, create a project root directory (perhaps calling it QtBindingsTdd) and inside it create the three directories bin/, lib/ and spec/. Within the project root, create a file named Gemfile and add the following lines.

source 'https://rubygems.org'
gem 'qtbindings'
gem 'rspec'
gem 'simplecov'

From the project root directory, run the command bundle install and all the required dependencies will be downloaded and installed onto your system. This may take a while as the qtbindings package can be large.

Configuring RSpec and creating a first test

You’ll need to create a file spec/spec_helper.rb which turns on coverage:

require 'simplecov'

SimpleCov.start do
  add_filter spec/
end

$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'

The add_filter line is telling SimpleCov to ignore coverage of the spec folder, which we do because including them will distort our coverage statistics. We want coverage to be of our production code only.

The final line ($LOAD_PATH...) enables us to write single file names in our require call names without having to prefix our folder names.

If you run rspec now, you’ll get the uninteresting output below.

No examples found.
Finished in 0.00019 seconds (files took 0.05207 seconds to load)

That’s what we expected to see. Great. Let’s start writing some code.

Building a window

Our imaginary user Barbara (remember?) has given us the requirement that there should a window with a button that causes a visible count to increment when clicked. The count should start at 0. Let’s start building that.

So what should we write for our first test? How about that we have a Qt window.

require 'spec_helper'
require 'qt'

describe ClickCounter do

  it 'is a window' do
    app = Qt::Application.new(ARGV)
    window = ClickCounter.new
    expect(window).to be_kind_of(Qt::Widget)
    expect(window.parent).to be_nil
  end
end

A Qt window is actually just a Qt widget that has no parent, hence the two expectations on lines 9 and 10. Of interest is that I’ve determined that we’ll have a class named ClickCounter which will subtype this widget, so that’s our first design decision. You’ll also note the line to initiailize the Qt system by calling Qt::Application.new(ARGV). Without this line, constructing any Qt object will fail.

Let’s run rspec and see what happens.

Coverage report generated for RSpec to /QtBindingsTdd/coverage. 0.0 / 0.0 LOC (100.0%) covered.
/QtBindingsTdd/spec/click_counter_spec.rb:3:in `<top (required)>': 
                                          uninitialized constant ClickCounter (NameError)

The good news is that we have 100% coverage. The bad news is that our tests won’t even run. Let’s fix that by defining the ClickCounter class. Create the file lib/click_counter.rb with the following code.

class ClickCounter
end

Add the following line to the top of spec/click_counter_spec.rb.

require 'click_counter'

Now re-run rspec.

F

Failures:

  1) ClickCounter displays a window
     Failure/Error: expect(window).to be_kind_of(Qt::Widget)
       expected #<ClickCounter:0x007fd1a910afe0> to be a kind of Qt::Widget
     # ./spec/click_counter_spec.rb:10:in `block (2 levels) in <top (required)>'

Finished in 0.42094 seconds (files took 0.21685 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/click_counter_spec.rb:7 # ClickCounter displays a window
Coverage report generated for RSpec to /QtBindingsTdd/coverage. 1 / 1 LOC (100.0%) covered.

Ah ha! Our first actual failing test. We’re still at 100% coverage--and this time it’s covering a single line of code. (Slightly odd, since we wrote 2 lines of code. I guess the ‘end’ doesn’t count.) The failing test is telling us that the window should be a Qt::Widget, so let’s fix that.

require 'qt'

class ClickCounter < Qt::Widget

  def initialize
    super(nil)
  end

end

Running RSpec:

.

Finished in 0.42189 seconds (files took 0.21612 seconds to load)
1 example, 0 failures
Coverage report generated for RSpec to /QtBindingsTdd/coverage. 4 / 4 LOC (100.0%) covered.

Yahhhhooooo! Our first passing test. Amazing. On line 6, I’m passing nil to the Qt::Widget constructor in order to tell it that the widget has no parent--that’s what we wanted.

Moving faster

For the next test, how about that the label 0 appears on the window when it’s first initialized? Add this spec:

it 'shows 0 before any clicks are made' do
  app = Qt::Application.new(ARGV)
  window = ClickCounter.new
  expect(window).to have_label_with_text('0')
end

I’m using a strange-looking expectation on line 4; that’s a special RSpec matcher and we need to define it. Create a file called spec/qt_helpers.rb and add the following.

require 'qt'

RSpec::Matchers.define :have_label_with_text do |expected|
  match do |widget|
    widget.children.any? do |child|
      child.kind_of?(Qt::Label) and child.text==expected
    end
  end
end

What’s going on here? Let’s take a step back and think about Qt widgets. Widgets exist in a hierachy; each widget can have a parent and multiple children. In this way a window or region of a window can be built up as a tree of objects. If you think about the window being the root widget, its children are each of the elements that make up the controls on the window. We can call Qt::Widget#children to retrieve an enumerable of the widget’s children, which we can then search through for a Qt::Label with a certain value set for its text field.

So, this RSpec matcher will return true if the widget holds any Qt::Label with the expected text set on it.

You’ll need to reference this in your spec_helpers.rb file by adding in a call to require 'qt_helpers'.

Your test should now run but it’ll fail. To make this test pass, let’s write some more code in click_counter.rb. Change the constructor to this:

def initialize
    super(nil)
    layout = Qt::VBoxLayout.new
    layout.addWidget(Qt::Label.new('0'))
    setLayout(layout)
  end

More Qt learning here: a top-level Widget places all of its child widgets inside a layout, and in this case we’re using a Qt::VBoxLayout, which stacks items one on top of the other. We simply insert a label with text “0” inside of it. This is the simplest thing that can possibly make the test pass. So let’s run it and find out what happens.

..

Finished in 0.42505 seconds (files took 0.21255 seconds to load)
2 examples, 0 failures
Coverage report generated for RSpec to /QtBindingsTdd/coverage. 7 / 7 LOC (100.0%) covered.

The test passes. What’s more, we still have 100% coverage. On all seven lines of code! Glorious!

Taking stock

I’ll end here for now as this blog post is getting long. We’ve written two passing tests so far, and we’ve got some working, covered code. We haven’t yet seen any running GUI--in fact we haven’t written an entrypoint, so that needs to be done. Maybe we can put that off a bit longer though. More importantly, we’ve got some refactoring to do in our tests, since we now have repeated code across two tests.

In the next part then, we’ll refactor out that code into an RSpec shared context, and we’ll finish off the click counter requirement. And maybe, maybe we’ll create that entrypoint.

About the author

Daniel Irvine is a software craftsman at 8th Light, based in London. These days he prefers to code in Clojure and Ruby, despite having been a C++ and C# developer for the majority of his career.

For a longer bio please see danielirvine.com. To contact Daniel, send a tweet to @d_ir or use the comments section below.

Twitter GitHub Facebook Instagram dirv.me