CrackedRuby CrackedRuby

Overview

Unit testing validates individual components of software in isolation from the rest of the system. A unit test exercises a single method, function, or class behavior with specific inputs and verifies the output matches expectations. Unlike integration tests that verify multiple components working together, unit tests focus on the smallest testable parts of an application.

The practice emerged in the 1970s as part of structured programming methodologies, gaining widespread adoption through Extreme Programming and Test-Driven Development in the late 1990s. Kent Beck's SUnit framework for Smalltalk established patterns that influenced xUnit frameworks across languages, including Ruby's Test::Unit and RSpec.

Unit tests serve multiple purposes in software development. They provide immediate feedback during development, catching regressions when code changes. Tests document expected behavior more reliably than comments because they execute against actual code. A comprehensive test suite enables confident refactoring because breaking changes surface immediately. Teams use test coverage metrics to identify untested code paths, though high coverage percentages don't guarantee quality tests.

The relationship between production code and test code follows a ratio that varies by project. Some codebases maintain 1:1 ratios where test code equals production code volume. Others reach 2:1 or 3:1 where test code exceeds production code. The ratio matters less than whether tests effectively verify behavior and catch defects.

# Basic unit test structure
class Calculator
  def add(a, b)
    a + b
  end
end

# Test verifies single method behavior
require 'minitest/autorun'

class CalculatorTest < Minitest::Test
  def test_add_returns_sum
    calculator = Calculator.new
    result = calculator.add(2, 3)
    assert_equal 5, result
  end
end

Key Principles

Unit testing operates on several core principles that distinguish effective tests from ineffective ones. These principles guide test design and help teams build reliable test suites.

Isolation requires each test to run independently without depending on other tests or external state. Tests create their own fixtures, execute operations, and clean up afterward. A test that fails should pinpoint a specific defect in a specific unit, not indicate cascading failures from shared state. Ruby's setup and teardown methods provide hooks for establishing and destroying test fixtures.

Repeatability means tests produce identical results given identical conditions. A test that passes sometimes and fails other times indicates environmental dependencies, race conditions, or state pollution. Tests should never depend on execution order, current time, random number generation, or external services. Repeatable tests allow developers to run the suite multiple times during debugging without worrying about flaky results.

Fast execution keeps the development feedback loop tight. Unit tests should complete in milliseconds, allowing developers to run hundreds or thousands of tests frequently. Tests that take seconds indicate integration with databases, file systems, or network resources. Slow tests discourage frequent execution, reducing their value. A well-designed test suite runs the entire suite in under a minute.

Clear assertions make test failures immediately understandable. Each test should verify one logical concept, even if that requires multiple assertion statements. Assertion messages should explain what failed and why. Generic assertions like assert result provide no context when they fail. Specific assertions like assert_equal expected_status, response.status, "API should return 200 for valid requests" communicate intent.

Comprehensive coverage targets all code paths including edge cases, error conditions, and boundary values. Testing only happy paths leaves defects undetected. Tests should exercise minimum values, maximum values, empty inputs, null inputs, and invalid inputs. Ruby's dynamic nature requires testing type coercion, method missing scenarios, and module inclusion effects.

Minimal setup reduces test complexity and maintenance burden. Tests requiring dozens of lines of fixture creation often indicate design problems in production code. Objects with many dependencies signal tight coupling. Tests should construct the minimum necessary state to verify specific behavior.

No external dependencies prevents test failures from environmental changes. Tests should not connect to databases, file systems, network services, or system clocks. Ruby provides stubbing and mocking facilities to replace external dependencies with controlled test doubles. The Time.stub pattern freezes time for tests verifying time-dependent behavior.

# Demonstrating isolation principle
class UserValidatorTest < Minitest::Test
  def setup
    @validator = UserValidator.new
  end
  
  def teardown
    # Each test gets fresh validator instance
    @validator = nil
  end
  
  def test_validates_email_format
    # Isolated test - no dependency on other tests
    assert @validator.valid_email?('user@example.com')
  end
  
  def test_rejects_invalid_email
    # Runs independently regardless of previous test
    refute @validator.valid_email?('invalid')
  end
