How We Made Writing Tests Fun and Easy
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:
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:
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:
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:
To use this we will just call:
Now we can start working on the most interesting part. Our own DSL.
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:
To make things easy and readable we use it as rspec subject like this:
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
?
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:
Now we can add our first expect to the shared example:
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?
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:
Finally we can write expects:
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’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:
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:
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:
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:
Now we can safely expand our shared example with email related expects:
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:
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:
We also need to create ApiRequest::Stub
:
Now, inside ApiRequest::Stub
we can add request specific methods such as .facebook
, .twitter
etc.
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:
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:
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.
We’ve added all stuff required for stubbing with webmock. We still have to implement .finish_stubbing
method on ApiRequest::Stub
:
Now we’re ready to use those stubs in our shared example:
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):
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:
And the best thing is that we instantly have them in our documentation (watch out, long image ahead 🙈)
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:
Into a hopefully more readable and easier to use form:
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:
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 👏👏👏