Twitter GitHub Facebook Instagram dirv.me

Daniel Irvine on building software

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

17 June 2014

This is the fourth part in 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 part 3, we looked at using RSpec stubs effectively. In this part we’ll look at the emit method and how to simulate Qt events.

Timed events

Barbara, our user, has a new requirement: every five seconds, the count should decrease by 1, but never go lower than 0.

The Qt::Timer class can be used for this purpose. It emits a :timeout signal at a specific time interval. We can attach a slot to this and use it to decrease the count.

The timer itself is started with a call to Qt::Timer#start(interval), with interval being a millisecond value--in this case we’ll want it to be 5000. As with the Qt::Application#exec and Qt::Window::show, we do not want to call this method in our tests. We never want to rely on specific time intervals in our tests. Instead we want to simulate the time interval firing. Let’s write a test.

it 'has a timer that runs every 5s' do
  expect(window.timer).to receive(:start).with(5000)
  window.start_timer
end

I’ve chosen to expose the timer via a property, so you’ll need to add the following line at the top of ClickCounter.

attr_reader :timer

(Another possible way of doing this is to create a subtype of Qt::Timer and pass it in to the constructor of ClickCounter. That’s a valid approach, but in the interest of brevity I won’t go into it here.)

Then we’ll need to instantiate the backing field. Let’s add this line to the ClickCounter#initialize method.

@timer = Qt::Timer.new

Finally we’ll need a new method:

def start_timer
end

You might ask why we need this method at all--why not start the timer in the initializer. There’s a couple of reasons. The first is the most obvious one--we can’t stub out the timer method until the initializer has run and returned us a ClickCounter object with a timer property. Once we have that property, we can stub out a method on the object it references. That means we have to split out the instantiation of the timer from the start.

The second reason is that logically we don’t want to start the timer until the window is shown. So there’s another requirement here--that the timer is started after the window is shown. That’s another test that we should write.

For now, let’s run our test and watch it fail:

Failures:

  1) ClickCounter has a timer that runs every 5s
     Failure/Error: expect(window.timer).to receive(:start).with(5)
       (#<Qt::Timer:0x007fa85b0cd130>).start(5000)
           expected: 1 time with arguments: (5000)
           received: 0 times
     # ./spec/click_counter_spec.rb:43:in `block (2 levels) in <top (required)>'

Let’s make it pass. Add the following line to start_timer:

@timer.start(5000)

Running the tests:

.......
Finished in 0.44081 seconds (files took 0.21262 seconds to load)
7 examples, 0 failures
Coverage report generated for RSpec to /Work/Test/coverage. 0.0 / 0.0 LOC (100.0%) covered.

Excellent--all passing, and still at 100%. Now let’s test that the timer actually does something useful. Add the following test:

it 'decreases the count when the timeout fires' do
  window.click
  expect(window.count).to eq 1
  emit window.timer.timeout()
  expect(window.count).to eq 0
end

Note the call to emit. The syntax is a little odd, but this essentially raises the :timeout signal on the timer.

For this test to run we’ll need to add the following line at the top of ClickCounter.

attr_reader :count

There’s an interesting question here: is our test doing too much? We’re performing two actions, not just one. The first action is a setup action and involves clicking on the window in order to increase the count. This is necessary because clicking is the only way we have to increase the count. But what would happen if the click_button method broke? We would end up with two failing tests: the one that tested click_button and this one, which theoretically only tests the timeout signal. So our tests are not independent. As ever, there are tradeoffs to be made and I’m going to leave fixing this as an exercise for the reader!

Let’s make this test pass. Add in the following line to the top of ClickCounter:

slots :decrease_count

In the initializer, after the construction of the timer, add:

connect(@timer, SIGNAL(:timeout), self, :decrease_count)

Finally add this method:

def decrease_count
  @count -= 1
  @count_label.text = @count.to_s
end

Now let’s rerun the test:

........

Finished in 0.43961 seconds (files took 0.21746 seconds to load)
8 examples, 0 failures
Coverage report generated for RSpec to /Work/Test/coverage. 0.0 / 0.0 LOC (100.0%) covered.

Simply wonderful. All passing, all covered.

So we’ve gone through red-green a couple of times and I think this is potentially a good time to refactor. I’m going to leave that as an exercise for the reader as it’s straightforward and won’t add much value to have it here.

What’s left?

To complete this exercise, there’s one more test to write, and that’s for ClickCounterApp#run: it should start the timer after the window is shown. Again, I’m leaving this as an exercise for the reader.

You can do this using the ordered RSpec syntax. Here’s a quick example of what it might look like in your test:

expect(window).to receive(:show).ordered
expect(window).to receive(:start_timer).ordered

In the final part of this series I’ll briefly summarize the various techniques I’ve used.

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