end
# Demonstrating repeatability with stubbing
class OrderProcessorTest < Minitest::Test
  def test_processes_order_at_specific_time
    # Freeze time for repeatable results
    freeze_time = Time.new(2025, 1, 15, 10, 30, 0)
    Time.stub :now, freeze_time do
      processor = OrderProcessor.new
      order = processor.create_order
      assert_equal freeze_time, order.created_at
    end
  end
end

Ruby Implementation

Ruby provides multiple testing frameworks, each with different syntaxes and philosophies. Test::Unit ships with Ruby's standard library, offering a classic xUnit-style approach. Minitest succeeded Test::Unit as the default framework, providing both xUnit and spec-style interfaces with minimal dependencies. RSpec dominates Ruby testing with its behavior-driven development syntax and extensive matcher library.

Minitest follows a straightforward class-based structure. Test classes inherit from Minitest::Test, and methods starting with test_ define test cases. Assertions use methods like assert_equal, assert_nil, assert_raises, and refute. The spec syntax offers an alternative DSL using describe and it blocks.

# Minitest classic syntax
require 'minitest/autorun'

class StringProcessorTest < Minitest::Test
  def setup
    @processor = StringProcessor.new
  end
  
  def test_upcase_converts_to_uppercase
    result = @processor.upcase('hello')
    assert_equal 'HELLO', result
  end
  
  def test_reverse_reverses_string
    result = @processor.reverse('hello')
    assert_equal 'olleh', result
  end
  
  def test_raises_on_nil_input
    assert_raises(ArgumentError) { @processor.upcase(nil) }
  end
end

RSpec structures tests using describe, context, and it blocks. The framework encourages readable test descriptions that document behavior. Expectations use expect(value).to matcher syntax with matchers like eq, be_nil, raise_error, and include. RSpec's extensive matcher library covers common testing scenarios.

# RSpec syntax
require 'rspec'

RSpec.describe StringProcessor do
  let(:processor) { StringProcessor.new }
  
  describe '#upcase' do
    it 'converts string to uppercase' do
      expect(processor.upcase('hello')).to eq('HELLO')
    end
    
    context 'with nil input' do
      it 'raises ArgumentError' do
        expect { processor.upcase(nil) }.to raise_error(ArgumentError)
      end
    end
  end
  
  describe '#reverse' do
    it 'reverses the string' do
      expect(processor.reverse('hello')).to eq('olleh')
    end
  end
end

Test doubles replace real objects with controlled substitutes during testing. Ruby's testing frameworks provide mocking, stubbing, and spying capabilities. Mocks define expected method calls and verify they occurred. Stubs replace method implementations with fixed return values. Spies record method calls for later verification.

# Mocking with Minitest
require 'minitest/mock'

class NotificationServiceTest < Minitest::Test
  def test_sends_email_on_order_completion
    email_service = Minitest::Mock.new
    email_service.expect :send, true, ['Order completed', 'user@example.com']
    
    notifier = NotificationService.new(email_service)
    notifier.notify_order_complete('user@example.com')
    
    email_service.verify # Confirms expected call occurred
  end
end

Fixtures and factories generate test data. Fixtures store static test data in YAML or JSON files, loaded before tests run. Factories create objects programmatically with default attributes, allowing customization per test. Factory Bot dominates Ruby factory patterns, replacing the older FactoryGirl.

# Factory Bot pattern
FactoryBot.define do
  factory :user do
    email { "user#{sequence(:email_sequence)}@example.com" }
    name { 'Test User' }
    role { :member }
    
    trait :admin do
      role { :admin }
    end
  end
end

