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

Easels App with Test Driven Development: Part 2

Master Test Driven Development with Ruby on Rails

Building on Previous Knowledge

This tutorial builds directly on Exercise 13A. If you haven't completed the previous exercise, follow the setup instructions to get your environment ready.

Topics Covered in This Ruby on Rails Tutorial:

Setting up a Test That Builds on Previous Features, Test #3: Ensuring the User Only Sees Their Own To-dos

Key Learning Objectives

User-Specific Data Access

Learn to implement user authentication and ensure users only see their own to-do items through proper database associations.

Test-Driven Development

Continue building features using TDD methodology, writing tests first and implementing functionality to pass those tests.

Database Migrations

Add new fields to existing database tables using Rails migrations and update your models accordingly.

Exercise Overview

In the previous exercise, you built the Easels task management application using test-driven development principles. Now that users can view the homepage and add to-do tasks to their list, we need to implement a critical security feature: ensuring users can only view their own tasks. This exercise demonstrates how to implement user authentication and data isolation—fundamental concepts in any production web application.

  1. If you completed the previous exercises, you can skip the following sidebar. We strongly recommend finishing the previous exercise (13A) before proceeding, as this builds directly on that foundation. If you haven't completed it, follow the setup instructions in the sidebar below.

    Tutorial Prerequisites

    1

    Complete Previous Exercise

    Finish Exercise 13A which covers basic to-do creation functionality and homepage setup

    2

    Understand TDD Workflow

    Be familiar with the red-green-refactor cycle of test-driven development

    3

    Rails Basics

    Have working knowledge of Rails controllers, views, and basic routing concepts

If You Did Not Do the Previous Exercise (13A)

  1. Close any files you may have open.
  2. Open the Finder and navigate to Class Files > yourname-Rails Class
  3. Open Terminal.
  4. Type cd and a single space (do NOT press Return yet).
  5. Drag the yourname-Rails Class folder from the Finder to the Terminal window and press ENTER.
  6. Run rm -rf easels to delete your existing copy of the easels site.
  7. Run Git clone https://bitbucket.org/Noble Desktop/easels.Git to copy the easels Git repository.
  8. Type cd easels to enter the new directory.
  9. Type Git checkout 13A to bring the site up to the end of the previous exercise.
  10. Run bundle to install any necessary gems.
  11. Run yarn install—check-files to install JavaScript dependencies.

Setup Checklist for New Users

0/6

Setting up a Test That Builds on Previous Features

Now we'll create a comprehensive test that verifies user data isolation. This pattern of building incremental features through testing is essential for maintaining code quality in complex applications.

  1. Ensure Terminal is open and navigate to your project directory. If you aren't in the easels directory, type: cd easels

  2. Generate a new feature test specification: rails g rspec:feature user_can_see_own_todos

  3. In the newly created file (spec > features > user_can_see_own_todos_spec), update the content as shown:

    require "rails_helper"
    
    RSpec.feature "User Can See Own Todos", type: :feature do
       scenario "successfully" do
       end
    end
  4. Before writing new test code, let's leverage existing functionality. We need to verify that users can create to-dos, save them to the database, and see appropriate CSS styling—functionality we've already tested. To maintain efficiency, we'll reuse proven test steps from our previous specification. Open: spec > features > user_creates_todo_spec.rb

  5. Around lines 9–17, copy the following three comment blocks and their associated code:

    • #user creates a to-do via a form
    • #user saves the to-do to the database
    • #verify that the page has our to-do
  6. Return to user_can_see_own_todos_spec.rb and paste the code into the empty scenario block:

    scenario "successfully" do
    
       #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"
    
       #verify that the page has our to-do
       expect(page).to have_css ".todos li", text: "paint house"
    
    end
  7. To verify proper user isolation, we need the database to associate tasks with specific users. Let's implement a basic authentication system using email addresses. Above the existing code, add the following test setup that creates competing data:

    #create different user's to-do in background
    Todo.create(title: "buy milk", email: "somebody@example.com")
    
    #user signs in
    visit root_path
    fill_in "Email", with: "user@example.com"
    click_on "Sign In"
    
    #user creates a to-do via a form

    This test strategy creates two simultaneous to-dos: while our authenticated user adds their "paint house" task, we create a background "buy milk" to-do owned by a different user. Our test will verify that users only see their own data—a critical security requirement in any multi-tenant application.

  8. Now we need to verify data isolation by ensuring the logged-in user sees only their own to-dos. Modify the final verification step as shown in bold:

    #verify that the page has the signed in user's to-do, but not the to-do created in the background
    expect(page).to have_css ".todos li", text: "paint house"
    expect(page).not_to have_css ".todos li", text: "buy milk"
  9. Save the file and switch to Terminal to begin our test-driven development cycle.

Code Reuse in Testing

Copying test steps from previous features saves development time and ensures consistency. The user_creates_todo_spec.rb file contains valuable patterns we can reuse for testing user-specific functionality.

