How We Made Writing Tests Fun and Easy

Maciej Głowacki
Daftcode Blog
Published in
12 min readAug 29, 2017

--

Illustration by Sonia Budner

Testing, right? You may love it, you may hate it, but you should agree that good tests can be beneficial to you, your team, and people who might work on your project in the future. Testing might not be the most exciting part of your work, but it’s definitely quite useful. Well tested code brings peace to your mind when refactoring and creating new features.

Or does it? What if those tests weren’t written by you? Are you sure they cover all cases you think they do? Do they actually test anything or just mock pretty much the whole application? Ouch. Now apart from what you were supposed to do, you also have to spend time making sure that existing tests are useful and well written.

That’s how it usually goes if there are no rules for testing within a project.

Let’s create some rules

You may now be tempted to come up with rules regarding testing. Yeah, let’s do this! You say:

From now on, let’s write good tests and aim for 100% code coverage

You then pass that idea to the rest of your team and wait for it to work.

But will it work? Well, you’ll probably end up with a bunch of “tests” that make a few requests here and there, resulting in high coverage without doing much. What about the “good tests” part? Who knows what it even means. I bet you’re not satisfied with such an outcome. Let’s change something about it!

But do you actually know what’s wrong with this approach? To begin with, it does nothing to make development faster or easier. It actually does quite the opposite — forcing developers to write at least twice the amount of code, without even giving them a hope for some reward. If you ask others to write tests they will probably do it, but should you expect them to put heart and soul into it?

Developers want carrots, not sticks

Since the stick is not a good way to enforce writing tests, let’s try with a carrot. What if writing tests gave an instant reward? How about having an API documentation generated for you without any extra work? It’s great if you ask me, and this specific “reward” was exactly what we needed to start writing better tests.

(Don’t get me wrong here. Good tests are great by themselves, and they’ll always pay off in a longer term. However, some instant gratification can be a real productivity booster, especially when doing tasks which are a bit mundane.)

At this point, you have to make a choice.

You can either keep on reading to discover how much fun testing can become (which I recommend you do), or you can jump straight into an example application (but you may miss out on stories which are about to unfold).

So you’ve chosen to stay here? Very good! Then, if you like the idea of killing two birds with one stone let’s see how can we make writing acceptance tests easier. Since we’re using rspec we can make things easier by using the rspec_api_documentation gem. Add it to your project according to the instructions so you can create the very first test:

Try to understand what’s going on here before continuing

Woah, just looking at this test lets you get a grasp of what this application can do. We can instantly see what are the parameters, what’s the response and what headers should be sent. But it gets even better after you run rake docs:generate and wait for tests to finish because you get this as a result:

This looks like something we can just show to people who will use our API. All the styling is already done for us. There are two documentation browsers available out of the box for rspec_api_documentation: apitome and raddocs

That was fast and easy, wasn’t it? Now if we want to add any more examples to this documentation, we have to actually write tests for it. This gives us a push to instantly cover all 3 of the following cases:

  • valid request
  • invalid parameters
  • missing parameters

And now we’re talking! Our tests are beginning to get useful. Have we accidentally broken something in an endpoint for creating new posts? Worry not — there is a test for this case and we will know about it. Maybe we’ve disabled some validation that seemed unnecessary, but actually it wasn’t? Since we wanted documentation for invalid parameters, there’s a test for this as well.

We may have just solved one issue with testing. It’s now more fun than before and yields something that is instantly visible. But our tests are neither easier to write nor prettier yet.

The ugly side of testing

So we have this API that allows creating posts. Wouldn’t it be great if users could choose to publish those posts on, say, Twitter or Facebook? Aww yeah, sounds awesome! But we don’t want to hit 3rd party API every time we run tests, do we? At the same time, it would be great to check whether or not there will be a request made there.

Sounds like something webmock can do. We add it to the Gemfile and install it according to instructions. From now on we can’t talk to the internets from within our tests. We have to explicitly tell webmock that we’ll be making a specific request and remember to set an expectation with rspec:

To keep it simple let’s only implement Twitter now

This doesn’t look too bad. But we’re just looking at one test written by one person. If we ask 10 people to write the same test we may end up with 10 different solutions. What if someone wants to get over stubbing quickly and doesn’t even check parameters that they send? What if somebody else forgets to actually check if they’ve made a request? Something can get broken later and nobody will know about it until it’s too late.

It seems that we’re back to square one — we have to make sure that other people’s tests behave the way we expect them to. But how can we make sure all people will write tests in the same way?

Make it easier

The problem is, writing bad tests is way easier than writing good ones. Why would people care about stubbing requests properly and setting good expectations, when they can just “make it green” with much lower effort? After all, they will have a passing test and a generated documentation.

