alumni_lookup

Testing Guide

Updated: November 26, 2025
Purpose: Document testing conventions, fixture structure, and best practices for the alumni_lookup application.


Table of Contents

  1. Test Suite Summary
  2. Running Tests
  3. Fixture Structure
  4. Test Categories with Examples
  5. Testing Subdomains
  6. Fixture Conventions
  7. Writing New Tests
  8. Common Gotchas

Test Suite Summary

Current Status: 241 tests, 573 assertions, 0 failures

Category Tests Files
Models 106 8 files
Services 37 3 files
Controllers 95 10 files
Mailers 3 1 file
Total 241 22 files

Test Coverage & Priorities

Priority Levels:

Coverage Goals:

Future Goals:

Test Organization

test/
├── models/
│   ├── alumni_test.rb              # 16 tests
│   ├── alumni_affinity_test.rb     # 14 tests
│   ├── affinity_test.rb            # 8 tests
│   ├── champion_signup_test.rb     # 28 tests
│   ├── college_test.rb             # 6 tests
│   ├── degree_test.rb              # 14 tests
│   ├── major_test.rb               # 9 tests
│   └── user_test.rb                # 11 tests
├── services/
│   ├── engagement_score_calculator_test.rb  # 17 tests
│   ├── top_engaged_alumni_service_test.rb   # 8 tests
│   └── champion_signup_merger_test.rb       # 12 tests
├── controllers/
│   ├── alumni_controller_test.rb                    # 18 tests
│   ├── alumni_affinities_controller_test.rb         # 17 tests
│   ├── users_controller_test.rb                     # 15 tests
│   ├── sessions_controller_test.rb                  # 10 tests
│   ├── champion_signups_controller_test.rb          # 3 tests
│   ├── champion_signups_management_controller_test.rb # 5 tests
│   └── settings/
│       ├── majors_controller_test.rb            # 8 tests
│       ├── affinities_controller_test.rb        # 8 tests
│       └── colleges_controller_test.rb          # 8 tests
├── mailers/
│   ├── champion_signup_mailer_test.rb        # 2 tests
│   └── user_mailer_test.rb                   # 2 tests (existence)
└── fixtures/
    ├── affinities.yml
    ├── alumni.yml                  # Note: NOT alumnis.yml (Latin plural)
    ├── alumni_affinities.yml
    ├── champion_signups.yml
    ├── colleges.yml
    ├── degrees.yml
    ├── engagement_activities.yml
    ├── engagement_types.yml
    ├── majors.yml
    └── users.yml

Running Tests

Full Test Suite

rails test

Specific Test File

rails test test/models/alumni_test.rb

Specific Test

rails test test/models/champion_signup_test.rb:4

Test with Verbose Output

rails test -v

Fixture Structure

Directory Layout

test/fixtures/
├── affinities.yml          # Alumni groups/organizations
├── alumni.yml              # Core alumni records (NOT alumnis.yml)
├── alumni_affinities.yml   # Join table: alumni ↔ affinities
├── champion_signups.yml    # External signup submissions
├── colleges.yml            # Academic colleges
├── degrees.yml             # Alumni degrees
├── engagement_activities.yml # Engagement tracking data
├── engagement_types.yml    # Activity type definitions
├── majors.yml              # Academic majors
├── users.yml               # Internal application users
└── files/                  # File attachments for tests

Alumni/Alum Terminology

Important: The fixture file is alumni.yml (not alumnis.yml). The inflection rule makes “alumni” the same in singular and plural.

# Correct fixture accessor
alumni(:john_doe)      # ✅ Correct
alumnis(:john_doe)     # ❌ Will fail - file doesn't exist

Fixture Naming Convention

Use descriptive snake_case names that indicate the purpose of the fixture:

# Good - descriptive names
john_doe:                    # Named alumni
completed_linked:            # Status + state
admin_user:                  # Role-based name
accounting:                  # Domain-specific name

# Avoid - generic names
one:
two:
test1:

Test Categories with Examples

Model Tests

Located in test/models/. Test validations, scopes, associations, and instance methods.

Example: Alumni Model Test (test/models/alumni_test.rb)

require "test_helper"