Database Migration Process

1

Generate Migration File

Use rails g migration AddEmailToTodo email:string to create a new migration that adds an email field

2

Run Database Migration

Execute rails db:migrate to apply the changes to your database schema

3

Verify Schema Changes

Check db/schema.rb to confirm the new email field was added to the todos table

Test #3: Ensuring the User Only Sees Their Own To-Dos

This test implements a fundamental security pattern: user data isolation. In production applications, this principle prevents users from accessing unauthorized data and maintains privacy compliance.

  1. Execute the new feature test (tip: in Sublime Text, CTRL–click or Right–click on the filename and choose Copy Name for quick copying):

    rspec spec/features/user_can_see_own_todos_spec.rb

    The test fails immediately when attempting to create the dummy "buy milk" to-do. Rails doesn't recognize the Todo model's 'email' attribute because we haven't defined it in our database schema yet.

  2. In your code editor, examine the current database structure by opening db > schema.rb. This file provides a snapshot of your database's current state and is automatically maintained by Rails migrations.

  3. Notice around line 17 that our todos table currently contains only a "title" field. We need to add an email field to associate to-dos with users. Remember: never edit schema.rb directly—always use migrations to modify database structure.

  4. Generate a migration to add the email field to our todos table:

    rails g migration AddEmailToTodo email:string

    This migration follows Rails naming conventions and will automatically generate the appropriate database modification code.

  5. Apply the migration to update your database structure: rails db:migrate

    Pro tip: Refresh schema.rb to see the newly added "email" field. Migrations are the backbone of database versioning in Rails applications—they ensure all team members and deployment environments maintain identical database structures.

  6. Re-run the test: bundle exec rspec spec/features/user_can_see_own_todos_spec.rb (Remember: use the Up Arrow key to recall previous commands—a time-saving habit for any developer.)

    Progress! The absence of "buy milk" errors indicates successful background to-do creation. Now Capybara, our browser simulator, attempts to sign in but can't locate the "Email" field. This suggests our current view lacks authentication functionality.

  7. Examine the current test in your code editor (user_can_see_own_todos_spec.rb). Notice that around line 9, under the #user signs in comment, the test navigates to root_path for authentication.

  8. Review our current homepage by opening: app > views > todos > index.html.erb

    Our homepage currently displays an unordered list of all to-dos plus an "add new" link—no authentication form exists. Rather than cluttering the main interface with login functionality, we should implement proper authentication flow through dedicated routes.

  9. Open config > routes.rb to configure our authentication system.

    We'll create a sessions controller to handle user authentication—a standard Rails pattern for managing user sessions without requiring a full user model initially.

  10. Add resourceful routes for session management by inserting the bold code around line 5:

    root to: "todos#index"
    resources :todos
    resources :sessions
  11. Save the file and return to Terminal.

  12. Examine your new routing options: rails routes

    You now have session-specific paths alongside your existing todo routes. The new_session path will serve our sign-in form, providing a clean separation of concerns between authentication and core functionality.

  13. Return to user_can_see_own_todos_spec.rb in your code editor and update the authentication path:

  14. Around line 10, modify the test to use the proper authentication route:

    #user signs in
    visit new_session_path
    fill_in "Email", with: "user@example.com"
  15. Save and test your changes: rspec spec/features/user_can_see_own_todos_spec.rb

    As expected, we encounter a routing error—the SessionsController doesn't exist yet. This is a typical TDD cycle: test, fail, implement, repeat.

  16. Generate the sessions controller: rails g controller sessions

  17. Run the test again: rspec spec/features/user_can_see_own_todos_spec.rb

    Another familiar error pattern: our controller exists but lacks the required action method. In Rails MVC architecture, controllers need explicit action methods to handle specific routes.

  18. In sessions_controller.rb, add the new action method:

    class SessionsController < ApplicationController
    
       def new
       end
    
    end
  19. Save and test: bundle exec rspec spec/features/user_can_see_own_todos_spec.rb

    Now we have a MissingTemplate error—Rails needs a corresponding view file to render the authentication form.

  20. Create a new file in the app > views > sessions folder called: new.html.erb

  21. Anticipating the next test failure, let's implement the complete authentication form:

    <%= form_with url: '/sessions', method: :post do |f| %>
       <%= f.label :email %>
       <%= f.text_field :email %>
       <%= f.submit "Sign In" %>
    <% end %>
  22. Understanding this form structure is crucial:

    • We use form_with url: instead of form_with model: because we're not persisting session data to a database model.
    • No @session instance variable is needed—we'll store authentication state in the browser session, not the database. This approach works well for simple applications while maintaining security.
  23. Save and test: bundle exec rspec spec/features/user_can_see_own_todos_spec.rb

    Excellent progress! Now the test fails when clicking "Sign In" because our SessionsController lacks a create action to process the form submission.

  24. Add the create action to sessions_controller.rb:

    def new
    end
    
    def create
    end
  25. Test again: bundle exec rspec spec/features/user_can_see_own_todos_spec.rb

    We're getting a missing template error for sessions/create. However, create actions typically don't render views—they process data and redirect. Let's implement proper session handling.

  26. Update the create action in sessions_controller.rb with session management logic:

    def create
       session[:current_email] = params[:email]
       redirect_to root_path and return
    end

    This code leverages Rails' built-in session object—a secure, encrypted storage mechanism that maintains user state across requests. The session[:current_email] key stores the authenticated user's email, making it available throughout the application lifecycle. This pattern provides the foundation for more sophisticated authentication systems.

  27. Test the authentication flow: bundle exec rspec spec/features/user_can_see_own_todos_spec.rb

    Success! The authentication system now works. However, our test fails at the final verification step: both the user's "paint house" task and the background "buy milk" task appear on the page. We need to implement data filtering based on the authenticated user.

  28. Open the todos controller: app > controllers > todos_controller.rb

  29. The current index action retrieves all to-dos from the database without filtering. Update it to scope results by the authenticated user's email:

    def index
       @todos = Todo.where(email: @current_email)
    end

    This change implements data isolation at the controller level—a critical security pattern that ensures users only access their own data.

  30. We need to make the current user's email available across all controllers. Open: app > controllers > application_controller.rb

  31. Add a before_action callback to set the current user context:

    class ApplicationController < ActionController::Base
      before_action :set_current_email
    
      private
        def set_current_email
          @current_email = session[:current_email]
        end
    end

    The before_action callback ensures every controller action has access to the current user's email. This pattern centralizes user context management and provides a foundation for implementing authorization across your entire application.

  32. Test your data isolation: bundle exec rspec spec/features/user_can_see_own_todos_spec.rb

    The test still fails, but now it can't find the authenticated user's "paint house" task. The issue is that when users create new to-dos, they aren't being associated with the current user's email address.

  33. Return to todos_controller.rb and examine the create method.

  34. Update the to-do creation logic to associate new tasks with the authenticated user (around line 16):

    #@todo = Todo.new(todo_params)
    @todo.email = @current_email
    @todo.save

    This change replaces Todo.create with a two-step process: Todo.new creates the object, then we explicitly set the email before saving. This approach prevents malicious users from submitting arbitrary email addresses through form manipulation—a security principle known as mass assignment protection.

  35. Run your complete test: bundle exec rspec spec/features/user_can_see_own_todos_spec.rb

    Excellent! The test passes completely. Users can now sign in, create to-dos, and view only their own tasks. The background "buy milk" task remains hidden, demonstrating proper data isolation.

  36. Verify all tests still pass: rake

    Running your full test suite regularly ensures new features don't break existing functionality—a cornerstone of reliable software development.

  37. Prepare for the next exercise by closing individual files but keeping the easels folder open in your code editor.

    Note that our current implementation has some limitations: visiting the root path without authentication may show no tasks, and the system could theoretically create tasks without email addresses. We'll address these edge cases in subsequent exercises to create a more robust, production-ready application.

    In the next exercise, we'll extend our test suite with two additional features: marking to-dos as complete and marking them as incomplete. These features will demonstrate state management and further solidify our TDD approach.