# Usage in tests
class UserAuthorizationTest < Minitest::Test
  def test_admin_can_delete_users
    admin = FactoryBot.create(:user, :admin)
    target_user = FactoryBot.create(:user)
    
    service = UserService.new(admin)
    assert service.can_delete?(target_user)
  end
end

Practical Examples

Testing a data transformation class demonstrates validation logic, error handling, and edge case coverage. The example shows testing normal operation, boundary conditions, and exceptional scenarios.

class DataNormalizer
  def normalize_price(value)
    raise ArgumentError, 'Price cannot be nil' if value.nil?
    
    cleaned = value.to_s.gsub(/[$,]/, '')
    float_value = Float(cleaned)
    
    raise ArgumentError, 'Price cannot be negative' if float_value < 0
    
    (float_value * 100).round / 100.0
  end
end

class DataNormalizerTest < Minitest::Test
  def setup
    @normalizer = DataNormalizer.new
  end
  
  def test_normalizes_simple_price
    result = @normalizer.normalize_price('49.99')
    assert_equal 49.99, result
  end
  
  def test_removes_dollar_sign
    result = @normalizer.normalize_price('$49.99')
    assert_equal 49.99, result
  end
  
  def test_removes_commas
    result = @normalizer.normalize_price('1,234.56')
    assert_equal 1234.56, result
  end
  
  def test_rounds_to_two_decimals
    result = @normalizer.normalize_price('49.999')
    assert_equal 50.0, result
  end
  
  def test_handles_integer_input
    result = @normalizer.normalize_price(50)
    assert_equal 50.0, result
  end
  
  def test_handles_zero
    result = @normalizer.normalize_price(0)
    assert_equal 0.0, result
  end
  
  def test_raises_on_nil
    error = assert_raises(ArgumentError) do
      @normalizer.normalize_price(nil)
    end
    assert_equal 'Price cannot be nil', error.message
  end
  
  def test_raises_on_negative
    error = assert_raises(ArgumentError) do
      @normalizer.normalize_price(-10)
    end
    assert_equal 'Price cannot be negative', error.message
  end
  
  def test_raises_on_invalid_string
    assert_raises(ArgumentError) { @normalizer.normalize_price('invalid') }
  end
end

Testing a caching layer requires stubbing time and verifying expiration logic. The example demonstrates testing stateful behavior across multiple method calls.

class SimpleCache
  def initialize(ttl_seconds)
    @ttl_seconds = ttl_seconds
    @cache = {}
  end
  
  def get(key)
    entry = @cache[key]
    return nil unless entry
    
    if Time.now - entry[:timestamp] > @ttl_seconds
      @cache.delete(key)
      return nil
    end
    
    entry[:value]
  end
  
  def set(key, value)
    @cache[key] = { value: value, timestamp: Time.now }
  end
  
  def clear
    @cache.clear
  end
end

class SimpleCacheTest < Minitest::Test
  def test_stores_and_retrieves_value
    cache = SimpleCache.new(60)
    cache.set('key1', 'value1')
    
    assert_equal 'value1', cache.get('key1')
  end
  
  def test_returns_nil_for_missing_key
    cache = SimpleCache.new(60)
    assert_nil cache.get('nonexistent')
  end
  
  def test_expires_after_ttl
    cache = SimpleCache.new(60)
    
    Time.stub :now, Time.new(2025, 1, 1, 12, 0, 0) do
      cache.set('key1', 'value1')
    end
    
    # 61 seconds later
    Time.stub :now, Time.new(2025, 1, 1, 12, 1, 1) do
      assert_nil cache.get('key1')
    end
  end
  
  def test_does_not_expire_before_ttl
    cache = SimpleCache.new(60)
    
    Time.stub :now, Time.new(2025, 1, 1, 12, 0, 0) do
      cache.set('key1', 'value1')
    end
    
    # 59 seconds later
    Time.stub :now, Time.new(2025, 1, 1, 12, 0, 59) do
      assert_equal 'value1', cache.get('key1')
    end
  end
  
  def test_clear_removes_all_entries
    cache = SimpleCache.new(60)
    cache.set('key1', 'value1')
    cache.set('key2', 'value2')
    
    cache.clear
    
    assert_nil cache.get('key1')
    assert_nil cache.get('key2')
  end
