We made it! The final post in the testability series. Here we bring the Benchmarker application to a reasonable close. Then we’ll take a bit of time to briefly cover the journey we’ve taken together here.
Our design pressure has led us to this point: delivering the value that we started off articulating in the first post. Specifically, organizations funding an archaeological dig site want to know if the team associated with the dig is going to finish it on schedule or not.
Through these posts we put a lot of tests and code in place. Here we’re going to finish up our dig_site_spec.rb by adding a few more tests to the the “pace” block.
Evolving the Code: Schedule
So what do we need to do here? Well, we only really have two options: the dig is either on schedule or it’s not. Keep in mind the context we have for our “pace” block and the tests within it. This is what we ended up with:
1 2 3 4 5 |
let(:dig) { DigSite.new } let(:finished_recently) { Activity.new(cost: 5, finished: 1.day.ago) } let(:finished_awhile_ago) { Activity.new(cost: 10, finished: 1.month.ago) } let(:small_unfinished) { Activity.new(cost: 2) } let(:large_unfinished) { Activity.new(cost: 25) } |
In the previous post we calculated how many days remaining there were (75.6, as it turns out) given the data here, which is indicating the rate of activities being finished. So let’s add a test to check if we’re on schedule. As in the previous posts, I’ll show the test first and then we’ll look at the full spec file:
1 2 3 4 |
it "provides an indication of not being on time" do dig.ideal_finish_date = 2.weeks.from_now expect(dig).not_to be_on_time end |
Make sure it’s clear why this test is structured as it is. Clearly two weeks from now is well under 75.6 days from now, which was the projected time. So this dig is definitely not on time in terms of schedule.
To get this test to pass, the DigSite (dig_site.rb) needs to be modified a bit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class DigSite attr_accessor :activities, :ideal_finish_date def initialize @activities = [] end def finished? unfinished_activities.empty? end def total_cost activities.sum(&:cost) end def remaining_cost unfinished_activities.sum(&:cost) end def unfinished_activities activities.reject(&:finished?) end def finished_pace activities.sum(&:counts_towards_pace) end def current_pace finished_pace * 1.0 / 14 end def projected_days_remaining remaining_cost / current_pace end def on_time? (Date.today + projected_days_remaining) <= ideal_finish_date end end |
Let’s put in place the other test that we know we need:
1 2 3 4 |
it "provides an indication of being on time" do dig.ideal_finish_date = 3.months.from_now expect(dig).to be_on_time end |
And that passes. And that’s to be expected: our dig is projected to take 75.6 days, which is about 2.3 months. So if the ideal finish date is 3 months from now, then the dig is on time.
So here’s what the full test spec looks like now:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
RSpec.describe DigSite do describe "state" do let(:dig) { DigSite.new } let(:activity) { Activity.new } it "represents a dig with no activities as finished" do expect(dig).to be_finished end it "represents a dig with an unfinished activity as unfinished" do dig.activities << activity expect(dig).not_to be_finished end it "considers a dig as finished if all activities are finished" do dig.activities << activity activity.set_finished expect(dig).to be_finished end end describe "costs" do let(:dig) { DigSite.new } let(:finished) { Activity.new(cost: 10, finished: true) } let(:small_unfinished) { Activity.new(cost: 2) } let(:large_unfinished) { Activity.new(cost: 25) } before(:example) do dig.activities = [finished, small_unfinished, large_unfinished] end it "provides a total cost of all activities" do expect(dig.total_cost).to eq(37) end it "provides a remaining cost based on unfinished activities" do expect(dig.remaining_cost).to eq(27) end end describe "pace" do let(:dig) { DigSite.new } let(:finished_recently) { Activity.new(cost: 5, finished: 1.day.ago) } let(:finished_awhile_ago) { Activity.new(cost: 10, finished: 1.month.ago) } let(:small_unfinished) { Activity.new(cost: 2) } let(:large_unfinished) { Activity.new(cost: 25) } before(:example) do dig.activities = [finished_recently, finished_awhile_ago, small_unfinished, large_unfinished] end it "provides a calculation based on finished activities" do expect(dig.finished_pace).to eq(5) end it "provides a calculation of current rate of finishing activities" do expect(dig.current_pace).to eq(1.0 / 2.8) end it "provides the projected days of work remaining" do expect(dig.projected_days_remaining).to eq(75.6) end it "provides an indication of not being on time" do dig.ideal_finish_date = 2.weeks.from_now expect(dig).not_to be_on_time end it "provides an indication of being on time" do dig.ideal_finish_date = 3.months.from_now expect(dig).to be_on_time end end end |
And that’s it for the code! Note that the final version of the project is available at the Benchmarker repo.
I think that brings us to the end of our code and immediate project, folks! Clearly there is some refactoring that could be done with the production code. And as we’ve gone through these posts, I’ve pointed out areas where our test code may not be ideal. I’ll leave those as thoughts and exercises for you to contemplate on your own.
So let’s close out this series by doing a bit of a summary of all this.
Was This Series Even Relevant to Testers?
I can see someone asking that, feeling that perhaps with so much code in the posts, perhaps this was more of a developer exercise.
I would suggest that testers do need to practice their skills at building solutions, even if only simple ones. By building solutions you start to understand the challenges of design. And then how to put pressure on design, which is what testing does. And you also start to learn how mistakes happen, when they are likely to happen, how they magnify as your code base grows, and so forth.
That’s what I hope this series did.
By going through this process, you also start to learn the sensitivities of various technologies, particularly when various technologies are layered on top of each other. That’s not something I covered here but in the context of Benchmarker, you can imagine this scenario by thinking about adding an API to it, and then a web front-end using perhaps Angular or React. And then having a database to store everything.
Working through actual implementations like we did here — and building your own, even if only as practice — encourages testers to think about how testability should be a primary quality attribute when building complex things. But to see that, and to truly understand it, you have to be able to build something on your own so that you can feel it.
Shifting the Vocabulary of Testing
What we looked at in this series of posts is essentially a very simplified, but accurate, view of “how the product is designed and works.” Along the way, we were able to think about external qualities and internal qualities. Testers need to be concerned with both, just as do developers. This is vocabulary that we need to get more testers speaking about.
Another vocabulary shift is that testers need to keep emphasizing something good developers already know: testing puts pressure on design. It does so at numerous abstraction levels, from the human on down to the deepest levels of the tech stack. It is critical to have a facility to move between those abstraction layers. This series of posts were able to actualize the philosophical point I was making in the shifting polarities of testing.
Another vocabulary shift that I believe needs to be emphasized more is that all of this means testing has to be framed (at minimum) as two types of activity: as a design activity and an execution activity. And those two activities can and should be interleaved.
The Value of Testers
So what we end up with is test design and test execution going on at multiple levels of abstraction, investigating external and internal qualities.
Speaking to my tester readers, you being aware of all that can be eye-opening to many decision-makers. This gets into another part of what testing is: it’s a framing activity. It helps frames discussions and thus helps enable rational decision-making under uncertainty.
Ultimately that’s what testing (as a discipline) and specialist testers (as domain experts in that discipline) are focusing on: how we help people make better decisions sooner. Doing so requires that we are reasoning about a manageable amount of scope. “Manageable,” in part, being defined by how hard it is to have a shared understanding of what quality means (how the product adds value) in the limits of that scope.
Finally, what we end up with here is a focus on a delivery team where — ideally — everyone is considered a type of developer. What matters is that all members of the team work to understand what users need to be successful. Focusing on finding the simplest solutions that provide the right amount of value, consistent with our knowledge of what that value actually is. Everyone on the team is responsible for turning the functionality needed by the users into software that provides value.
You Made It!
I hope this series was informative for you. This was, in many ways, really hard to put together. I’m thankful for the “Gatherer” project from the book Rails 5 Test Prescriptions which helped me consider a contrived example as well as well the book Developer Testing for providing the impetus to show how “developer testing” is extremely relevant to testers, not just (programmatic) developers.
I tried to show a lot in as little space as possible, consistent with recognizing that people’s time is valuable and you have a lot of choices of what to read out there. My hope is that this series is able to appeal to developers and testers — keeping in mind I believe testers should be a type of developer — and that you can use some of the ideas here for discussion on your own teams, either to agree with what I said, disagree with what I said, or find yourself somewhere in the middle and then, hopefully, carrying forward the discussion.
We need more of this in our industry. I want to encourage those who are better than me at presenting all this to do so. Only by doing this, by putting ourselves out there, will we reclaim our discipline from the professional class of test consultants (who often turn testing into a nomenclature problem), the technocrats (who turn testing into a programming problem), and various so-called “QA Managers” (who turn testing into nothing more than a clerical problem).
I hope you enjoyed this series and I appreciate you coming along for the ride.