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:
- Class names in Ruby look like
Qt::ClassNameand in C++ like
QClassName. For example,
- Many methods have special Ruby setters, so for example rather than calling
widget.setWindowTitle(title)you can instead call
widget.window_title = title.
I’m using Ruby 2.1 with qtbindings 188.8.131.52 which gives access to Qt 4.x. I’m assuming you have familiarity with the following:
- Installing packages using
- Basic use of RSpec
- Using a
spec_helper.rbfile for RSpec
- A project structure with
Even if this is new to you, you can figure it out by looking at the commit history for the repository.
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
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'
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.
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
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
. 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.
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
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
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!
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.