We must somehow outsmart their inner lazy-developer and make writing good tests easier than writing bad ones. What if we could give them a way to write good tests without them actually feeling like they write tests? What if those tests just wrote themselves? Sound like some sort of dark magic? Well, maybe. But it doesn’t matter it’s impossible.

The idea here is to create some sort of internal DSL (Domain-specific language) to describe our test cases. We don’t want it to be too fancy — just a simple way to extract common test logic. We also want it to look familiar for people who already know rspec, so we’ll build it around existing syntax.

Extracting common logic sounds like a task for shared examples. Let’s create our shared_examples_for_api_request and initialize it with everything that describes our endpoint:

  • Name
  • Explanation
  • Headers
  • Request example

It will look like this:

We can pass example name and explanation as parameters, while headers are always the same

To use this we will just call:

This doesn’t test anything for now (our shared example is empty), but we’ll get there shortly

Now we can start working on the most interesting part. Our own DSL.

Those are hands, and we’re about to get them dirty. Do you know how hard it is to find pictures for technical articles? 🙉 (ph. universe.com)

Getting your hands dirty

Our goal here is to create an object that will be used to automatically setup stubs and expectations for our tests. We should begin with a new class then:

Not much happening here yet

To make things easy and readable we use it as rspec subject like this:

Just basic rspec things

We now have it available inside our shared example, but it’s not really useful yet. The first thing we’d like to check is whether or not the request was successful. What do you think about specifying it by calling .success or .failure on our ApiRequest?

We can instantly tell what is our goal here

Those are simply ApiRequest methods, which can change its inner state to specify expected response code. They should return the object we’re working on, so we can chain more stuff later on:

Since we’ve made 200 a default success code, we should probably update our example request…

Now we can add our first expect to the shared example:

Do you remember that we’ve set an ApiRequest object as a subject?

Woohoo! It’s starting to get useful. But just checking the status code is not enough. We’d also like to check the response. And maybe we’ll also change this .new into something more English-like?

We can check both key value pairs and just keys if we just care about their existence, not value

Much nicer. Now, it’s the time for the implementation. Using .test to initialize object is as easy as delegating it to .new, but when implementing .response we have to remember that we want it to accept both keywords and key-value pairs. We have to store them separately, as they will be tested in different ways:

That’s some basic parameter-type sorcery ⭐️🌟

Finally we can write expects:

For keys we only check their existence, but for key-values we check their value as well. Makes sense, doesn’t it?

We’ve already got some solid base for testing our requests. But it’s often necessary to check some object state after the request. This can, however, differ from test to test, so we can’t describe it as a part of our DSL. But we can make it possible to pass some custom tests like this:

We’ll check if we’ve actually created that Post

We’re passing a block here that will be evaluated in an appropriate context. You can probably guess how the implementation looks:

And in the shared example we can call it like this:

We pass response body to the block, in case we need to do some extra testing with it

Our DSL is already starting to look pretty nice, and we haven’t even implemented its killer feature. Get ready for requests stubbing. Hold tight as it’s going to get a bit more difficult.

Let’s roll the big guns

Before we dive into stubbing API calls, there is one more thing that we should look at. Let’s assume that apart from posting to Twitter and Facebook, our application also sends an email (I don’t know whom to — probably to CIA). This sounds like something we should handle in acceptance tests as well.

Let’s say we want to check if we send notification email after the new post is created. I suggest we do it like this:

I’ve chosen to do it like this, but other way might be for example .emails(‘notify@cia.gov’).with(…) — it’s all up to you

Now if we think about it, then .to and .with probably don’t belong to ApiRequest itself. It looks like .email should create an object of some other class, which is bound to the request we’re working on. We can see it as a way to tell ApiRequest that we’re be describing its Mail now. Sounds reasonable? Let’s code this:

This creates new ApiRequest::Mail object and returns it to us. You’ll see why we need to pass self there in a minute
“to” and “from” methods are here to make it easier to use, but as you can see “with” can do the same thing

Before we move on we still have to figure out how to get out of Mail and return back to the ApiRequest we’ve been working on. We can assume that if Mail object receives something other than .to, .from or .with, then we want it to be handled by ApiRequest, not the Mail. Equipped with such knowledge we can do this:

That’s why we’ve passed self when creating this Mail object

Now we can safely expand our shared example with email related expects:

On line 11 we try to find an email matching our criteria, on lite 14 we make sure we’ve found one

We’re doing a little hack here by exploiting the fact that most of mail fields are either an array or a string, and both have include? method.

And now it’s time for the most difficult part. We want to create stubs and setup expects on them using syntax like this one:

You look at this, and instantly know what’s going on. This is once more just one way of doing this, and you can define this syntax according to your preferences. How about .requests(:twitter).with(…)?