end

Testing an API client demonstrates mocking HTTP requests and handling various response scenarios.

class WeatherClient
  def initialize(http_client)
    @http_client = http_client
  end
  
  def current_temperature(city)
    response = @http_client.get("/weather?city=#{city}")
    
    raise 'API request failed' unless response.success?
    
    data = JSON.parse(response.body)
    data['temperature']
  end
end

class WeatherClientTest < Minitest::Test
  def test_returns_temperature_for_valid_city
    http_mock = Minitest::Mock.new
    response = OpenStruct.new(
      success?: true,
      body: '{"temperature": 72}'
    )
    http_mock.expect :get, response, ['/weather?city=Seattle']
    
    client = WeatherClient.new(http_mock)
    temperature = client.current_temperature('Seattle')
    
    assert_equal 72, temperature
    http_mock.verify
  end
  
  def test_raises_on_api_failure
    http_mock = Minitest::Mock.new
    response = OpenStruct.new(success?: false)
    http_mock.expect :get, response, ['/weather?city=Seattle']
    
    client = WeatherClient.new(http_mock)
    
    assert_raises(RuntimeError, 'API request failed') do
      client.current_temperature('Seattle')
    end
  end
end

Common Patterns

Arrange-Act-Assert structures each test into three distinct phases. The arrange phase sets up test fixtures and preconditions. The act phase executes the behavior under test. The assert phase verifies the outcome. This pattern makes tests readable and maintainable by clearly separating setup, execution, and verification.

def test_calculates_discount
  # Arrange
  cart = ShoppingCart.new
  cart.add_item(Product.new(price: 100))
  calculator = DiscountCalculator.new
  
  # Act
  discount = calculator.calculate(cart, coupon: '10PERCENT')
  
  # Assert
  assert_equal 10.0, discount
end

One assertion per test encourages focused tests that verify single behaviors. Multiple unrelated assertions in one test make failures ambiguous. When the first assertion fails, subsequent assertions never execute, hiding additional defects. Tests verifying multiple aspects of the same operation can include multiple assertions that all relate to the same behavior.

# Unfocused test - multiple unrelated assertions
def test_user_operations
  user = User.create(email: 'test@example.com')
  assert user.valid?
  assert_equal 'member', user.role
  assert user.can_login?
  refute user.admin?
end

# Focused tests - one behavior each
def test_new_user_is_valid
  user = User.create(email: 'test@example.com')
  assert user.valid?
end

def test_new_user_has_member_role
  user = User.create(email: 'test@example.com')
  assert_equal 'member', user.role
end

def test_new_user_can_login
  user = User.create(email: 'test@example.com')
  assert user.can_login?
end

Test builders create complex objects with fluent interfaces that improve test readability. Builders provide default values and chainable methods for customization, reducing test setup verbosity.

class OrderBuilder
  def initialize
    @items = []
    @shipping_address = default_address
    @payment_method = default_payment
  end
  
  def with_item(product, quantity = 1)
    @items << { product: product, quantity: quantity }
    self
  end
  
  def to(address)
    @shipping_address = address
    self
  end
  
  def paid_with(payment_method)
    @payment_method = payment_method
    self
  end
  
  def build
    Order.new(
      items: @items,
      shipping_address: @shipping_address,
      payment_method: @payment_method
    )
  end
  
  private
  
  def default_address
    Address.new(street: '123 Test St', city: 'Testville')
  end
  
  def default_payment
    PaymentMethod.new(type: :credit_card)
  end
end

# Usage
def test_applies_free_shipping_over_100
  order = OrderBuilder.new
    .with_item(Product.new(price: 120))
    .to(Address.new(state: 'WA'))
    .build
  
  assert order.free_shipping?
