Updated: November 26, 2025
Purpose: Document testing conventions, fixture structure, and best practices for the alumni_lookup application.
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 |
Priority Levels:
Coverage Goals:
Future Goals:
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
rails test
rails test test/models/alumni_test.rb
rails test test/models/champion_signup_test.rb:4
rails test -v
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
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
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:
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
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
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
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
The champions.bualum.co subdomain requires special setup in tests.
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
When on the champions subdomain:
new_signup_path (not new_champion_signup_path)signups_path (not champion_signups_path)The routes are scoped by subdomain constraint, so the namespace prefix is dropped.
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
...
Use fixture names (not IDs) when referencing other fixtures:
# alumni.yml
assigned_alumni:
buid: B00456789
primary_contact: admin_user # References users(:admin_user)
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 %>
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
}
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 |
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
...
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
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).
# 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
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
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
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
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
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
NoMethodError: undefined method `alumnis' for #<AlumniTest>
Fix: Use alumni(:fixture_name) not alumnis(:fixture_name).
ActiveRecord::StatementInvalid: table "X" has no columns named "Y"
Fix: Check db/schema.rb for correct column names and update fixture.
StandardError: No fixture named 'one' found for fixture set 'users'
Fix: Update test to use actual fixture names (admin_user, staff_user), not placeholders.
ActiveRecord::RecordNotUnique: Duplicate key value violates unique constraint
Fix: Ensure fixture data has unique values for indexed columns.
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
ActionController::RoutingError: No route matches
Fix: Add host! "champions.example.com" in setup for champions subdomain tests.
Controller tests should use the correct instance variable names:
@alum for singular Alumni record@alumni for collections (search results)Last updated: November 2025