class AlumniTest < ActiveSupport::TestCase
  # ===========================================
  # Fixture Validation
  # ===========================================
  
  test "valid alumni fixtures load correctly" do
    john = alumni(:john_doe)
    assert john.valid?, "john_doe fixture should be valid"
    assert_equal "B00123456", john.buid
    assert_equal "John", john.first_name
    assert_equal "Doe", john.last_name
  end

  # ===========================================
  # Contact ID Validation
  # ===========================================
  
  test "contact_id with valid format is accepted" do
    alum = alumni(:john_doe)
    alum.contact_id = "C-123456789"
    assert alum.valid?, "Valid contact_id format should be accepted"
  end

  test "contact_id with invalid format is rejected" do
    alum = alumni(:john_doe)
    
    invalid_formats = [
      "123456789",       # Missing C- prefix
      "C123456789",      # Missing hyphen
      "C-12345678",      # Too few digits
    ]
    
    invalid_formats.each do |invalid_id|
      alum.contact_id = invalid_id
      assert_not alum.valid?, "Contact ID '#{invalid_id}' should be invalid"
    end
  end
end

Controller Tests

Located in test/controllers/. Test HTTP responses, authentication, and redirects.

Example: Alumni Controller Test (test/controllers/alumni_controller_test.rb)

require "test_helper"

class AlumniControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:admin_user)
    @alum = alumni(:john_doe)
  end

  # ============================================================================
  # Authentication Tests
  # ============================================================================

  test "should redirect unauthenticated users away from search" do
    get alumni_search_path
    assert_response :redirect
  end

  # ============================================================================
  # Search Tests
  # ============================================================================

  test "should get search page when authenticated" do
    sign_in @user
    get alumni_search_path
    assert_response :success
  end

  test "should search alumni by name" do
    sign_in @user
    get alumni_search_path, params: { name: "John" }
    assert_response :success
  end

  # ============================================================================
  # Show Tests
  # ============================================================================

  test "should show alumni with valid id" do
    sign_in @user
    get alumni_path(@alum.id)
    assert_response :success
  end
end

Service Tests

Located in test/services/. Test business logic and calculations.

Example: Engagement Score Calculator Test (test/services/engagement_score_calculator_test.rb)

require "test_helper"

class EngagementScoreCalculatorTest < ActiveSupport::TestCase
  # ===========================================
  # Basic Score Calculation
  # ===========================================
  
  test "score returns 0 for alumni with no activities" do
    alum = Alumni.create!(buid: "B00EMPTY01", first_name: "Empty", last_name: "User")
    calculator = EngagementScoreCalculator.new(alum)
    
    assert_equal 0, calculator.score
  end

  test "score calculates correctly for level 1 activities" do
    john = alumni(:john_doe)
    calculator = EngagementScoreCalculator.new(john)
    score = calculator.score
    
    assert score > 0, "Score should be positive for engaged alumni"
    assert_kind_of Numeric, score
  end

  # ===========================================
  # Activity Caps
  # ===========================================
  
  test "email_click activities are capped at 5" do
    alum = Alumni.create!(buid: "B00CAPTEST", first_name: "Cap", last_name: "Test")
    
    # Create 10 email_click activities (cap is 5)
    10.times do |i|
      EngagementActivity.create!(
        buid: alum.buid,
        activity_code: "email_click",
        description: "Email click #{i + 1}",
        engagement_date: i.days.ago.to_date
      )
    end
    
    calculator = EngagementScoreCalculator.new(alum)
    
    # With cap of 5 at 1 point each, max score from email_clicks is 5
    assert_equal 5, calculator.score, "email_click should be capped at 5 activities"
  end
end

Mailer Tests

Located in test/mailers/. Test email content and delivery.

Example: Champion Signup Mailer Test (test/mailers/champion_signup_mailer_test.rb)

require "test_helper"

class ChampionSignupMailerTest < ActionMailer::TestCase
  test "welcome_email content" do
    signup = champion_signups(:completed_linked)
    email = ChampionSignupMailer.welcome_email(signup)
    
    assert_equal [signup.email], email.to
    assert_match /champion/i, email.subject
    
    # Handle multipart emails
    body = email.text_part&.body&.to_s || email.html_part&.body&.to_s || email.body.to_s
    assert_match signup.first_name, body
  end
