Using BDD to Implement New and Edit Functionality
Learn how to use behavior driven development (BDD) to build the Topic new and edit feature in a Ruby on Rails application.
Guide Tasks
  • Read Tutorial

I've always loved how the Capybara system works with form elements. Having the ability to write code that will mimic actual user behavior never ceases to excite me, with this in mind let's build out our Topic new and edit actions. This will let us integrate the functionality for users to create and edit topics for the application.

Opening up the integration specs we're going to create a new describe block that will merge in both the edit and new actions into a form block:

# spec/features/topic_spec.rb

  describe 'form' do
    it 'can be reached successfully when navigating to the /new path' do
      visit new_topic_path
      expect(page.status_code).to eq(200)
    end
  end

Running the specs is going to complain that The action 'new' could not be found for TopicsController, by now you should have a good idea what to do to get this working. Let's create the method in the controller and create the template file:

# app/controllers/topics_controller.rb

  def new
  end

And then create the file:

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

If you run the specs now they'll all be passing, now let's test if we can create a new topic from the new page:

# spec/features/topic_spec.rb

    it 'allows users to create a new topic from the /new page' do
      visit new_topic_path

      fill_in 'topic[title]', with: "Star Wars"

      click_on "Save"

      expect(page).to have_content("Star Wars")
    end