end

Parameterized tests verify behavior across multiple input values without duplicating test code. Ruby supports parameterized testing through data-driven approaches.

class ValidationTest < Minitest::Test
  def test_validates_email_formats
    valid_emails = [
      'user@example.com',
      'user.name@example.com',
      'user+tag@example.co.uk',
      'user_123@example.com'
    ]
    
    valid_emails.each do |email|
      assert EmailValidator.valid?(email), "Expected #{email} to be valid"
    end
  end
  
  def test_rejects_invalid_email_formats
    invalid_emails = [
      'invalid',
      '@example.com',
      'user@',
      'user @example.com',
      'user@.com'
    ]
    
    invalid_emails.each do |email|
      refute EmailValidator.valid?(email), "Expected #{email} to be invalid"
    end
  end
end

Custom assertions encapsulate complex verification logic into reusable methods. Custom assertions improve test readability and reduce duplication when the same verification pattern appears across multiple tests.

module CustomAssertions
  def assert_valid_json(string, message = nil)
    JSON.parse(string)
    assert true
  rescue JSON::ParserError
    flunk message || "Expected valid JSON but got: #{string}"
  end
  
  def assert_includes_hash(collection, expected_hash, message = nil)
    found = collection.any? { |item| expected_hash.all? { |k, v| item[k] == v } }
    assert found, message || "Expected collection to include hash #{expected_hash}"
  end
end

class ApiResponseTest < Minitest::Test
  include CustomAssertions
  
  def test_returns_valid_json_response
    response = api_client.fetch_data
    assert_valid_json response.body
  end
  
  def test_includes_expected_record
    results = api_client.search(query: 'test')
    assert_includes_hash results, { id: 123, name: 'Test Item' }
  end
end

Tools & Ecosystem

Minitest ships with Ruby and requires no additional dependencies. The framework provides both classical xUnit-style tests and spec-style tests through minitest/spec. Minitest runs faster than alternatives due to its minimal feature set. The framework includes basic mocking through Minitest::Mock and stubbing through the stub method. Minitest's simplicity makes it ideal for projects avoiding external dependencies.

RSpec offers the most comprehensive testing framework in the Ruby ecosystem. The matcher library includes dozens of built-in matchers for common assertions. RSpec's let and subject helpers define memoized values that improve test performance and readability. The framework supports sophisticated mocking through the rspec-mocks library. RSpec's configuration system allows global test settings and metadata-driven test filtering.

# RSpec advanced features
RSpec.describe PaymentProcessor do
  let(:processor) { described_class.new }
  subject(:payment) { processor.charge(amount, card) }
  
  describe '#charge' do
    let(:amount) { 50.00 }
    let(:card) { instance_double('CreditCard', valid?: true) }
    
    it 'processes the payment' do
      expect(payment).to be_successful
    end
    
    context 'with invalid card' do
      let(:card) { instance_double('CreditCard', valid?: false) }
      
      it 'raises an error' do
        expect { payment }.to raise_error(PaymentError)
      end
    end
  end
end

SimpleCov measures test coverage by tracking which lines execute during test runs. The gem generates HTML reports showing covered and uncovered code. Coverage reports identify untested code paths but don't guarantee test quality. High coverage percentages can exist alongside poor tests that execute code without meaningful assertions.

# SimpleCov configuration
require 'simplecov'

SimpleCov.start do
  add_filter '/test/'
  add_filter '/spec/'
  add_group 'Models', 'app/models'
  add_group 'Controllers', 'app/controllers'
  add_group 'Services', 'app/services'
  
  minimum_coverage 90
end

FactoryBot creates test objects with default attributes and customization options. Factories define associations, sequences, and traits for common variations. The gem reduces test setup code and maintains consistency across test data. FactoryBot strategies include build for unsaved objects, create for persisted objects, and build_stubbed for objects with IDs but no database interaction.

