Explore an in-depth tutorial on Ruby on Rails, featuring a comprehensive guide on refactoring the CompletionsController, streamlining test code, and ensuring user authentication for creating a to-do. Learn valuable insights on making your code DRY (Don't Repeat Yourself), enhancing the security of your app, and verifying test results in a browser.
Key Insights:
- This tutorial covers a range of topics including refactoring the CompletionsController, streamlining test code, and user authentication for creating a to-do.
- To make your code DRY, you can separate out the shared functionality for reuse between both 'create' and 'destroy' methods in the CompletionsController.
- Streamlining your test code can involve creating custom methods that several tests share, which are stored in a separate 'support' folder.
- It's essential to require user authentication for creating a to-do. This can be done by defining an authenticate method in the ApplicationController, which can be used throughout the app.
- Whenever a change is made to your code, it's necessary to check that the change did not break a different part of the application. This can be done by running 'rspec' in the command line.
- The final part of the tutorial involves verifying the successful test results in a browser. Starting the server and playing around with the app in a browser can provide a visual confirmation that everything is working as intended.
Learn how to refactor your code and streamline your test code in Ruby on Rails with this in-depth tutorial, guiding you to make your code more dynamic and reusable through key techniques like separating shared functionality, utilizing private methods, and more.
This exercise is excerpted from Noble Desktop’s past web development training materials. Noble Desktop now teaches JavaScript and the MERN Stack in our Full Stack Development Certificate. To learn current skills in web development, check out our coding bootcamps in NYC and live online.
Topics Covered in This Ruby on Rails Tutorial:
Refactoring the CompletionsController, Streamlining the Test Code, Ensuring the User Must Sign in to Create a To-do, Verifying the Successful Test Results in a Browser
Exercise Overview
Although all of our feature tests are passing, the code is quite repetitive. The mantra of test driven development is red, green, refactor. When your tests initially fail, you’ll see red errors until they pass and then you’ll see green text. In this exercise you’ll go onto the next part of TDD: refactoring (cleaning up your code to make it DRY).
-
If you completed the previous exercises, you can skip the following sidebar. We recommend you finish the previous exercises (13A–14A) before starting this one. If you haven’t finished them, do the following sidebar.
If You Did Not Do the Previous Exercises (13A–14A)
- Close any files you may have open.
- Open the Finder and navigate to Class Files > yourname-Rails Class
- Open Terminal.
- Type
cd
and a single space (do NOT press Return yet). - Drag the yourname-Rails Class folder from the Finder to the Terminal window and press ENTER.
- Run
rm -rf easels
to delete your copy of the easels site. - Run
Git clone https://bitbucket.org/Noble Desktop/easels.Git
to copy the easels Git repository. - Type
cd easels
to enter the new directory. - Type
Git checkout 14A
to bring the site up to the end of the previous exercise. - Run
bundle
to install any necessary gems. - Run
yarn install—check-files
to install JavaScript dependencies.
Refactoring the CompletionsController
Make sure Terminal is open. If you aren’t in the easels directory, type:
cd easels
In your code editor open app > controllers > completions_controller.rb and notice how similar the create and destroy methods are. Let’s separate out the shared functionality so it can be reused between both of these methods.
-
Below the end of the destroy method, around line 17, add the following private method for getting the to-do that the user marks as Complete/Incomplete:
end private def get_todo end end
-
Cut any of the Todo.find lines of code (either around line 4 or 11):
@todo = Todo.find(params[:todo_id])
-
Paste the code into the get_todo private method as shown in bold below:
def get_todo @todo = Todo.find(params[:todo_id]) end
-
Delete the other Todo.find line (and any leftover whitespace) so the code looks like:
def create @todo.completed_at = Time.current @todo.save! redirect_to root_path end def destroy @todo.completed_at = nil @todo.save! redirect_to root_path end
-
Around line 3, tell the controller to run the get_todo private method any time it first loads by typing the following bold before_action:
class CompletionsController < ApplicationController before_action :get_todo def create
Save the file and switch to Terminal.
To make sure the tests still pass, type
rspec
in the command line. Phew!-
Switch back to completions_controller.rb in your code editor.
The remaining code in the create and destroy methods looks very similar. The only difference is the first line. The code inside the create method sets the completed_at timestamp to the current time, and the same line within the delete method sets it to nothing.
-
Under the get_todo private method and above the final
end
(around line 23), add the following bold code that allows us to pass in a time as an argument:def get_todo @todo = Todo.find(params[:todo_id]) end def mark_todo(time) @todo.completed_at = time @todo.save! end
-
As shown in bold, replace all the code inside the create and delete methods and tell them to run the
mark_todo(time)
method, noticing that the only code that differs between the methods is being passed in as an argument in place of the dynamic time:def create mark_todo(Time.current) redirect_to root_path end def destroy mark_todo(nil) redirect_to root_path end
The code is a lot more dynamic and reusable now! If we add additional actions to this controller, we could reuse some of this code wherever needed.
Let’s make sure the tests still work. Save the file and switch to Terminal.
Type the
rspec
command to see that all the tests still pass.
Streamlining the Test Code
-
The other controllers already have DRY code so let’s move on to streamlining our test code. Switch back to your code editor and from spec > features open the last three test files you worked on. To keep organized, it may help you to open them in the order you created them in:
- User_can_see_own_todos_spec.rb
- User_marks_todo_complete_spec.rb
- User_marks_todo_incomplete_spec.rb
Switch between the three spec files to see that we reused quite a bit of code. For one, the sign in steps under the #user signs in comment are the same in all of the three files!
-
In any of the files copy the code shown below (starting around either line 7 or 10):
visit new_session_path fill_in "Email", with: "user@example.com" click_on "Sign In"
RSpec allows us to create one or more separate files for storing custom methods that several tests share. Because the file(s) must be in a separate folder called support, in the spec folder, create a new folder and name it: support
Inside the support folder, create a nested folder called features (this is because we only want the custom methods we’ll add to apply to feature tests like the ones we wrote in previous exercises).
We want to create a custom method for signing in. Create a new file in the features folder called sign_in.rb. (We can name the file anything we want.)
-
In the blank file, write the following code that any of our feature tests can access:
module Features def sign_in_as(email) end
-
Let’s break that down:
In order to share code between all of our feature tests, RSpec requires us to place all the code we write in this file into a module called Features. (Remember that modules are ways of keeping pieces of code separate from one another.)
The
sign_in_as
method takes an argument calledemail
that allows us to pass in the email address of the user who is signing in.
-
Paste the code you copied earlier inside the
sign_in_as(email)
method as shown:def sign_in_as(email) visit new_session_path fill_in "Email", with: "user@example.com" click_on "Sign In" end
-
To make the pasted code dynamic, all we need is to change the dummy email address string in the fill_in method around line 5. Make the edit as shown in bold:
fill_in "Email", with: email
Save the file.
Switch to user_can_see_own_todos_spec.rb (which should be the first test tab).
-
Under the #user signs in comment around line 9, call the
sign_in_as
method and remove the previous code:#user signs in sign_in_as("user@example.com")
NOTE: We are passing in the dummy email address we used earlier as an argument. Additionally, we’re commenting the old code out until we make sure our test passes.
Save the file and switch to Terminal.
-
Type
rspec
in the command line.As indicated by one F and four periods, one of our five tests failed. Even though the user was able to successfully see their own to-dos the last time we ran the test suite, they are no longer able to sign in as user@example.com because we haven’t defined the `sign_in_as’ method.
In order to load the shared code in sign_in.rb, we need to make some modifications. Switch back to your code editor.
Open the file that is loaded into every test: spec > rails_helper.rb
-
Around line 23, UNcomment the following code:
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
As you can see in the explanatory comment above this code, this line auto-requires all files in the support directory. It’s telling RSpec to access anything inside the support folder, then any subfolder (such as our features folder), then for any individual files that end in .rb (the asterisks are wildcards that mean “any”).
-
Around line 53, add the following bold code:
config.infer_spec_type_from_file_location! config.include Features, type: :feature
This include statement is necessary to load the Feature module where we are storing the code shared between our :feature tests. This powerful statement extends our test suite to include any and all of the Feature module’s methods, no matter how many we add later.
Save the file and switch to Terminal.
-
Type
rspec
in the command line.We’re back in the green! Because all of our tests are passing, we know that the sign_in_as method works.
-
Back in user_can_see_own_todos_spec.rb, delete the comments under
#user signs in
so the code looks as follows:#user signs in sign_in_as("user@example.com") #user creates a to-do via a form
Save the file.
-
In user_marks_todo_complete_spec.rb, replace the three lines under
#user signs in
with:#user signs in sign_in_as("user@example.com")
Save the file.
Make the same modification in user_marks_todo_incomplete_spec.rb (and make sure to save when done!):
-
Switch to Terminal and type
rspec
to run the test suite.All the tests pass, proving that we’re able to use the shared sign_in_as method in any of our feature tests. Let’s DRY up our test code even more.
Return to user_can_see_own_todos_spec.rb (the first test tab) in your code editor.
-
In order to test this feature and all the subsequent ones, the user needs to be able to create a new to-do and save it to the database. Let’s combine all this code into a single shared method. From around line 13–17, copy all the code shown below:
click_on "Add a New To-Do" fill_in "Title", with: "paint house" #user saves the to-do to the database click_on "Add To-Do"
We want to paste this code into a new shared method, which we could either put in a new file inside the support > features folder or inside sign_in.rb (it doesn’t matter what we choose).
-
For the sake of convenience let’s keep our code in one file. Because our file will have more than just a sign in method, rename sign_in.rb to helper_methods.rb
TIP: If you are using Sublime Text, you can CTRL–click or Right–click on the filename and choose Rename. At the bottom enter helper_methods.rb and hit Return.
-
Switch to helper_methods.rb and add the following method around line 9 before the final
end
:def create_todo(title) end
-
Inside the method paste in the copied code. Remove the comments and whitespace so it looks as shown below:
def create_todo(title) click_on "Add a New To-Do" fill_in "Title", with: "paint house" click_on "Add To-Do" end
-
To make the pasted code dynamic, make the following bold edit around line 11:
fill_in "Title", with: title
Save the file.
Switch back to the first test tab: user_can_see_own_todos_spec.rb
-
Under the #user creates a to-do via a form comment around line 12, call the method (passing in the string), and temporarily comment out the pre-existing code:
#user creates a to-do via a form create_todo("paint house")
Save the file and switch to Terminal.
Type
rspec
in the command line to see all five tests pass!-
Back in user_can_see_own_todos_spec.rb, delete the code you just commented out so you have:
#user creates a to-do via a form create_todo("paint house") #verify that the page has the signed in user's to-do, but not the to-do created in the background
Save the file.
Make the same modifications in the other two files (the complete and incomplete specs), replacing the code from around lines 11–15. Save each file when done.
The user also had create a to-do in our second test: user_creates_todo_spec.rb Open that file so we can refactor it.
-
Call the create_todo method, replacing the code from around lines 10–14:
#user creates a to-do via a form create_todo("paint house") #verify that the page has our to-do
Save the file and switch to Terminal.
-
Type
rspec
in the command line.We’ve refactored all the repetitive code and all five tests are still passing! An advantage of sharing code in a shared module is the greater ease of making any necessary changes to these methods later.
Ensuring the User Must Sign in to Create a To-Do
We’ve followed the test driven design mantra of red, green, refactor from start to finish. But come to think of it, we have one more thing to do. At this juncture any old schmuck can create a to-do. Let’s add a method that ensures a user must be signed in before they can add their tasks to our site.
We now need to define this method in our ApplicationController so it can be used throughout the app. Open: app > controllers > application_controller.rb
-
Around line 12, before the final
end
, add the following bold method that redirects a user who isn’t signed in to the new_session_path where they can log into the app:def set_current_email @current_email = session[:current_email] end def authenticate! redirect_to new_session_path unless @current_email end
Just like the Devise gem for user authentication, the method we wrote checks the controller to make sure the user is signed in. Remember that we are using the current_email method (around line 6) to log the user in. !current_email means that the user is not signed in.
Save the file.
To require the user to be signed in before creating a to-do, we need to call the method in the TodosController using a before_action. Switch to todos_controller.rb.
-
Around line 3, tell the controller to run the authenticate method any time it first loads by adding the bold before_action:
class TodosController < ApplicationController before_action :authenticate! def index
-
To keep everything secure, we should go ahead and make the same change to app > controllers > completions_controller.rb as well:
class CompletionsController < ApplicationController before_action :authenticate! before_action :get_todo
Every time you make a change to your code, it’s necessary to check that the change did not break a different part of the application. Save the file and switch to Terminal.
-
Type
rspec
in the command line.We have two failures–let’s resolve them one at a time. The first error message shows that the user can no longer create a to-do successfully. Capybara tried to create a to-do of
"
paint house"
but it couldn’t find the"
Add a New To-Do"
button. -
Switch to user_creates_todo_spec.rb in your code editor so we can see why it fails.
Yikes, we don’t have any step in which the user signs into the app! When we created this test we assumed the user did not have to be signed in. Let’s update our test to take our new authenticate method into account.
-
Around line 9, add the following bold sign in step (which should work because the user will get redirected to the sign in page after they visit the root_path):
visit root_path #user signs in sign_in_as("user@example.com") #user creates a to-do via a form
Save the file and switch to Terminal.
-
Type
rspec
in the command line.Progress—we only have one failure! Now we need to make sure that the user can successfully visit the homepage and see the proper CSS with text of
'
To-Dos'
.If you guessed that this is happening because the user needs to be signed in to see the homepage, you’re correct! To fix this issue we can either change where the route goes to or we can move the h1 element to the main page of our application so that a visitor will always see it. The second option is easier so let’s do that.
Back in your code editor, open the index page: app > views > todos > index.html.erb
On the first line cut the
<h1>To-Dos</h1>
code and any remaining whitespace.Save the file.
Open the global view template: app > views > layouts > application.html.erb
-
In order for the heading to be a permanent, static part of the site, paste it within the body tag but before the yield tag (around line 10), as shown in bold:
<body> <h1>To-Dos</h1> <%= yield %>
Save the file.
-
In Terminal, type
rspec
in the command line.Great, everything passes and now all our five tests are much cleaner!
Verifying the Successful Test Results in a Browser
Now that we’ve cleaned up our test code and everything is working as we intended, let’s start the server so we can see all the hard work we did. Let’s play around with our app in a browser!
Type
rails server
in the command line to start the server.-
Open a web browser and go to the sign in page at: localhost:,000/sessions/new
Great, we can see the To-Dos headline we just moved to our app’s main page! We’ve now gone full circle with test driven development. Because our browser simulator, Capybara, spared us the tedium of constantly testing in a browser, once we view the site in an actual browser, the features on our site are guaranteed to work.
Now that we’re requiring users to log in using an email address, let’s do so. In the Email field, enter test@example.com (or some other email address).
Hit Sign In to authenticate yourself.
Now we can add a task to our list. Click Add a New To-Do.
In the Title field, enter the name of your task such as buy milk.
Hit Add To-Do to save the to-do to the database.
-
Continue to play around with the app by doing one or more of the following:
- You can add more to-dos such as clean the bathroom, walk the dog, etc.
- You can hit Complete and then Incomplete to see the functionality.
While the app works, nothing is styled. We’ll keep the simple look but add a few basic styles to make for a better user experience. For one, we really need a way to distinguish the complete tasks from the incomplete tasks. We could apply any styling we want. Let’s “cross off” the completed to-dos by adding a strike-through.
Leave the page open in your browser so we can reload it when we come back.
Switch back to your code editor.
In the app > assets > stylesheets folder, create a new file called: styles.css
We prepared a snippet for you to save time. On the Desktop navigate to Class Files > yourname-Rails Bootcamp and open easels-styles.css in your code editor.
Select all the code and copy it.
Close the file. If you aren’t back in styles.css, switch to it.
Paste the code into the empty style sheet. Feel free to examine these simple styles.
Save styles.css.
Switch back to the browser where the Easels app should still be open.
-
Reload the page and play around with the app.
That’s it! Admire your creation that you built using test-driven development!
When you’re done, back in Terminal, stop the server by hitting CTRL–C.