This is going to test a few different features:

  1. That a user can navigate to the /new path

  2. That a user can fill out a form and enter in a title value

  3. That a user can click the Save button

  4. That a user is redirected to a page that renders the title value (this will be the show page, however this test won't break if we decide to redirect the user to the index page, which is handy)

After running the spec we see the following error: Capybara::ElementNotFound: Unable to find field "topic[title]". Let's let the tests guide our development one step at a time. Let's update the new template with what Capybara is asking for:

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

<%= text_field :title %>

As a side note, I'll usually implement a few code changes when I'm at work, however I think it's helpful to take it step by step so you can see the types of errors that you'll run into and how to fix them

This will fail with the error message ActionView::Template::Error: wrong number of arguments (1 for 2..3). This is because we need to setup the form in its entirety, not just the form element. Let's update the page:

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

<%= form_for @topic do |f| %>

  <%= f.text_field :title %>

<% end %>

Now we're getting somewhere, the specs will return this error First argument in form cannot contain nil or be empty. This is because that @topic instance variable needs to be sent to the view from the controller. Update the controller's new method:

# app/controllers/topics_controller.rb

  def new
    @topic = Topic.new
  end

Doing this in the controller's new method will create a new instance of Topic that our form can work with. Running the specs will show that we're heading down the right track, it's now saying Capybara::ElementNotFound: Unable to find link or button "Save". Let's implement that into the form:

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

<%= form_for @topic do |f| %>

  <%= f.text_field :title %>
  <%= f.submit 'Save' %>

<% end %>

Now our form is setup properly, running our specs will give the error The action 'create' could not be found for TopicsController. Note that the new action simply renders the form to the user, in order to create a new topic we need a proper create action. Let's implement this into the controller:

# app/controllers/topics_controller.rb

  def create
    @topic = Topic.new(params)
    @topic.save
  end

In a perfect world this would work, however we're in a world of hackers and nasty people who want to attack your website. For that reason Rails requires you to whitelist the parameters that you will allow to pass through your form. Let's add this call as an argument:

# app/controllers/topics_controller.rb

  def create
    @topic = Topic.new(params.require(:topic).permit(:title))
    @topic.save
  end

That portion worked, running the specs will complain that we have a missing template because by default Rails is going to look for a create view template file. However we want to redirect the users automatically to the show page, let's update the create action:

# app/controllers/topics_controller.rb

  def create
    @topic = Topic.new(params.require(:topic).permit(:title))
    @topic.save
    redirect_to topic_path(@topic)
  end

Perfect, we're back to all green. Now it's time to refactor. Our create action isn't very comprehensive, what happens if there's an error? Right now we're not planning on anything wrong happening (and it will, I promise), we're also not giving the user any feedback. So let's update our code:

# app/controllers/topics_controller.rb

  def create
    @topic = Topic.new(params.require(:topic).permit(:title))

    if @topic.save
      redirect_to topic_path(@topic), notice: 'Topic was successfully created.'
    else
      render :new
    end
  end

If you run the specs again you'll see that our refactor worked and the functionality is still passing. Notice how the refactor has a conditional that will handle both a success and failure and will respond accordingly, we're also giving the user feedback with the notice method.

Now let's build out the edit action, we're not going to create an edit routing spec since our form filling spec using the edit path will test this. This is a good time to mention that the further we go and the more comfortable you get with testing the more you'll see that we don't have to create overly simplistic tests since our more practical tests, such as filling out a form when visiting a page will naturally take care of this for us and also make our test suite more efficient. We want our tests to be comprehensive, but just like you wouldn't want duplicate implementation code, you also don't want duplicate tests. With all this in mind let's create the edit spec:

# spec/features/topic_spec.rb

    it 'allows users to update a an existing topic from the /edit page' do
      visit edit_topic_path(@topic)

      fill_in 'topic[title]', with: "Star Wars"

      click_on "Save"

      expect(page).to have_content("Star Wars")
    end

As you can this, this spec is pretty similar to the new spec, we're simply navigating to a different page. Running rspec spec/features/topic_spec.rb will give us the error The action 'edit' could not be found for TopicsController. Let's create the method, add it to the before_action call since we know it's going to have to have access to the @topic instance variable, and also create the view template. Yes, we're doing a few steps, however I think they're all pretty basic and we're not creating too much implementation code at a single time.

# app/controllers/topics_controller.rb

before_action :set_topic, only: [:show, :edit]

  def edit
  end

And:

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

On a side note, you may wonder why I like to create files on the command line instead of using the text editor. The main reason is because I've discovered that doing it this way allows me to focus on the flow of the file system and makes it more efficient when navigating to files. Running the specs will now give us the error Unable to find field "topic[title]", this makes sense since our edit template doesn't have any code. Remember with BDD we want to use the most basic implementation possible to get the tests passing, so let's simply copy the code from the new template into our edit view:

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

<%= form_for @topic do |f| %>

  <%= f.text_field :title %>
  <%= f.submit 'Save' %>

<% end %>

Running the specs now will give us the error The action 'update' could not be found for TopicsController. Hopefully by now you can see the similar pattern that we saw when creating the new/create action earlier. Let's implement the update method in the controller:

# app/controllers/topics_controller.rb

before_action :set_topic, only: [:show, :edit, :update]

  def update
    @topic.update(params.require(:topic).permit(:title))
    redirect_to topic_path(@topic)
  end

We're doing a few things here:

  1. Adding update to the set_topic method so that the method will have access to the @topic

  2. Calling on the Rails update method and passing in the strong parameters in as the argument

  3. Redirecting the user to the topic_path

Running the specs now you'll see that all of the tests are passing, nice work! However we can't stop here, there are a few key refactors. First, remember that it should be our goal for our project to be DRY, but if you look through our code you'll see quite a bit of duplicate code, let's clean this up one at a time.

Use a Partial for the Form

If you look at our new and edit templates you'll see how they're literally duplicates of each other, that's not good #frownyface. Let's use a form partial to remove this duplication. Create a partial in the views/topics directory:

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

Remember that Rails interprets view files with an underscore as a view partial, this means we wouldn't ever send a user directly to this file, we'd simply call it from other files. Now we can change three files:

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

<%= form_for @topic do |f| %>

  <%= f.text_field :title %>
  <%= f.submit 'Save' %>

<% end %>
<!-- app/views/topics/new.html.erb -->

<%= render partial: 'form', locals: { source: 'new' } %>
<!-- app/views/topics/edit.html.erb -->

<%= render partial: 'form', locals: { source: 'edit' } %>

Running the specs will show that the application is still working, but now we have much better view template code. I used a longer partial call on the new and edit views, technically I could have used:

<%= render 'form' %>

And it would have worked the same way. However I've found that I like for my form to know if it's being used as a 'neworeditform, which is why I use the longer partial call and pass in thesourcevariable. By doing this the form now has access to a variable calledsourcethat will say if it's coming from theneworedit` actions.

Refactor the update method

Now let's refactor the update method like how we cleaned up the create action.

# app/controllers/topics_controller.rb

  def update
    if @topic.update(params.require(:topic).permit(:title))
      redirect_to @topic, notice: 'Your topic was successfully updated.'
    else
      render :edit, notice: 'There was an error processing your request!'
    end
  end

This will let the update action handle errors and give the user feedback. Running the specs again will show that the tests are all passing, so that refactor worked.

Creating a Strong Parameters Method

Did you notice that our create and update methods are both using the same, very ugly, strong parameters call params.require(:topic).permit(:title)? Whenever you see duplication like this it's always the sign that the code should be moved into it's own method. Let's create a new private method and move the strong params call into it:

# app/controllers/topics_controller.rb

  private

    def topic_params
      params.require(:topic).permit(:title)
    end

Now we can call this topic_params method instead of the long params.require(:topic).permit(:title) string of code:

# app/controllers/topics_controller.rb

  def create
    @topic = Topic.new(topic_params)

    if @topic.save
      redirect_to topic_path(@topic), notice: 'Topic was successfully created.'
    else
      render :new
    end
  end

  def update
    if @topic.update(topic_params)
      redirect_to @topic, notice: 'Your topic was successfully updated.'
    else
      render :edit, notice: 'There was an error processing your request!'
    end
  end

If you run the specs now you'll see that they're all passing and our code is much cleaner. This last refactor was very important because the strong parameters method will need to be updated each time we want to add a new form element into the topic form. It would be bad form to have to update multiple parts of the same file with identical changes, but now we simply can edit the topic_params method and it will populate to the other two actions.

Let's startup the Rails server and make sure that everything is working like our specs would like us to believe. First navigate to localhost:3000/topics/new, you'll see it properly shows us the new form:

large

Now if we enter a topic title and click save it will work and take us to the show page:

large

If you append /edit to the show page URL it will take us to the edit view:

large

And if you change the content and click save it will update the title and take you back to the show view:

large

Very nice work, you've created a fully functional feature for the application and you did it using behavior driven development, so you can be confident that the code works. In the next few guides we're going to walk through how to build out our Post functionality so users will be able to create posts that are nested under topics.

Before we move onto the next lesson, let's make sure to merge in the add-topic git branch into the master branch.

Commit your latest work and then run git checkout master

From there you can run git merge add-topic, that will merge all of our work into the master branch. From here we can run git push and it will merge everything up to the GitHub master branch (n the future we'll also walkthrough how to do it using the GitHub tool.

Resources