This time we will make it a bit different than with Mail and won’t use method_missing to decide when we’re done stubbing. What we’ll do instead, is have two methods: .success and .failure (not shown in the example above) that will be responsible for choosing an appropriate response and going back to ApiRequest we were working on. This way we can use method_missing to read parameters passed using .with. But we’ll get there one step at a time. Let’s start with adding required methods and classes. We already know how to do this for the .request method:

This looks exactly like what we did with Mail

We also need to create ApiRequest::Stub:

Once more, original ApiRequest will be useful later

Now, inside ApiRequest::Stub we can add request specific methods such as .facebook, .twitter etc.

It’s no mistake that @stubber doesn’t have a reader. You’ll soon see why

To keep everything nice and clean those methods should return some kind of specialised class. All those classes are sure to have something in common (trust me, I’ve been through this 😅), so we will also create a superclass for them.

We’re passing self to those classes because we need a way to go back there later. The next method in stub creation is .with. We can pass parameters there and they will be used inside our stub. Keep in mind, that right now we’ll be working on stubber object, so we add this method to our AbstractStubber like this:

method_missing is here to serve us as a parameters reader

Take a look at this code. We’ve added .with method as a way to store parameters for later, and we’ll be using method_missing to retrieve them later. Why? We’re about to find out now, as we implement handling of specific request types. To properly stub a request we need to have its URL, request body and something to return later:

Take a look at line 6 to see how we make use of method_missing

Do you see it now? If we want to have parametrized requests, we can just call a method named like a parameter and our method_missing will handle the rest of it. This way we make adding new stubs much easier — we just have to specify its address and write some response. This also makes sure that people stub requests consciously, as those parameters are required (take a look at the method_missing implementation — if the parameter is not found, we will raise an error).

If you ever need to stub a request to a different Twitter API endpoint you’d only have to define one new method here and use it in tests. It’s actually that easy.

And now that we’re done setting up our stub, there is one last method call we make. We can make this request be a .success or a .failure. Those two methods will be responsible for preparing final Stub that we’ll use in our tests. They will also handle going back to our original request object.

Oops, looks like we’re missing finish_stubbing method on ApiRequest::Stub

We’ve added all stuff required for stubbing with webmock. We still have to implement .finish_stubbing method on ApiRequest::Stub:

@data holds webmock stub object that we’ll need in tests

Now we’re ready to use those stubs in our shared example:

Remember to stub before doing request

And we’re pretty much done here. There are just a few small things we can add to make our ApiRequest even more fun.

Finishing touches

The gem we use for generating documentation allows you to make a few requests within the single example, but our implementation is limited to just one. To get around this we can accept an array of ApiRequests instead of just one instance. To keep compatibility with existing code, we will wrap our subject in array (it won’t do anything if it’s already an array):

We must remember to manually clear ActionMailer deliveries and remove stubbed requests each time, because they are only cleaned automatically between examples, not within one

Ok, so we can execute many requests in one example, but it’s not very usable yet. We have to add a way to easily override some parameters. Let’s add one last method to our ApiRequest class:

Now when making a request we pass those parameters and they will neatly override default ones:

With this in place, we can now do multiple requests in each example:

Let’s add a second example, this time showing a failure

And the best thing is that we instantly have them in our documentation (watch out, long image ahead 🙈)

002 is our validation message for body text shorter than 10 letters

And we’re finally done. We’ve just created an easy to use DSL which allows us to change this long and error-prone test:

We have to manually write web stubs, check email, parameters and responses

Into a hopefully more readable and easier to use form:

All the stubbing and most expects are made for us — we just write our custom tests inside “and” block

It’s now faster to use ApiRequest than to write tests manually, so we can easily convince the rest of our team to use it for acceptance testing. As a result, we get tests that can be trusted, and a documentation for our API. Goal achieved!

Final words

With the help of ApiRequest class, we can write tests for new endpoints in a matter of minutes. It’s easy to specify both business requirements and the rough idea of a feature using this method, so further development gets easier as well. But keep in mind that those are only acceptance tests. You should still unit test your code to catch any implementation errors.

To show you how to use this method in a real application, I’ve prepared a GitHub repository with a complete (but very basic) use case. Go ahead, clone it and try it out yourself:

https://github.com/glowacki-dev/testifier

That’s as far as this article goes. If you’re still here I guess that you liked this idea of making tests easy to write while keeping a high level of their usability. There is still a lot of things that can be done here. For example, we could partially generate endpoint descriptions based on stubbing we make. Or we could think of a way to extract some common logic to a gem to share with the World (you have my blessing if you want to do this 🙌).

If you have any questions or remarks regarding this approach (or anything else) feel free to ask them in the comments below.

If you enjoyed this post, please hit the clap button below 👏👏👏

You can also follow us on Facebook, Twitter and LinkedIn.

--

--

Freelance Ruby Magician. I do full-stack, photography, learn Chinese and stuff.