end

Testing Subdomains

The champions.bualum.co subdomain requires special setup in tests.

Setting the Host

class ChampionSignupsControllerTest < ActionDispatch::IntegrationTest
  setup do
    # REQUIRED: Set subdomain for route matching
    host! "champions.example.com"
  end

  test "should get new signup page" do
    get new_signup_path  # NOT new_champion_signup_path
    assert_response :success
  end

  test "should create signup with first step" do
    post signups_path, params: {
      step: 'info',
      champion_signup: {
        first_name: "Test",
        last_name: "User",
        email: "test@example.com"
      }
    }
    assert_response :redirect
  end
end

Route Helper Names

When on the champions subdomain:

The routes are scoped by subdomain constraint, so the namespace prefix is dropped.


Fixture Conventions

1. Primary Keys and Foreign Keys

This application uses custom primary keys for several relationships:

Model Primary Key Notes
Alumni buid Format: B00123456
Affinity affinity_code Short uppercase codes
Major major_code Short uppercase codes
College college_code Short uppercase codes

Example relationships:

# alumni.yml
john_doe:
  buid: B00123456
  first_name: John
  ...

# degrees.yml - references alumni by buid
john_doe_bs:
  buid: B00123456        # Must match alumni buid
  major_code: ACCT       # Must match major fixture
  ...

2. Fixture References

Use fixture names (not IDs) when referencing other fixtures:

# alumni.yml
assigned_alumni:
  buid: B00456789
  primary_contact: admin_user   # References users(:admin_user)

3. ERB for Dynamic Values

Use ERB for timestamps and calculations:

# users.yml
admin_user:
  encrypted_password: <%= Devise::Encryptor.digest(User, 'password123') %>
  last_login: <%= Time.current %>
  datetime_added: <%= 1.year.ago %>

# degrees.yml
john_doe_bs:
  degree_date: <%= Date.new(2020, 5, 9) %>

# champion_signups.yml
completed_linked:
  created_at: <%= 1.month.ago %>

4. Enum Values

Use the integer value for enums in fixtures:

# champion_signups.yml
completed_linked:
  status: 5                           # Integer for status enum (zip_code = 5)
  selected_role: Connection Advisor   # String for role field

Check model definitions for enum mappings:

# ChampionSignup model
enum status: {
  started: 1,
  completed_questions: 2,
  selected_role: 3,
  interests: 4,
  zip_code: 5
}

5. Unique Constraints

Be aware of unique indexes when creating fixtures:

Table Unique Columns
alumni buid, contact_id
affinities affinity_code
alumni_affinities (buid, affinity_code) composite
users email
majors major_code

6. Fixture Dependencies

When creating test data, ensure dependencies exist in order:

# 1. First create colleges
business:
  college_code: BUS
  ...

# 2. Then majors (references college)
accounting:
  major_code: ACCT
  college_code: BUS   # Must match college fixture
  ...

# 3. Then alumni
john_doe:
  buid: B00123456
  ...

# 4. Finally degrees (references both)
john_doe_bs:
  buid: B00123456     # Must match alumni
  major_code: ACCT    # Must match major
  ...

Settings Controller Tests

The Settings::* controllers require admin access (access_level: 1). Tests verify both authentication and authorization.

Example: Settings::MajorsControllerTest (test/controllers/settings/majors_controller_test.rb)

require "test_helper"

class Settings::MajorsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @admin_user = users(:admin_user)      # access_level: 1 (admin)
    @regular_user = users(:staff_user)    # access_level: 2 (non-admin)
    @major = majors(:accounting)
    @college = colleges(:business)
  end

  # Authentication Tests
  test "should redirect to login when not authenticated" do
    get settings_majors_path
    assert_redirected_to unauthenticated_root_path
  end

  test "should deny access to non-admin users" do
    sign_in @regular_user
    get settings_majors_path
    assert_redirected_to root_path
  end

  test "should allow admin access to index" do
    sign_in @admin_user
    get settings_majors_path
    assert_response :success
  end

  # CRUD Tests
  test "should create major" do
    sign_in @admin_user
    assert_difference("Major.count", 1) do
      post settings_majors_path, params: {
        major: {
          major_code: "TEST",
          major_desc: "Test Major",
          college_code: @college.college_code,
          dept: "TST",
          dept_desc: "Test Department",
          active: true
        }
      }
    end
    assert_redirected_to settings_majors_path
  end
