Building up tests via context and let in Ruby

One of my favorite parts about Ruby is writing tests. Here's a pattern I use frequently, which allows you to use context and let to build up state as you progress deeper into the context tree. This is best described with an example:

What's going on here?

First of all, I like to always add a describe method block for each public method of the class, and then inside it, declare a subject variable which calls that method. Then I add a let(:...) for each of the arguments to the method. This allows me to setup some sensible defaults for each argument, and only modify the arguments as needed in each context. First, I test the default arguments, like in the context "with defaults". Then I proceed to test modifications to each argument. For example, the context "with nil player" expects the described method to return nil when a nil player is passed in.

Where things get more interesting is the context "with other existing character". Here, we have a before statement which gets triggered before all of the tests in that context. It sets up state such that there's already another character generated. It expects each sub-context to declare a new variable, existing_name, and generates an object with that name. Then we have two contexts where existing_name either matches or does not match the name being generated by the function call. Under the name matches context, we have another sub-context for when that existing character is under a different player, which is treated differently. You can easily nest many sub-contexts, each with a useful description of the state that they are in. As a result, we can test all the different states of our class or database with fine granularity. That's where I find the sort of pattern most handy: checking for all the different states that are possible by changing each state option one at a time.

Finally, we can easily test each of our validations this way. The simplest example was the nil player near the beginning, which yields a nil return. Another example is the name containing invalid characters context. Here we have a very simple context which defines the expectation that the class will raise an exception when an invalid name is passed in.


Popular posts from this blog

Crossing the Rubycon

The many benefits of working in small increments