What is a discovery test?

I’m a purist. I like to split my apps into the smallest units of work I can, each nicely layered.

The trouble with purists is that it’s easy to get lost in a sea of tiny objects with undecipherable relationships between each other.

I’m a pragmatist. I don’t want too much of that crap; just enough to give me some tangible benefits.

Adding a Command layer into your application gets you both – things are nicely parcelled up into small, easy to understand blocks. But you don’t end up with thousands of tiny little things scattered all over the place (continued below).

Do you know what to do but not how it works?

Ever wanted to understand why Rails views work the way that they do? Why variables from your controllers are visible inside your views?

Sign up below to get a free 5 part email course on how Ruby on Rails view rendering works and gain a deep understanding of the Rails magic.

We will send you the course, plus the occasional update from this web-site. But no spam, we promise, and it's easy to unsubscribe

Let’s take a simple real world example and use Discovery Tests to figure out our implementation.

One of my clients has a system that generates reports based upon assessments people have taken. To download a report for the first time, the system subtracts a credit (known as a meter) which the customers have purchased in advance.

I like to know what I’m building before I dive into the code … so a quick flowchart on the whiteboard is in order.

Generating a report

It’s pretty simple – if we’re still waiting for the PDF to be generated, show a “please wait” message. If not, have they already paid? If not, have they got enough credits (meters)? If not they can’t have the report, if yes then call off a meter (subtract a credit) and then send them the report.

What makes the purists happy is that this is easily broken down into several trivial classes. What makes me happy is that some of those classes could probably be reused elsewhere.

And what we are about to discover is how those classes interact.

We start at the top. With a test

Pretty much all my discovery tests start with this template:

So we load up a test_setup routine (which just gets minitest ready and loads any dependencies, preferably without the Rails environment) and the actual class under test. I use require_relative so I can run the test individually.

We describe what we are testing, then name the subject of our test – in this case a Command::DownloadsReport. It references a “services” object, which we define as an empty Hash, below.

Then we describe three conditions we want to test the command under – when we’re not logged in, when we’re logged in with permission and without permission.

Let’s fill in the easy cases first.

If we invoke the command (by calling download_report_for) and the user is nil, or the user is just some user, then it should raise a Command::Unauthorised exception.

We’re also referencing a couple of other objects here – a candidate and a user. What are these? Actually, we don’t really care at the moment – so we leave them as empty stub objects.

We can run this test and it fails – as we don’t have a DownloadsReport object or a download_report_for method. Let’s add one:

We require our Unauthorised exception (this is pure Ruby so we can’t rely on Rails autoloading all our classes for you – this is a good thing as it makes you aware of what your dependencies are). And then we define our class – I’ve made it a Struct with a single field, services, to hold that mysterious Hash we mentioned earlier. Finally we add in a basic implementation of the download_report_for method – if the user is nil or the user can’t “download_report” then raise the exception.

Now we find our test passes, as if by magic. Even though there’s this “can?” method on our user that we haven’t done anything with.

That’s because we used “stub_everything” to define our user – if it receives a message it can’t understand, it returns nil, which in turn triggers our exception.

So suddenly, we’ve hit a case where we do sort of care what kind of object a User is. At the very least, we know that it requires a “can?” method that returns true (or something similar) when given :download_report.

This is our first discovery.

So let’s prepare for the “when I’m logged in” section of the test by adding that in, and we’ll also add in the first decision that needs to be made.

How do we decide if the PDF has been generated? That sounds like a bit of data. In fact, it’s the exact sort of thing the candidate object would know.

So now we’ve discovered something about our candidate as well – she needs a “pdf_has_been_generated?” method (which may just be a field in the database, or may be something that looks at a file-path or some other type of field and figures it out – the point is our DownloadsReport command doesn’t actually care).

And then we’ve got two more test clauses to fill out. Firstly, we need to say that the user should wait if the PDF hasn’t been generated. We could use a return value, such as :please_wait to do this. But look again at the name of our command – DownloadsReport. If it can’t do its duty, it can’t do its duty. I think this is a perfect candidate for an exception.

And we make that pass, by defining this new exception and raising it if necessary.

And finally, we need to call off a meter.

But that involves its own complexities, involving a whole series of decisions and actions.

As our command wants to live up to its name, it’s going to tell someone else to do the work.

Which is where our mysterious “services” Hash comes in.

You see, DownloadsReport is going to tell something else to call off a meter. But we don’t really care what that is at the moment, we only care that DownloadsReport orders it to do the right thing. So, once more, we use a stub, and then make it discoverable for our command.

We add a new stub into this services Hash, then we expect DownloadsReport to call a “call_off_meter_for” method, giving it a candidate.

Again, our test fails, so let’s make it pass.

And there we have it.

We’ve been given a complex task, broken it into several decisions and plotted a path to various outcomes based upon those decisions.

We’ve defined a Command object that takes account of the first of those decisions.

Our tests prove that it is secure – the user must be logged in and the user must have adequate permissions to download the report.

We’ve discovered that our User model needs to be able to report back whether it has a particular permission (can?) and we also discovered that our Candidate model needs to be able to report back whether the PDF has been generated. Both of which will have an impact on our database schema.

To handle this first stage, we’ve got three lines of active code, two of which are dealing with exceptional cases.

And we know that the next stage can be handed off to another command. Which will probably be just as simple. We don’t know or care what that is at the moment, so we’ve made locating this other command another process of discovery.

And most importantly, we can see, at a glance, that for a DownloadsReport command to work, it needs to know about something it can tell to call off a meter (from the services hash), a user-type object that can answer whether it has a permission and a candidate-type object that can answer whether a PDF has been generated. And none of those dependencies are hard-coded to particular classes, so we can unplug them and plug replacements in at a moment’s notice – flexibility in a nutshell.

So the process of writing your app is describing each feature as a full-stack test, then discovering how to implement that feature as a series of commands.

Easy as pie. Except for those days when it all goes wrong.

If you’d like weekly updates and a free Ruby on Rails security checklist, sign up below.

Do you know what to do but not how it works?

Ever wanted to understand why Rails views work the way that they do? Why variables from your controllers are visible inside your views?

Sign up below to get a free 5 part email course on how Ruby on Rails view rendering works and gain a deep understanding of the Rails magic.

We will send you the course, plus the occasional update from this web-site. But no spam, we promise, and it's easy to unsubscribe

1 Comment What is a discovery test?

  1. Pingback: TDD is bullshit | The Art and Science of Ruby

Comments are closed.