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

Easels App with Test Driven Development: Part 3

Master Test-Driven Development with Ruby on Rails

Ruby on Rails Testing Fundamentals

Test-Driven Development

Write tests before implementing functionality to ensure code quality and proper feature behavior. Build confidence in your application's reliability.

RSpec Feature Testing

Simulate user interactions with Capybara to test complete workflows. Verify that users can successfully complete tasks end-to-end.

Rails MVC Architecture

Understand how models, views, and controllers work together. Learn proper separation of concerns for maintainable applications.

Topics Covered in This Ruby on Rails Tutorial:

Test #4: Ensuring the User Can Mark Their To-do Complete, Test #5: Ensuring the User Can Mark Their To-do Incomplete

Exercise Overview

Now that users can navigate to the Easels homepage, authenticate securely, persist their tasks to the database, and view only their own to-do items, we're ready to implement the core task management functionality. The primary value proposition of the Easels application lies in providing users with an intuitive interface to toggle their to-dos between complete and incomplete states. This workflow mirrors real-world productivity patterns where users frequently need to mark tasks as done and occasionally revert completed items back to pending status.

In this exercise, we'll implement these features using test-driven development principles, ensuring our application behaves predictably and maintains data integrity throughout the task lifecycle. We'll start by creating comprehensive tests that validate the user's ability to mark tasks as complete, then build the necessary controller actions and view components to support this functionality.

  1. If you've successfully completed the previous exercises (13A–13B), you can proceed directly to the next section. We strongly recommend finishing those foundational exercises before continuing, as they establish the authentication and data persistence layers that this exercise builds upon. If you haven't completed them, follow the setup instructions in the sidebar below.

    Todo Management Implementation Timeline

    Previous Exercises

    User Authentication

    Users can sign in and see only their own tasks

    Test #4

    Mark Complete Feature

    Add ability to mark todos as completed with visual feedback

    Test #5

    Mark Incomplete Feature

    Allow users to undo completion status for flexibility

    Next Exercise

    Code Refactoring

    Clean up duplicate code following DRY principles

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

  1. Close any open files in your editor to start with a clean workspace.
  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 the Finder window into Terminal and press ENTER.
  6. Run rm -rf easels to remove any existing copy of the easels application.
  7. Run git clone https://bitbucket.org/Noble Desktop/easels.git to clone the current easels repository.
  8. Type cd easels to navigate into the project directory.
  9. Type git checkout 13B to switch to the branch that represents the completed state from the previous exercise.
  10. Run bundle install to install all required Ruby gems and their dependencies.
  11. Run yarn install --check-files to install JavaScript dependencies and verify file integrity.

Test #4: Ensuring the User Can Mark a To-Do Complete

