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
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,
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:
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
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
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!
In the next part we’ll look at emitting signals in order to simulate Qt events.