Beginning Outside-In Rails Development with Cucumber and RSpec

Posted on by in Process, Web

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.

features/api/v1/articles.feature

Let’s run this feature to figure out what to do next.

Fantasy Coding

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.

features/step_definitions/api/v1/articles_steps.rb

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.

spec/factories/articles.rb

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.

config/routes.rb

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.

app/controllers/api/v1/articles_controller.rb

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).

app/views/api/v1/articles/index.json.jbuilder

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.

app/controllers/api/v1/articles_controller.rb

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.

spec/models/article_spec.rb

RSpec will now be our guide.

app/models/article.rb

app/models/article.rb

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.


Feedback

  Comments: 28


  1. … but I usually feel all the lower level specs, except model specs, aren’t worth it.

    Why only model specs? Why not unit test controller specs?


    • I write model specs because I like the shorter test cycle. Having to run an integration test just to test drive a method in a model is overkill. However, using an integration test to test drive a view or controller is a short enough test cycle for me, so I usually skip directly unit testing controllers and views.


  2. What part of your test required the coffeescript files created by the generator? By using generators, you are not letting the tests guide you as they should. Additional cruft is unnecessarily added to the project.


    • That’s a good point. The Rails generators do generate extra unnecessary files, however I feel they’re a good tool for guiding beginners.


  3. Nice example, thx for sharing


  4. Hi, I wanted to read this article using Instapaper on my Kindle but the Gists are missing. Any chance of providing the code within elements so that I can read this and other Carbon Five posts through Instapaper?

    • Jared Carroll


      Andy,

      I installed a GitHub Gist WordPress plugin that injects the generated HTML into the post. How does it look now?

  5. Gil Shahrabany


    Hi
    I need to write a scenario that test an app-this app use mogli/facebooker2 plugin
    And i need to integrate the cucumber (or something else ) with the mogli/facebooker2 ….
    Do you know any idea or direction how can i do that?
    Do you know any example about scenario with ability to login to a facebook page ?
    I suggested to use mock library … do you know something or direction about that ?
    I prefer still working with cucumber but i don’t know how to integrate it with facebooker2 ….
    I will thanks to hear if you have some light about that issue
    thank,Gil


  6. Well that’s the missing chapter from every programming book ever made!

  7. Leonid Dinershtein


    You have used features/api/v1/… paths for features and features/step_definitions/api/v1/… for step definitions, but there is big chance that you would like to have “Given some published articles” and many others steps in v2 API, but steps already described in step_definistions/api/v1/.

    What do you thing about this, does this name conventions works in real apps?


    • If version 2 of the API isn’t backward compatible, then a separate directory for both v1 and v2 step definitions would be a good way to keep their differences separate. However, if the API doesn’t change significantly, then refactoring to a more generic directory structure is probably a good solution.

  8. Gonzalo Arreche


    Excellent guide, I will try this. Thank you!.

  9. Manimaran Malaichamy


    A useful tutorial.. Thanks

Your feedback