We'll begin by implementing the functionality that allows users to mark their tasks as complete. This feature represents a core user workflow and requires careful integration between our view layer, routing system, and data persistence logic.

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

  2. Switch to your code editor where the easels project folder should be loaded and accessible.

  3. Generate a new feature test file using RSpec's built-in generators:

    rails g rspec:feature user_marks_todo_complete
  4. Open the newly generated test file and update it with the basic test structure:

    require "rails_helper"
    
    RSpec.feature "User Marks Todo Complete", type: :feature do
      scenario "successfully" do
      end
    end
  5. Since this test builds upon the authentication and todo creation workflow we've already established, we'll reuse some existing test code. Open spec > features > user_can_see_own_todos_spec.rb to access the authentication and todo creation steps we've already written and tested.

  6. Locate lines 9–19 and copy the three comment blocks along with their associated code (from # user signs in through click_on "Add To-Do"). This code represents the prerequisite steps that must occur before we can test the completion functionality.

  7. Return to user_marks_todo_complete_spec.rb and paste the copied code within the empty scenario block:

    scenario "successfully" do
    
       # user signs in
       visit new_session_path
       fill_in "Email", with: "user@example.com"
       click_on "Sign In"
    
       # user creates a to-do via a form
       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"
    
    end
  8. Now we'll add the specific test steps for the completion functionality. This involves simulating a user clicking a "Complete" button and then verifying that the application properly reflects the completed state. Add the following code below the existing steps:

    click_on "Add To-Do"
    
       # user marks the to-do complete
       click_on "Complete"
    
       # verify that the to-do is marked complete
       expect(page).to have_css ".todos li.completed", text: "paint house"
    end

    NOTE: This test assumes our application will add a completed CSS class to finished to-dos, enabling visual styling such as strikethrough text or dimmed appearance. In production applications, you might implement alternative visual indicators like checkmarks, color changes, or even moving completed items to a separate section. The specific UI treatment is a design decision that should align with your application's overall user experience strategy.

  9. Save the file and switch back to Terminal to run our new test.

  10. Execute the feature test using the following command (you can copy the filename by Ctrl-clicking or Right-clicking on the tab in most editors and selecting Copy Name):

    rspec spec/features/user_marks_todo_complete_spec.rb

    As expected, the test passes through the authentication and todo creation steps we borrowed from our previous test, but fails when Capybara attempts to locate the "Complete" button. This failure is anticipated and represents our next development target.

  11. Return to your code editor and open the todos index view: app > views > todos > index.html.erb

  12. We need to add completion toggle buttons to each todo item. These buttons should display different text and trigger different actions depending on the current completion state of each todo. Modify the todo display loop to include conditional button rendering:

    <% @todos.each do |todo| %>
       <li>
        <%= todo.title %>
       <% if todo.completed? %>
          <%= button_to "Incomplete", root_path %>
       <% else %>
          <%= button_to "Complete", root_path %>
       <% end %>
      </li>
    <% end %>

    NOTE: These buttons temporarily route to the root path since we haven't yet created the specific controller actions they'll eventually trigger. This is a common development pattern—we establish the UI elements first, then build the backend functionality to support them.

  13. Our test expects completed todos to have a specific CSS class for styling purposes. Add a conditional class attribute to the list item that applies the "completed" class when appropriate:

    <li class="<%= 'completed' if todo.completed? %>">
  14. Save your changes and return to Terminal.

  15. Re-run the test command: rspec spec/features/user_marks_todo_complete_spec.rb (use the Up Arrow key to recall the previous command)

    The test now fails at an earlier step because our view references a completed? method that doesn't exist on our Todo model. This method is essential for determining whether to display "Complete" or "Incomplete" buttons and whether to apply the "completed" CSS class.

  16. Let's examine our current database schema before adding the completion tracking field. Open db > schema.rb and review the existing fields in the todos table.

  17. We'll add a timestamp field to track when each todo was completed. Generate a migration to add this field to the Todo model:

    rails g migration AddCompletedToTodo completed_at:datetime
  18. Execute the migration to update your database schema:

    rails db:migrate

    NOTE: You can verify the migration's success by re-examining schema.rb—you should see a new completed_at datetime field in the todos table. This field will contain a timestamp when a todo is completed, or NULL when it's incomplete.

  19. Now we need to implement the completed? method in our Todo model. Open app > models > todo.rb

  20. Add the completed? method that checks for the presence of a completion timestamp:

    class Todo < ActiveRecord::Base
       def completed?
          completed_at.present?
       end
    end

    NOTE: The present? method is a Rails convenience method that returns true if the value exists and is not blank. This is more robust than simply checking for truthiness, as it properly handles nil values and empty strings.

  21. Save the file and return to Terminal.

  22. Run the test again: rspec spec/features/user_marks_todo_complete_spec.rb

    The test now progresses further but fails when attempting to POST to the root path via the "Complete" button. We need to create proper routes and controller actions to handle todo completion rather than sending the user back to the homepage.

  23. Let's establish the proper routing structure for todo completions. Open config > routes.rb

  24. We'll create nested routes that associate completion actions with specific todos. This design clearly expresses the relationship between todos and their completion states:

    root to: "todos#index"
    resources :todos do
       resource :completion, only: [:create, :destroy]
    end
    resources :sessions

    NOTE: We're using a singular resource (not resources) because completion is a state rather than a distinct model object. Singular resources don't include ID parameters in their routes, which is appropriate since we're operating on the completion state of a specific todo rather than managing separate completion records.

  25. Save the routing file and examine the routes we've created:

    rails routes

    Look for the todo_completion POST route—this will handle marking todos as complete. Note how :todo_id is included in the route pattern, allowing Rails to identify which specific todo should be marked complete.

  26. Update the "Complete" button in index.html.erb to use the proper route:

    <%= button_to "Complete", todo_completion_path(todo) %>
  27. Save the view file and test our routing improvement:

    rspec spec/features/user_marks_todo_complete_spec.rb

    Progress! The routing error is resolved, but now we need to create the CompletionsController to handle the actual completion logic.

  28. Generate the new controller:

    rails g controller completions
  29. Run the test again to confirm the controller was created:

    rspec spec/features/user_marks_todo_complete_spec.rb

    Now we see the expected "missing action" error. We need to implement the create action that will handle POST requests to mark todos as complete.

  30. Open app > controllers > completions_controller.rb and add the create action:

    class CompletionsController < ApplicationController
    
       def create
          redirect_to root_path
       end
    
    end
  31. Save and test to verify the basic action structure works:

    rspec spec/features/user_marks_todo_complete_spec.rb

    The button now functions, but our test fails on the final assertion because we're not actually marking the todo as complete. We need to implement the completion logic within our create action.

  32. Return to completions_controller.rb and enhance the create action to actually mark todos as complete:

    def create
       @todo = Todo.find(params[:todo_id])
       redirect_to root_path
    end

    This code locates the specific todo using the ID passed through our nested route. The params[:todo_id] corresponds to the route parameter we saw in our rails routes output.

  33. Complete the implementation by adding the completion timestamp and saving the record:

    @todo = Todo.find(params[:todo_id])
    @todo.completed_at = Time.current
    @todo.save
    redirect_to root_path

    NOTE: Time.current respects your application's configured time zone, making it preferable to Time.now for user-facing timestamps.

  34. Save the controller and run the complete test suite for this feature:

    rspec spec/features/user_marks_todo_complete_spec.rb

    Excellent! The test passes, confirming that users can successfully mark todos as complete. Let's verify that all our existing tests still pass:

  35. Run the entire test suite to ensure we haven't introduced any regressions:

    rspec

    With todo completion working correctly, we can now implement the inverse functionality—allowing users to mark completed todos as incomplete.

Test Structure Pattern

Feature tests follow a consistent pattern: user authentication, task creation, specific action testing, and verification. This approach ensures comprehensive coverage of user workflows.

Complete Feature Implementation

1

Generate Feature Test

Create RSpec feature test file with rails g rspec:feature command and define test scenario

2

Add Database Migration

Create completed_at datetime field with AddCompletedToTodo migration to track completion timestamps

3

Update Model Logic

Define completed? method in Todo model that checks for presence of completed_at value

4

Create Controller Actions

Build CompletionsController with create action to handle marking todos as complete

Nested Routes Strategy

Using nested routes like resources :todos do resource :completion creates logical URL structure that clearly associates completion actions with specific todo items.

Test #5: Ensuring the User Can Mark a To-Do Incomplete

The ability to revert completed todos back to incomplete status is crucial for real-world usability. Users frequently mark items complete prematurely or need to re-open tasks when additional work is discovered. This feature completes the todo lifecycle management functionality.

  1. Return to user_marks_todo_complete_spec.rb in your code editor—we'll use this as the foundation for our incomplete functionality test.

  2. Duplicate the file and name the copy user_marks_todo_incomplete_spec.rb

    TIP: Most modern editors support file duplication through the context menu. In Sublime Text, you can Ctrl-click or Right-click on the filename tab and select Duplicate.

  3. Update the feature name to reflect the new functionality being tested:

    RSpec.feature "User Marks Todo Incomplete", type: :feature do
       scenario "successfully" do
  4. The incomplete test requires that a todo first be marked complete before it can be marked incomplete. Add the step to click the "Incomplete" button after the completion step:

    # user marks the to-do complete
    click_on "Complete"
    
    # user marks the to-do incomplete
    click_on "Incomplete"
  5. Update the final assertion to verify that the todo no longer has the completed styling:

    # verify that the to-do is marked incomplete
    expect(page).to have_css ".todos li", text: "paint house"
    expect(page).not_to have_css ".todos li.completed", text: "paint house"

    These assertions confirm that the todo is still visible but no longer carries the "completed" CSS class.

  6. Save the new test file and run it to identify what functionality we need to implement:

    rspec spec/features/user_marks_todo_incomplete_spec.rb

    The test fails with a routing error for the "Incomplete" button, which is expected since we haven't yet configured its route properly.

  7. Check our available routes to find the appropriate endpoint for marking todos incomplete:

    rails routes

    The todo_completion DELETE route is what we need—deleting a completion effectively marks the todo as incomplete.

  8. Update the "Incomplete" button in app > views > todos > index.html.erb to use the correct route and HTTP method:

    <% if todo.completed? %>
       <%= button_to "Incomplete", todo_completion_path(todo), method: :delete %>
  9. Save the view and test our routing fix:

    rspec spec/features/user_marks_todo_incomplete_spec.rb

    Now we need to implement the destroy action in our CompletionsController to handle DELETE requests.

  10. Open app > controllers > completions_controller.rb and add the destroy method. Since the logic is very similar to the create method, we'll start by copying it:

  11. Copy the entire create method and paste it below the original.

  12. Modify the copy to handle removing the completion timestamp instead of adding it:

    def destroy
       @todo = Todo.find(params[:todo_id])
       @todo.completed_at = nil
       @todo.save
       redirect_to root_path
    end

    Setting completed_at to nil removes the completion timestamp, which causes our completed? method to return false.

  13. Save the controller and run the incomplete test:

    rspec spec/features/user_marks_todo_incomplete_spec.rb

    Perfect! The test passes, confirming that users can successfully toggle todos between complete and incomplete states.

  14. Run the full test suite to ensure all functionality works together harmoniously:

    rspec

    All five tests should pass, indicating that our todo application now supports the complete lifecycle of task management.

  15. To prepare for the next exercise, close individual files in your editor but keep the easels project folder open for continued development.

    You may have noticed some code duplication between our create and destroy methods in the CompletionsController. This violates the DRY (Don't Repeat Yourself) principle and represents an opportunity for refactoring. In professional development environments, we regularly revisit our code to eliminate duplication, improve readability, and enhance maintainability. The next exercise will address this technical debt while also introducing you to the completed application running in a web browser, giving you the full user experience perspective on the functionality we've built.

Complete vs Incomplete Actions

FeatureMark CompleteMark Incomplete
HTTP MethodPOSTDELETE
Controller Actioncreatedestroy
Database ChangeSet completed_at timestampSet completed_at to nil
Button VisibilityShows when incompleteShows when completed
Recommended: Use RESTful conventions where completion creation represents marking complete and completion destruction represents marking incomplete.
Code Duplication Notice

The current implementation contains duplicate code between create and destroy methods. This violates DRY principles and should be refactored in the next exercise for better maintainability.

Implementation Verification

0/4

Key Takeaways

1Test-driven development ensures feature reliability by writing tests before implementing functionality, catching issues early in the development process.
2RSpec feature tests with Capybara simulate real user interactions, providing comprehensive end-to-end testing for web application workflows.
3Database migrations allow safe schema changes by adding new fields like completed_at timestamps without losing existing data.
4Nested RESTful routes create logical URL structures that clearly associate actions with parent resources, improving API design.
5The completed? method uses Rails conventions with question mark syntax to return boolean values for conditional logic in views.
6Separate controllers for specific actions like CompletionsController promote single responsibility and cleaner code organization.
7Conditional view rendering based on model state enables dynamic user interfaces that adapt to current data conditions.
8Following RESTful conventions where POST creates completions and DELETE destroys them maintains consistent API patterns throughout the application.

RELATED ARTICLES