Skip to main content
March 23, 2026Noble Desktop Publishing Team/9 min read

Easels App with Test Driven Development: Part 4

Master Test Driven Development with Ruby on Rails

Test Driven Development Cycle

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. This exercise focuses on the crucial refactoring phase.

Topics Covered in This Ruby on Rails Tutorial:

Refactoring the CompletionsController for improved maintainability, streamlining test code to eliminate redundancy, implementing user authentication requirements for to-do creation, and verifying successful test results through browser validation

Tutorial Learning Objectives

Controller Refactoring

Learn to clean up repetitive code in the CompletionsController by extracting shared functionality into reusable private methods.

Test Code Optimization

Streamline test code by creating shared methods and eliminating duplication across feature specifications.

Authentication Implementation

Ensure proper user authentication before allowing to-do creation and completion actions.

Exercise Overview

While all feature tests are passing successfully, the current codebase suffers from significant repetition—a violation of software engineering best practices. Test-driven development follows the fundamental mantra of "red, green, refactor." Initially, failing tests display red error messages. Once functionality is implemented correctly, tests pass and show green indicators. This exercise focuses on the crucial third phase of TDD: refactoring existing code to achieve DRY (Don't Repeat Yourself) principles while maintaining full functionality.

Effective refactoring not only improves code maintainability but also reduces the likelihood of bugs and makes future feature additions more straightforward. By eliminating duplication, you create a more robust foundation for your Rails application.

  1. If you completed the previous exercises (13A–14A), you can proceed directly to the refactoring section below. For those who haven't completed the prerequisite exercises, we strongly recommend finishing exercises 13A–14A first, as they establish the foundational code structure this tutorial builds upon. If you need to catch up, follow the setup instructions in the sidebar.

If You Did Not Complete Previous Exercises (13A–14A)

  1. Close any files currently open in your editor.
  2. Open the Finder and navigate to Class Files > yourname-Rails Class
  3. Launch Terminal.
  4. Type cd followed by a single space (do NOT press Return yet).
  5. Drag the yourname-Rails Class folder from Finder to the Terminal window and press ENTER.
  6. Execute rm -rf easels to remove your existing copy of the easels site.
  7. Run git clone https://bitbucket.org/Noble Desktop/easels.git to clone the easels Git repository.
  8. Navigate into the directory: cd easels
  9. Checkout the appropriate branch: git checkout 14A to synchronize with the previous exercise's completion state.
  10. Install required gems: bundle install
  11. Install JavaScript dependencies: yarn install --check-files

Refactoring the CompletionsController

Controller refactoring is essential for maintaining clean, scalable Rails applications. The CompletionsController currently contains duplicated logic that violates DRY principles and makes maintenance more difficult.

  1. Ensure Terminal is open and you're in the correct directory. If not in the easels directory, navigate there: cd easels

  2. Open app > controllers > completions_controller.rb in your code editor. Examine the create and destroy methods—notice the significant similarity between them. This duplication presents an opportunity to extract shared functionality into reusable methods.

  3. Add a private method for retrieving the to-do item below the destroy method (around line 17):

    end
    
    private
    
       def get_todo
       end
    
    end
  4. Cut one of the Todo.find lines (from either line 4 or 11):

    @todo = Todo.find(params[:todo_id])
  5. Paste this code into the get_todo private method:

    def get_todo
       @todo = Todo.find(params[:todo_id])
    end
  6. Delete the remaining Todo.find line and clean up any leftover whitespace. Your methods should now look 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
  7. Add a before_action callback at the top of the controller class (around line 3) to automatically execute the get_todo method:

    class CompletionsController < ApplicationController 
    
       before_action :get_todo
    
       def create
  8. Save the file and return to Terminal to verify the refactoring hasn't broken functionality.

  9. Execute the test suite: rspec. All tests should still pass, confirming successful refactoring.

  10. Return to completions_controller.rb. The create and destroy methods still contain nearly identical code—only the first line differs. The create method sets completed_at to the current timestamp, while destroy sets it to nil. This presents another refactoring opportunity.

  11. Add a parameterized method below get_todo and above the final end (around line 23):

    def get_todo
       @todo = Todo.find(params[:todo_id])
    end
    
    def mark_todo(time)
       @todo.completed_at = time
       @todo.save!
    end
  12. Refactor the create and destroy methods to use the new mark_todo method, passing the appropriate time value as an argument:

    def create
       mark_todo(Time.current)
       redirect_to root_path
    end
    
    def destroy
       mark_todo(nil)
       redirect_to root_path
    end

    This refactored code is significantly more maintainable and reusable. Future controller actions can leverage these methods, and modifications need only be made in one location.

  13. Verify the refactoring maintains functionality. Save the file and return to Terminal.

  14. Run the test suite again: rspec. All tests should continue passing.

Before vs After Controller Refactoring

FeatureBefore RefactoringAfter Refactoring
Code DuplicationTodo.find repeated in create and destroySingle get_todo private method
Method StructureSeparate logic in each methodShared mark_todo method with time parameter
MaintainabilityChanges require multiple updatesSingle source of truth for shared logic
ReusabilityLimited code reuseMethods can be used by additional actions
Recommended: The refactored approach provides better maintainability and follows DRY principles

Streamlining the Test Code

Test code deserves the same attention to DRY principles as application code. Repetitive test code is harder to maintain and more prone to inconsistencies. By creating shared helper methods, you can improve test maintainability and reduce the likelihood of copy-paste errors.

  1. With the controller refactoring complete, let's address test code duplication. Open the three most recent test files from spec > features in creation order:

    • user_can_see_own_todos_spec.rb
    • user_marks_todo_complete_spec.rb
    • user_marks_todo_incomplete_spec.rb
  2. Compare these files—notice the identical sign-in steps under the #user signs in comments. This duplication violates DRY principles and creates maintenance overhead.

  3. Copy the following sign-in code from any of the files (typically around lines 7-10):

    visit new_session_path
    fill_in "Email", with: "user@example.com"
    click_on "Sign In"
  4. RSpec supports custom helper methods through a support directory structure. Create a new folder called support within the spec folder.

  5. Inside the support folder, create a features subfolder. This nested structure ensures helper methods only apply to feature tests, maintaining proper test isolation.

  6. Create a new file called sign_in.rb inside the spec/support/features directory.

  7. Add the following module structure to the blank file:

    module Features
    
       def sign_in_as(email)
       end
    end
  8. Understanding the structure:

    • RSpec requires shared feature test code to be encapsulated within a Features module for proper namespacing and organization.

    • The sign_in_as method accepts an email parameter, making it flexible for different user scenarios.

  9. Paste the previously copied sign-in code into the method:

    def sign_in_as(email)
       visit new_session_path
       fill_in "Email", with: "user@example.com"
       click_on "Sign In"
    end
  10. Make the method dynamic by replacing the hardcoded email with the parameter:

    fill_in "Email", with: email
  11. Save the file and open user_can_see_own_todos_spec.rb.

  12. Replace the sign-in steps (around line 9) with the new helper method call:

    #user signs in
    sign_in_as("user@example.com")
  13. Save the file and test the implementation by running rspec in Terminal.

    The test will fail because RSpec hasn't been configured to load the helper methods yet.

  14. To enable the shared code, open spec > rails_helper.rb.

  15. Locate and uncomment the following line (around line 23):

    Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

    This directive instructs RSpec to automatically load all Ruby files within the support directory and its subdirectories.

  16. Add the module inclusion configuration (around line 53):

    config.infer_spec_type_from_file_location!
    
    config.include Features, type: :feature

    This configuration makes all Features module methods available to feature tests, extending the test environment with your custom helpers.

  17. Save the file and verify functionality: rspec

    All tests should now pass, confirming the helper method integration is successful.

  18. Clean up the test file by removing the commented-out code in user_can_see_own_todos_spec.rb:

    #user signs in
    sign_in_as("user@example.com")
    
    #user creates a to-do via a form
  19. Apply the same refactoring to the remaining test files. In user_marks_todo_complete_spec.rb, replace the sign-in code:

    #user signs in
    sign_in_as("user@example.com")
  20. Make the identical change in user_marks_todo_incomplete_spec.rb and save all files.

  21. Confirm all tests still pass: rspec

  22. Further streamline the test code by addressing to-do creation duplication. In user_can_see_own_todos_spec.rb, copy the to-do creation code (lines 13-17):

    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"
  23. Since the helper file will contain multiple methods, rename sign_in.rb to helper_methods.rb for better organization.

  24. Add a new method to helper_methods.rb before the final end:

    def create_todo(title)
    end
  25. Paste the copied code into the method and remove comments:

    def create_todo(title)
       click_on "Add a New To-Do"
       fill_in "Title", with: "paint house"
       click_on "Add To-Do"
    end
  26. Make the method dynamic by using the title parameter:

    fill_in "Title", with: title
  27. Save the file and update user_can_see_own_todos_spec.rb to use the new helper:

    #user creates a to-do via a form
    create_todo("paint house")
  28. Verify functionality: rspec

  29. Clean up the commented code and apply the same refactoring to the other test files that create to-dos, including user_creates_todo_spec.rb.

  30. Run the complete test suite to ensure all refactoring is successful: rspec

    All five tests should pass, confirming that the shared helper methods work correctly across all feature tests.

RSpec Support Directory Structure

RSpec allows us to create separate files for storing custom methods that several tests share. Files must be in a separate folder called support, with feature-specific methods in spec/support/features/

Creating Shared Test Methods

1

Create Support Structure

Create spec/support/features directory structure for organizing shared test methods

2

Build Sign-in Method

Create sign_in_as method in Features module to handle user authentication in tests

3

Configure Rails Helper

Uncomment auto-require line and add config.include Features statement to load shared methods

4

Implement Todo Creation

Add create_todo method to eliminate repetitive form filling code across test files

Ensuring the User Must Sign in to Create a To-Do

Security is paramount in web applications. Currently, any visitor can create to-dos without authentication—a significant security vulnerability. Implementing proper authentication ensures that only authenticated users can access protected resources, following Rails security best practices.

  1. Authentication logic belongs in the ApplicationController to ensure availability across all controllers. Open app > controllers > application_controller.rb.

  2. Add an authentication method before the final end (around line 12):

    def set_current_email
       @current_email = session[:current_email]
    end
    
    def authenticate!
       redirect_to new_session_path unless @current_email
    end

    This method implements a security check similar to the popular Devise gem. It verifies user authentication status and redirects unauthenticated users to the sign-in page.

  3. Save the file and apply authentication to the TodosController. Open app > controllers > todos_controller.rb.

  4. Add the authentication requirement using a before_action callback:

    class TodosController < ApplicationController
    
       before_action :authenticate!
    
       def index
  5. Apply the same protection to app > controllers > completions_controller.rb:

    class CompletionsController < ApplicationController
      before_action :authenticate!
      before_action :get_todo
  6. Test the authentication implementation. Save all files and run rspec.

    Two tests will fail because they don't account for the new authentication requirement.

  7. Fix the first failure by updating user_creates_todo_spec.rb. Add authentication before to-do creation:

    visit root_path
    
    #user signs in
    sign_in_as("user@example.com")
    
    #user creates a to-do via a form
  8. Save and test: rspec

    Only one failure should remain, related to the homepage display test.

  9. The remaining failure occurs because the homepage now requires authentication. Move the main heading to a publicly accessible location. Open app > views > todos > index.html.erb.

  10. Cut the <h1>To-Dos</h1> element from the first line.

  11. Open app > views > layouts > application.html.erb and paste the heading in an appropriate location within the layout, ensuring it's visible to all users regardless of authentication status.

  12. Save both files and run the final test: rspec

    All tests should now pass, confirming that authentication is properly implemented while maintaining all existing functionality.

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.
Identifying the security gap in the application before implementing authentication

Authentication Implementation

ApplicationController Method

Add authenticate! method that redirects unsigned users to the sign-in page, similar to the Devise gem pattern.

Before Action Filters

Implement before_action :authenticate! in both TodosController and CompletionsController to secure all actions.

Verifying the Successful Test Results in a Browser

While automated tests provide confidence in functionality, browser testing ensures the user experience meets expectations. This final verification step confirms that refactored code not only passes tests but also delivers the intended user experience.

Launch your Rails server with rails server and navigate through the application workflow: visit the homepage, sign in with valid credentials, create a to-do item, and test the completion functionality. This manual verification ensures that your refactored, secure application provides the seamless experience your users expect.

The comprehensive refactoring completed in this exercise demonstrates professional Rails development practices: maintaining DRY code principles, implementing proper security measures, and ensuring thorough test coverage. These practices form the foundation for scalable, maintainable web applications.

Final Verification Steps

0/4

Key Takeaways

1Test Driven Development follows a red-green-refactor cycle where refactoring is essential for maintaining clean, DRY code
2Controller refactoring involves extracting shared functionality into private methods and using before_action filters for common operations
3RSpec support directories allow creation of shared methods that eliminate test code duplication across multiple specification files
4The Features module pattern enables sharing of common test functionality like user sign-in and form interactions across feature tests
5Authentication should be implemented at the controller level using before_action filters to secure application endpoints
6Private methods in controllers can accept parameters to make shared functionality more flexible and reusable
7Moving UI elements to application layout ensures consistent display across all pages regardless of authentication state
8Regular test suite execution after each change ensures refactoring doesn't break existing functionality

RELATED ARTICLES