VCR records HTTP interactions during test runs and replays them in subsequent runs. The gem eliminates external service dependencies while maintaining realistic response data. VCR cassettes store request-response pairs in YAML files. Tests run faster with VCR because they avoid network latency. Configuration options control when cassettes record new interactions versus reusing existing ones.

# VCR configuration
require 'vcr'

VCR.configure do |config|
  config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
  config.hook_into :webmock
  config.configure_rspec_metadata!
  config.filter_sensitive_data('<API_KEY>') { ENV['API_KEY'] }
end

# Usage in tests
RSpec.describe WeatherService, vcr: true do
  it 'fetches current conditions' do
    weather = WeatherService.current('Seattle')
    expect(weather.temperature).to be_a(Numeric)
  end
end

WebMock stubs HTTP requests in tests without recording real responses. The gem blocks all external HTTP connections by default, forcing explicit stubs. WebMock supports request matching by URL patterns, headers, and body content. Stubbed responses define status codes, headers, and body content.

require 'webmock/minitest'

class ApiClientTest < Minitest::Test
  def test_handles_rate_limiting
    stub_request(:get, 'https://api.example.com/data')
      .to_return(status: 429, body: 'Rate limit exceeded')
    
    client = ApiClient.new
    assert_raises(RateLimitError) { client.fetch_data }
  end
end

Timecop controls time and date during tests by freezing, traveling, or scaling time. The gem prevents time-dependent test failures and makes temporal testing deterministic. Timecop methods include freeze for stopping time, travel for jumping to specific moments, and return for resetting to real time.

Common Pitfalls

Testing implementation instead of behavior couples tests to internal structure rather than external contracts. Tests that verify private method calls or internal state break when refactoring changes implementation without altering behavior. Tests should verify inputs and outputs, not the steps between them.

# Brittle test - tests implementation
def test_sends_notification_through_queue
  notifier = Notifier.new
  queue = notifier.instance_variable_get(:@queue)
  
  notifier.send_alert('message')
  
  assert_equal 1, queue.length # Assumes internal queue structure
end

# Better test - tests behavior
def test_delivers_notification
  notifier = Notifier.new
  email_spy = EmailSpy.new
  
  notifier.send_alert('message', to: email_spy)
  
  assert email_spy.received?('message')
end

Shared state between tests causes intermittent failures when test execution order changes. Tests that modify class variables, global state, or singleton objects affect subsequent tests. Ruby's class instance variables persist across test methods unless explicitly reset.

# Problem - shared state
class UserCounter
  @count = 0
  
  def self.increment
    @count += 1
  end
  
  def self.total
    @count
  end
end

class UserCounterTest < Minitest::Test
  def test_increments_count
    UserCounter.increment
    assert_equal 1, UserCounter.total # Fails if other tests ran first
  end
end

# Solution - reset state in setup
class UserCounterTest < Minitest::Test
  def setup
    UserCounter.instance_variable_set(:@count, 0)
  end
  
  def test_increments_count
    UserCounter.increment
    assert_equal 1, UserCounter.total
  end
end

Testing multiple concepts in one test makes failures ambiguous. When a test verifies unrelated behaviors and fails, developers cannot determine which behavior broke without reading the entire test. Focused tests communicate intent and simplify debugging.

Overusing mocks creates brittle tests that break from implementation changes. Every mock introduces coupling to internal structure. Tests with excessive mocking often indicate design problems in production code. Preferring real objects over mocks when possible produces more resilient tests.

Insufficient edge case coverage leaves defects undetected until production. Tests focusing on happy paths miss boundary conditions, null inputs, empty collections, and error scenarios. Ruby's dynamic typing increases the importance of testing type coercion and duck typing failures.

# Incomplete test coverage
def test_finds_user_by_email
  user = User.create(email: 'test@example.com')
  found = User.find_by_email('test@example.com')
  assert_equal user, found
end

