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.
Barbara, our user, has a new requirement: every five seconds, the count should decrease by 1, but never go lower than 0.
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::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
(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
@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
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
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
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.
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.