- 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:
That a user can navigate to the
/new
pathThat a user can fill out a form and enter in a
title
valueThat a user can click the
Save
buttonThat 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:
Adding
update
to theset_topic
method so that the method will have access to the@topic
Calling on the Rails
update
method and passing in the strong parameters in as the argumentRedirecting 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 'newor
editform, which is why I use the longer partial call and pass in the
sourcevariable. By doing this the form now has access to a variable called
sourcethat will say if it's coming from the
newor
edit` 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:
Now if we enter a topic title and click save
it will work and take us to the show
page:
If you append /edit
to the show page URL it will take us to the edit
view:
And if you change the content and click save it will update the title and take you back to the show view:
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.