/rails-api-bdd

Primary LanguageRubyOtherNOASSERTION

General Assembly Logo

Behavior-Driven Development of Rails APIs

We'll answer the following questions in this talk.

  • Why should you care about testing?
  • How do you know what to test?
  • What is the purpose of a feature test?
  • What is the purpose of a unit test?

Tests limit what we have to debug. For instance, if we have all green, passing tests, we know that when we deploy to Heroku our code isn't the problem, rather an issue with Heroku. Passing tests limits the types of debugging you have to do.

Prerequisites

Objectives

By the end of this lesson, students should be able to:

  • Develop a Rails API using outside-in, behavior-driven testing.
  • Make user stories to drive wireframes.
  • Drive behavior specification with user stories.
  • Write automated CRUD request specs with RSpec.
  • Drive routing, model, and controller specs using request specs.

Preparation

  1. Fork and clone this repository.
  2. Install dependencies with bundle install.
  3. Run rake db:create and rake db:migrate.

User Story Exercise

Before diving into testing, let's revisit wireframes and user stories. We've used both of these for our first projects.

Why were they important? What do our user stories do for the scope of our project? How are they useful when wireframing our webapp's layout and UX? How do good user stories and wireframes help with app development?

Lab: Create User Stories and Wireframes

Say we're building a food-rating app. To make sure our app is useable and our time spent building it is efficient, we should take some time to:

  • write out user stories so we know exaclty what our app should be able to do
  • sketch wireframes so we have a clearer understanding of the main components of our app and clearer direction on how to build out each

In your squads, list out as many user stories as you can for a food-rating app and then use those stories to drive your wireframing.

Outside-In Testing

Test Cycle

Feature Tests

Feature tests are for catching regressions/bugs. Features break less because they're higher level. Features test user experience. Feature tests document workflow within the app. Feature tests tell you what's missing, and drive each step of the development process.

Unit Tests

Unit tests drive implementation and break more often, but they're smaller in scale and faster to execute. Unit tests test developer experience. Unit tests don't break down the problem into smaller pieces, they give you the confidence that the smallest pieces work as expected. Unit tests document your code base.

Both of these tests provide documentation of your code. Writing tests makes refactoring easy because we can change one thing and see how it affects the entire system. After each change in the code we run our unit tests to confirm our expectations.

Developer Workflow

Use feature tests to drive your unit tests, and your unit tests to drive your code. You'll want to save and commit your work often during development. We suggest commiting:

  • After passing unit tests.
  • After passing a feature.
  • After refactoring and passing all tests.

You can push when you're done passing a feature. You should always run your tests before you commit/push your work.

We'll start with a request spec. Request specs perform a similar job to curl: they emulate testing your API from the client's point-of-view.

Failing request specs will drive creating routing specs. Routing specs will drive creating controller specs. Finally, controller specs will drive creating model specs. Once we have all these smaller tests (units) passing, the feature spec (request spec) should pass automatically!

Let the tests tell you what to do next, and you'll never have to think about your next task. It helps us get in "the zone"!

GET All Articles

Demo: GET /articles Request Spec

To check our specs, we run rspec spec from the command line. What output to we get?

We see the following output: uninitialized constant ArticlesController (NameError)

What does that tell us? That we need a controller... rails g controller Articles

Now what does rspec spec tell us? uninitialized constant Article (NameError)

What does that tell us? That we need a model... rails g model Article

Now what? Migrations are pending. To resolve this issue, run: bin/rake db:migrate RAILS_ENV=test Do just that: bundle exec rake db:migrate

Now we finally get to our test, but it's still throwing an error...

ActiveRecord::UnknownAttributeError:
    unknown attribute 'title' for Article.
  # ./spec/requests/articles_spec.rb:20:in `block (2 levels) in <top (required)>'
  # ------------------
  # --- Caused by: ---
  # NoMethodError:
  #   undefined method `title=' for #<Article id: nil, created_at: nil, updated_at: nil>

We see a few things here unknown attribute 'title' for Article and undefined method 'title=' for #<Article...

What could this mean? Maybe that we need getters and setters for Articles? But we havent written those in awhile. Don't we just have our models inherit from ActiveRecord, and AR does the work for us? The answer is yes, and ActiveRecord writes our setters and getters to allow us to access and modify the database--but this doesn't solve our problem, does it?

The issue actually stems from our migration and schema. ActiveRecord never knew to add setters and getters for 'title' because it doesn't exist in the schema and therefore our database.

So, let's modify our migration--BUT FIRST! What do we have to do!?

rake db:rollback RAILS_ENV=test

Add the following line to your migration:

  t.string :title

Now run migrate and try running rspec spec again.

Repeat for content

Now when we run rspec spec we should see something like this:

Failures:
  1) Articles API GET /articles lists all articles
     Failure/Error: get '/articles'

     ActionController::RoutingError:
       No route matches [GET] "/articles"
     # ./spec/requests/articles_spec.rb:29:in `block (3 levels) in <top (required)>'

This output tells us exactly what went wrong (or more accurately, what did not go as expected), and should be treated as our guide towards working code.

Code-along: GET /articles Routing Spec

Let's work on our GET /articles routing spec in spec/routing/articles_spec.rb together to ensure that our routes are mapped to the correct controller method.

Code-along: articles#index Controller Spec

To wrap up our checks that all articles are correctly returned from our index method, we'll need a passing test for the controller method itself: spec/controllers/articles_spec.rb.

GET One Article

Code-along: GET /articles/:id Request Spec

In spec/requests/articles_spec.rb, let's make sure our API is returning a single article correctly.

Code-along: GET /articles/:id Routing Spec

How do we make sure our routes are set to receive GET requests for a single article? How does routing to articles#show differ from articles#index?

Lab: articles#show Controller Spec

Working off of our articles#index, build out the two GET show tests in spec/controllers/articles_spec.rb to pass. Again, remember how articles#show differs from articles#index and be sure to be testing against that.

Completing Specs

Lab: Complete Request and Routing Specs

Based on our GET specs, complete request and routing specs for POST, PATCH, and DELETE.

Lab: Finish ArticlesController Specs

Continue working in spec/controllers/articles_spec.rb to create passing tests for the POST, PATCH, and DELETE controller actions.

Testing Our Model

Code-along: Article Model Spec

In spec/models/articles_spec.rb, we will need to test to make sure that new Articles created are new instances of the Article model.

Lab: Write Article Model and Run the Specs

Let's get the test for our Article Model working.

Further Learning

Build out the Controller, Model, and Routes for a Comment entity that belongs to Article. Let request, routing, controller and model tests drive your build.

Note: a comments migration has already been created. The rest is up to you.

Bonus Challenge

If you're looking for extra challenge or practice once you've completed the above, create a voting feature for articles using outside-in testing.

This will likely be a modification of a resource (rather than creating a new resource) with different controller actions than you're used to (perhaps a up and down actions instead of show or index). Think about what it means to vote something, and how you might test it. Start by sketching out page flow on paper. Try to outline your work at a high level before you start testing and coding.

Additional Resources

Source code distributed under the MIT license. Text and other assets copyright General Assembly, Inc., all rights reserved.