Twitter GitHub Facebook Instagram dirv.me

Daniel Irvine on building software

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

15 June 2014

This is the third part of a series looking at using TDD to build a desktop GUI app in Ruby using Qt.

In part 1 we created our first two tests and the corresponding implementation. In part 2, we extended our implementation to satisfy the user’s requirements. In this part we’ll get the application running, using RSpec stub functionality to ensure we keep 100% coverage.

Showing the window

At some point we’ll need to instruct Qt to show the window on screen. That’s easily done by calling the method Qt::Widget#show. The question is, how do we capture this requirement in a test?

One crucial difficulty with testing UI code is that you absolutely cannot call any framework method that will cause screen output to occur. Doing this might work on your local machine, but it certainly won’t work on a build box with a non-interactive user account.

You also want to avoid starting the UI event loop, which is often necessary in order to do any real UI work. Starting that via a test would require you to spawn a control thread, and we always avoid spawning threads in tests because it’s a sure-fire way to create intermittently failing tests (not to mention adding seriously complicating your tests).

In Qt’s case the UI event loop is started with a call to Qt::Application#exec, so we want to avoid calling both this and the Qt::Widget#show method.

We can do this by stubbing out these methods using RSpec.

Stubbing out a method

Let’s write a quick test to show how this will work. Enter the following in a new file, spec/click_counter_app_spec.rb.

require 'qt'
require 'click_counter_app'

describe ClickCounterApp do

  it 'starts the Qt event loop' do
    app = ClickCounterApp.new
    expect(app.kind_of?(Qt::Application)).to be true
    expect(app).to receive(:exec)
    app.run
  end
end

I’m defining a new class called ClickCounterApp. This class will have just one method, run, that will perform all the necessary initialization logic for the app and then finally call the Qt::Application#exec method to pass control over to the UI.

(If this were a real app, it may be beneficial to start the event loop and perform initialization on a background thread. You would do this if initialization took more than a second or two to complete.)

The key line is line 8 above. The “expect-to-receive” call tells RSpec to stub out the real implementation of exec and simply record that it was called instead.

So open up lib/click_counter_app.rb and enter the following:

class ClickCounter 
  def run
  end
end

With this in place, we should be able to see a failing test:


Failures:

  1) ClickCounterApp starts the Qt event loop
     Failure/Error: expect(app).to receive(:exec)
       (#<ClickCounterApp:0x007fc76d95df80>).exec(any args)
           expected: 1 time with any arguments
           received: 0 times with any arguments
     # ./spec/click_counter_app_spec.rb:7:in `block (2 levels) in <top (required)>'

Finished in 0.43507 seconds (files took 0.21572 seconds to load)
5 examples, 1 failure

Failed examples:

rspec ./spec/click_counter_app_spec.rb:5 # ClickCounterApp starts the Qt event loop

Let’s make this green. Change the lib/click_counter_app.rb file to read as follows:

require 'qt'

class ClickCounterApp < Qt::Application
 
  def initialize
    super(ARGV)
  end

  def run
    exec
  end
end

As you can see, subtyping from Qt::Application gives us this ability to stub out its exec method, as is done in the test. This technique is very useful in helping us achieve that 100% coverage:

.....
Finished in 0.43846 seconds (files took 0.24089 seconds to load)
5 examples, 0 failures
Coverage report generated for RSpec to /QtBindingsTdd/coverage. 15 / 15 LOC (100.0%) covered.

Great. Now we can work on showing the window. Add in the following line to the top of the ClickCounterApp spec:

require 'click_counter'

We want to create a spec that reads like the following.

it 'shows the click counter window' do
  app = ClickCounterApp.new
  expect(app.window).to receive(:show)
  app.run
end

Unfortunately this won’t work as we still need to stub out the call to Qt::Application#exec. Without that line, the run method will block and our test will never complete. We could write this:

it 'shows the click counter window' do
  app = ClickCounterApp.new
  expect(app).to receive(:exec)
  expect(app.window).to receive(:show)
  app.run
end

This approach would work, but we’re now testing two different things and our first initial test would end up with the same two expectations (since it’ll need changing to accept the window input).

A better approach is to refactor this to use the RSpec allow syntax. Change the entire describe block to read as follows.

describe ClickCounterApp do
  
  let(:app) do
    app = ClickCounterApp.new
    allow(app).to receive(:exec)
    allow(app.window).to receive(:show)
    app
  end

  it 'starts the Qt event loop' do
    expect(app.kind_of(Qt::Application)).to be true
    expect(app).to receive(:exec)
    app.run
  end

  it 'shows the click counter window' do
    expect(app.window) to receive(:show)
    app.run
  end
end

The allow method is used to stub out a method without any expectations--a test may or may not call it. If it isn’t called, the test won’t failed. That’s unlike expect, which causes a test failure when the expected method is never called. Within the specs themselves, we’re using expect to override the more relaxed allow semantics.

Putting it all together

Now all that’s left is to actually create the entrypoint. This is the one part of the app that will not be covered by our tests. This script goes in the bin/ folder, and we keep it as short as possible.

Create a file named bin/click_counter.rb and enter the following code:

require 'click_counter_app'

ClickCounterApp.new.run

That’s it. Run it via a call to ruby bin/click_counter.rb. Our amazing app should be displayed. Click the button a few times!

The amazing Click CounterThe amazing Click Counter

In the next part we’ll look at emitting signals in order to simulate Qt events.

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