The RSpec Book defines outside-in Rails development as starting with views and working your way in toward the models. By developing from the outside in, you are always taking a client perspective at each layer of the application. The end result is an absolute minimum implementation, consisting of simple, expressive interfaces.
Outside-in development doesn’t require any specific tool or language. This article will demonstrate it in Rails, using two popular testing tools: Cucumber and RSpec.
Start with a High-level Specification
Starting with a high-level specification requires you to have a clear understanding about what you want to achieve. If it’s still unclear, now is the time to have a conversation with the client. After establishing a clear goal, we can use Cucumber to turn a plaintext story into executable code.
Our sample story will be from a news site. The feature is a JSON API endpoint for news articles.
This story can be copied directly into a Cucumber feature.
Let’s run this feature to figure out what to do next.
Cucumber successfully parsed our feature but it needs definitions for all of our steps. Let’s implement these steps writing code that we wish already existed.
In our two Given steps, we establish a context consisting of published and unpublished articles. These two factories don’t exist yet; we just wrote the code we wish we had. This is a major benefit of developing from the outside in. By writing code that doesn’t even exist, you’ll end up creating ideal objects and interfaces.
In our When step, we exercise our application by first setting a proper HTTP header and then making an HTTP GET request to a non-existent URL. Again, this URL doesn’t exist, it’s just where we would expect the articles to be.
Finally in our Then step, we verify the response. Here we begin to discover our domain model; imagining an Article class with a #published? instance method.
Let Cucumber Guide the Way
With our steps defined, a rerun of Cucumber fails in our Given step because our factories don’t exist yet.
Let’s define these two factories.
Cucumber guides us toward our next step.
Our factories are being automatically mapped to a non-existent Article class. We can use the Rails model generator to create this class. We’ll also specify the title and published attributes we referenced in our Then step.
With our setup passing, Cucumber now fails at our API request.
Drill Down to the Controller
We need to add a route for our API endpoint.
With the routes in place, Cucumber tells us we’re missing a constant. This particular error message is from the Rails #namespace method we used in our routes file. Namespacing our controller will fix this issue. We can use the Rails controller generator to create a namespaced controller class.
Cucumber fails again, this time looking for an #index action in our controller.
At this point, some outside-in practitioners will also drop down a level with respect to testing and use RSpec to spec out the controller. Since we’re new to outside-in development, I’m going to skip this step and go straight to the implementation. We’ll look at the value of controller specs and when it makes sense to write them later on.
Now we fail because our #index action needs a template. Let’s create an empty template just so we can finally get a non-infrastructure related failure, i.e., a logic error, from Cucumber.
Drill Down to the View
With the routing and request handling boilerplate out of the way, we finally get a “legitimate” failure from Cucumber. At this point, some outside-in practitioners will drop down a level with respect to testing and use RSpec to spec out the view. Like controller specs, I’m going to skip this step for simplicity. We’ll discuss when a view spec makes sense later on. For now, let’s update our blank view to actually render some JSON (we’ll use the jbuilder Gem to construct the JSON).
Again, we write this view imagining an “articles” instance variable; ideally, a collection containing our articles. This helps us avoid setting up unnecessary state in our action.
Cucumber fails again, this time with a long stacktrace (abbreviated below) originating in jbuilder.
This failure is because the instance variable doesn’t exist yet. Let’s update our Api::V1::ArticlesController#index action to find all published articles.
We’ve kept our controller thin and decided to not directly test it. By keeping controller logic to a minimum, skipping controller tests isn’t a significant risk.
Cucumber now guides us to our domain model.
Drill Down to the Model
Despite skipping controller and view specs, I do feel it’s beneficial to drill down a layer in our tests and directly test the model. Model tests will shorten our testing feedback loop and allow us to specify at a level closer to the code. Skipping model tests and relying on Cucumber, keeps the feedback loop too large, slowing you down.
RSpec will now be our guide.
With Article.published specified, we can return to Cucumber.
Jump Back Up to Cucumber
Cucumber is now passing and our story is complete.
The complete code for this example can be found on github.
Change Your Perspective
I use the above approach on every feature I write. At each layer, I find myself getting lazier and lazier, delaying the hard work until the very end. At that point, I’m in the domain model, the heart of the application, and where the majority of logic should be. By taking a client perspective at each layer, the resulting objects remain simple, have expressive interfaces, and logic naturally finds its home.
You can begin developing from the outside in at every one of your application’s interfaces. The example above demonstrated a JSON API. Traditional HTML interfaces can easily be tested using capybara. And if you’re developing a command line interface, perhaps for a Ruby gem, take a look at aruba.
Do I Need to Test at Every Layer?
The RSpec book suggests writing tests at each layer, i.e., view specs, controller specs, helper specs, and model specs. I’ve tried this approach several times but I usually feel all the lower level specs, except model specs, aren’t worth it. They do shorten the testing feedback loop, but their reliance on stubbing and mocking to achieve true isolation makes refactoring and maintenance difficult. I also keep the logic to such a minimum in these objects, e.g., controllers, that the additional fine-grained unit tests don’t provide that much benefit.
There is nothing wrong with not unit testing each part of your application. Don’t dogmatically insist that everything be unit tested. Oftentimes a higher-level integration test will sufficiently exercise (albeit indirectly) a particular piece of code. The tradeoff here is that your testing feedback loop will be large. You’ll need to execute a full-stack Cucumber test just to see if a change you made, perhaps in a controller or a view, passes your failing test. Occasionally, I’ll use a lower level view or controller test to handle an edge case but this is a pretty rare occurrence. This is just my personal style. I would recommend trying out testing at every level, especially view and controller tests, to see how it feels and if it’s beneficial for you and your team.
Give It A Try
Outside-in development often feels strange to newcomers. Most developers prefer to start with the “important” part of an application, i.e., the domain model, and work their way outwards. Thinking like a server and not a client can lead to overengineering by implementing more than you need. Your resulting objects and their interfaces will also be less than optimal, or at least take longer to get quite right.
Like most things in Rails, the tools for developing outside-in are easy to setup and configure. If Cucumber or RSpec aren’t your thing, adapt your favorite tool and give outside-in development a try.