Security Through User Association

The test creates two to-dos with different email addresses to verify that users can only see their own tasks. This is crucial for data privacy and security in multi-user applications.

Key Implementation Components

Sessions Controller

Handles user sign-in functionality and manages session state. Creates new and create actions for login form processing.

Database Associations

Links to-dos with user email addresses through the Todo model. Filters displayed tasks based on current session.

Route Configuration

Adds session routes and updates application flow to require login before accessing to-do functionality.

We replaced Todo.create with Todo.new because create creates the new Todo and saves it all in one step, but now we need to modify it before saving.
This demonstrates the importance of understanding Rails method differences when implementing security features.
Test-Driven Success

After implementing all required components (migration, controller, views, and session management), the test passes and users can only see their own to-dos while background tasks remain hidden.

Key Takeaways

1Test-driven development guides feature implementation by writing failing tests first, then building the minimum code necessary to make them pass
2Database migrations in Rails allow you to safely add new fields to existing tables while maintaining data integrity and version control
3User authentication can be implemented through session management, storing user identifiers in browser sessions for simple applications
4Proper data filtering at the controller level ensures users only access their own records, implementing basic security through database queries
5Rails resourceful routing automatically generates standard CRUD paths for controllers, simplifying URL structure and form handling
6Strong parameters in Rails controllers prevent security vulnerabilities by explicitly controlling which form data can be mass-assigned to models
7The Rails session object provides a secure way to store user-specific data across HTTP requests without requiring database storage
8Code reuse in testing reduces development time and maintains consistency, especially when building features that extend existing functionality

RELATED ARTICLES