# Better coverage - includes edge cases
def test_finds_user_by_email_case_insensitive
  user = User.create(email: 'test@example.com')
  found = User.find_by_email('TEST@example.com')
  assert_equal user, found
end

def test_returns_nil_when_user_not_found
  found = User.find_by_email('nonexistent@example.com')
  assert_nil found
end

def test_handles_nil_email
  assert_nil User.find_by_email(nil)
end

def test_handles_empty_email
  assert_nil User.find_by_email('')
end

Slow test suites discourage frequent test execution. Tests that interact with databases, file systems, or external services accumulate latency. Running tests before every commit becomes impractical when the suite takes minutes. Identifying and isolating slow tests preserves rapid feedback cycles.

Unclear test names obscure test intent and make failures harder to diagnose. Test names should describe the scenario and expected outcome. Names like test_user or test_validation provide no context. Descriptive names like test_rejects_duplicate_email_addresses communicate purpose.

# Poor test names
def test_user
  # What aspect of user?
end

def test_validation
  # Which validation?
end

# Better test names
def test_saves_user_with_valid_attributes
end

def test_rejects_user_without_email
end

def test_enforces_unique_email_addresses
end

Reference

Core Testing Concepts

Concept Definition Application
Unit Smallest testable component Single method or class
Test Case Individual test verifying specific behavior One test method
Test Suite Collection of related test cases All tests for a class or module
Fixture Test data or objects in known state Setup data for tests
Assertion Statement verifying expected outcome assert_equal, expect().to eq()
Mock Object simulating behavior with expectations Verify method calls occurred
Stub Object returning predetermined values Replace dependencies
Spy Object recording interactions for verification Track method calls

Minitest Assertions

Assertion Purpose Example
assert Verify truthy value assert user.valid?
refute Verify falsy value refute user.admin?
assert_equal Verify equality assert_equal 5, result
assert_nil Verify nil value assert_nil user.deleted_at
assert_empty Verify empty collection assert_empty errors
assert_includes Verify collection membership assert_includes list, item
assert_raises Verify exception raised assert_raises(ArgumentError) { method }
assert_silent Verify no output to stdout/stderr assert_silent { method }
assert_in_delta Verify float within tolerance assert_in_delta 1.0, result, 0.01
assert_match Verify regex match assert_match /pattern/, string

RSpec Matchers

Matcher Purpose Example
eq Verify equality expect(result).to eq(5)
be Verify object identity expect(obj).to be(same_obj)
be_nil Verify nil expect(value).to be_nil
be_empty Verify empty collection expect(list).to be_empty
include Verify collection membership expect(list).to include(item)
match Verify regex match expect(string).to match(/pattern/)
raise_error Verify exception expect { method }.to raise_error(ArgumentError)
change Verify state change expect { method }.to change { counter }.by(1)
be_between Verify numeric range expect(value).to be_between(1, 10)
respond_to Verify method exists expect(obj).to respond_to(:method_name)

Test Organization Checklist

Task Implementation Purpose
Arrange setup code Use setup/before hooks Initialize test fixtures
Execute one behavior Single method call in act phase Clear test focus
Verify outcomes Assertions in assert phase Confirm expected results
Clean up resources Use teardown/after hooks Prevent state pollution
Name tests descriptively test_verb_noun_condition format Document intent
Test edge cases Boundary values, nil, empty Comprehensive coverage
Isolate dependencies Use mocks and stubs Fast, reliable tests
Keep tests fast Avoid I/O operations Encourage frequent execution

Common Test Smells

Smell Description Solution
Long setup Many lines of fixture creation Simplify production code design
Multiple assertions Testing unrelated behaviors Split into focused tests
Conditional logic If statements in tests Create separate test cases
Test interdependence Tests fail when run in isolation Use setup/teardown properly
Slow execution Tests take seconds Remove external dependencies
Brittle tests Break from refactoring Test behavior not implementation
Mystery guest Unclear fixture sources Make test data explicit
Obscure test Unclear what is being verified Improve naming and structure