end

User Fixture Access Levels

The user fixtures have different access levels for testing authorization:

# test/fixtures/users.yml

# access_level values:
#   1 = Admin (full settings access)
#   2 = Staff (standard access, no settings)

admin_user:
  access_level: 1    # Can access Settings::* controllers
  admin: true

staff_user:
  access_level: 2    # Cannot access Settings::* controllers
  admin: false

Important: When testing admin-only features, use users(:admin_user). When testing that non-admins are blocked, use users(:staff_user).


Writing New Tests

Accessing Fixtures

# In any test - use the correct accessor name
@alum = alumni(:john_doe)           # ✅ Correct
@user = users(:admin_user)
@signup = champion_signups(:completed_linked)

# ❌ WRONG - will fail
@alum = alumnis(:john_doe)          # No such accessor

Authentication in Controller Tests

class MyControllerTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:admin_user)
    sign_in @user  # Devise test helper
  end
  
  test "authenticated access" do
    get some_path
    assert_response :success
  end
end

Testing CRUD Operations

test "admin can create user" do
  sign_in users(:admin_user)
  
  assert_difference("User.count") do
    post users_path, params: {
      user: {
        name: "New User",
        email: "newuser@example.com"
      }
    }
  end
  
  assert_redirected_to users_path
end

test "can update alumni affinity" do
  sign_in users(:admin_user)
  affinity = alumni_affinities(:john_doe_ambassador)
  
  patch alumni_affinity_path(alumni(:john_doe), affinity), params: {
    alumni_affinity: { role: "Senior Member" }
  }
  
  assert_redirected_to alumni_path(alumni(:john_doe))
  affinity.reload
  assert_equal "Senior Member", affinity.role
end

Creating Test Data in Tests

When fixtures aren’t sufficient:

test "creating new signup" do
  signup = ChampionSignup.create!(
    first_name: "Test",
    last_name: "User",
    email: "unique#{Time.now.to_i}@example.com",  # Ensure uniqueness
    graduation_year: "2020"
  )
  assert signup.persisted?
end

Testing with Time

test "recent signups scope" do
  travel_to Time.zone.local(2025, 11, 26) do
    recent = ChampionSignup.recent(30)
    assert_includes recent, champion_signups(:completed_linked)
  end
end

Testing Assertions on Collections

test "has_many association" do
  john = alumni(:john_doe)
  
  assert_respond_to john, :degrees
  assert_kind_of ActiveRecord::Associations::CollectionProxy, john.degrees
  assert john.degrees.any?, "John should have degrees"
end

Common Gotchas

1. Wrong Fixture Accessor Name

NoMethodError: undefined method `alumnis' for #<AlumniTest>

Fix: Use alumni(:fixture_name) not alumnis(:fixture_name).

2. Fixture Column Mismatch

ActiveRecord::StatementInvalid: table "X" has no columns named "Y"

Fix: Check db/schema.rb for correct column names and update fixture.

3. Missing Fixture References

StandardError: No fixture named 'one' found for fixture set 'users'

Fix: Update test to use actual fixture names (admin_user, staff_user), not placeholders.

4. Unique Constraint Violations

ActiveRecord::RecordNotUnique: Duplicate key value violates unique constraint

Fix: Ensure fixture data has unique values for indexed columns.

5. Multipart Email Bodies

For emails with both HTML and text parts:

# ❌ May fail if email has multiple parts
body = email.body.to_s

# ✅ Handle multipart correctly
body = email.text_part&.body&.to_s || email.html_part&.body&.to_s || email.body.to_s

6. Subdomain Routes Not Matching

ActionController::RoutingError: No route matches

Fix: Add host! "champions.example.com" in setup for champions subdomain tests.

7. Instance Variable Naming

Controller tests should use the correct instance variable names:



Last updated: November 2025