Using BDD to Create a Show View
Learn how to use behavior driven development (BDD) to build the Topic show page.
Guide Tasks
  • Read Tutorial

Let's keep going with building our Topic feature. Since we have a working index view that links to a show view it seems like a natural fit to build out the show action functionality. This will eventually be where users will be able to see posts for a specific topic, such as the Baseball topic show page having posts such as Houston Astros Batting.

We're still on the same branch since we're still working on the topic feature, so we can get started.

Start off by updating the topic feature spec to see if it can reach the show page. We're going to create a new describe block and put a base case test and some pending tests:

# spec/features/topic_spec.rb

describe 'show' do
  it 'can be reached successfully' do
    visit topic_path(@topic)
    expect(page.status_code).to eq(200)
  end

  xit 'should display the topic title' do
  end

  xit 'should have a url that matches the custom url slug' do
  end
end

Running this spec will give us a failure saying The action 'show' could not be found for TopicsController. Let's get this passing by creating a controller show action and creating the template file:

# app/controllers/topics_controller.rb

def show
end

And create the template file:

touch app/views/topics/show.html.erb

Now if we run rspec spec/features/topic_spec.rb the test will pass. With our base system setup let's fill out the next spec:

# spec/features/topic_spec.rb

it 'should display the topic title' do
  visit topic_path(@topic)
  expect(page).to have_css('h1', text: 'Sports')
end

Notice here that I'm leveraging the Capybara have_css matcher that not only looks for the content, but also ensure it is inside of an HTML <h1> header tag. Running this spec will fail, to get it working let's implement the minimum amount of code possible.

First, we need to update the show method so it can pass in the topic values to the view:

# app/controllers/topics_controller.rb

def show
  @topic = Topic.find(params[:id])
end

Next, update the show template:

<!-- app/views/topics/show.html.erb -->

<h1><%= @topic.title %></h1>

Now if you run the specs they're all passing... not quite. In fact now we're getting two failures, what gives? Let's look at the error:

ActiveRecord::RecordNotFound:
Couldn't find Topic with 'id'=sports

Ok, I screwed up (I'd love to say it was on purpose, but I really did forget something important), remember how we're using the friendly_id gem? The normal rails query of Topic.find(params[id] won't work because it's looking for an ID but we're passing a URL like localhost:3000/sports, which actually has an ID that's an integer. Thankfully the Friendly ID gem has a handy method we can use in our controllers to fix this. Update the controller and pass in the friendly method:

# app/controllers/topics_controller.rb

def show
  @topic = Topic.friendly.find(params[:id])
end

Do you see how using a BDD approach is very helpful? Failing tests can quickly tell us when we forget something on the implementation side like it just did. Now let's flesh out the last spec:

# spec/features/topic_spec.rb

it 'should have a url that matches the custom url slug' do
  visit topic_path(@topic)
  expect(current_path).to have_content('sports')
end

This is a little backwards since we typically want our tests to fail first (following the Red, Green, Refactor pattern), however this test is going to pass right away since the functionality was needed to get the other tests passing. Don't worry about it though, processes don't always have the same flow 100% of the time, even professional Rails developers. I put this spec in the test suite, even when I knew it was going to pass, to ensure that if something were to ever happen to the custom URL functionality, this spec should tell us what happened. I typically like having base case tests throughout my test suite so I always have the ability to isolate them in case something goes wrong (such as all of the tests failing for an unknown reason out of the blue... it happens... it happened to me today on DevCamp actually).

Now that our tests are passing, let's refactor some code. Starting with the tests themselves, do you notice some duplication in our show block? Let's change:

# spec/features/topic_spec.rb

  describe 'show' do
    it 'can be reached successfully' do
      visit topic_path(@topic)
      expect(page.status_code).to eq(200)
    end

    it 'should display the topic title' do
      visit topic_path(@topic)
      expect(page).to have_css('h1', text: 'Sports')
    end

    it 'should have a url that matches the custom url slug' do
      visit topic_path(@topic)
      expect(current_path).to have_content('sports')
    end
  end

To:

# spec/features/topic_spec.rb

  describe 'show' do
    before do
      visit topic_path(@topic)
    end

    it 'can be reached successfully' do
      expect(page.status_code).to eq(200)
    end

    it 'should display the topic title' do
      expect(page).to have_css('h1', text: 'Sports')
    end

    it 'should have a url that matches the custom url slug' do
      expect(current_path).to have_content('sports')
    end
  end

See how the visit topic_path(@topic) call was inside every spec? That's always a sign that it should be placed into a before block.

Now open up the controller. Even though it's not an issue right now, the query for a topic is going to be used by multiple methods in the future, so let's setup a before_action that sets the @topic instance variable.

# app/controllers/topics_controller.rb

class TopicsController < ApplicationController
  before_action :set_topic, only: [:show]

  def index
    @topics = Topic.all
  end

  def show
  end

  private

    def set_topic
      @topic = Topic.friendly.find(params[:id])
    end
end

This refactor will prevent duplicate code from being written in the future for the Topic query, all we'll need to do to give access to the @topic instance variable is add the method name, as a symbol, to the array like we did with [:show]. Also notice that I declared that the set_topic method is private. This is because we don't want any other part of the application calling this method, only the controller methods we give permission to do so.

Running the specs again will show that our refactor worked and all of the tests are still passing. Let's startup the server and navigate to localhost:3000 to ensure that our tests aren't lying (it's always a good practice to test out a feature in a browser since it's always possible that the tests could be missing edge cases). However in this case it looks like everything is working properly, if we click on one of the topics we're taken to the topic's show page and the custom URL is also working, nice work!

large

Make sure to make your git commit and in the next lesson we'll build out our form so that topics can be created and edited in the browser.

Resources