Saturday, August 30, 2025

Achieving 100% Test Coverage: A Milestone in Software Quality

Paul (Founder)
Software Quality
Hands holding novelty glasses with CODE and DEBUG written on lenses

On 30 August 2025, we achieved 100% test coverage—every line of application code covered by at least one automated test. This wasn't an arbitrary goal or vanity metric. It was a deliberate milestone that provides confidence for rapid iteration, prevents regressions, and signals professional engineering discipline to agencies entrusting their business operations to our platform.

For letting agencies, comprehensive testing translates to reliability. Features work as intended. Updates don't break existing functionality. Bugs are caught in development, not reported by frustrated staff during critical operations. This level of quality assurance separates enterprise-grade software from prototypes.

What 100% Coverage Means

Test coverage measures which lines of code execute during the test suite. SimpleCov generates reports showing:

Coverage Summary
----------------
Files:          142
Lines:          3,847
Relevant:       3,620
Covered:        3,620
Missed:         0
Coverage:       100.0%

Every controller action, every model method, every helper function, every background job—all covered by tests that verify correct behaviour. This doesn't guarantee bug-free code (tests can miss edge cases), but it ensures basic functionality works and provides a safety net for refactoring.

The test suite executes in under 3 minutes locally, faster in CI (parallelized across multiple workers). Fast tests enable running the suite frequently—before every commit, preventing broken code from entering the codebase.

The Test Pyramid in Practice

Our 100% coverage follows the test pyramid:

Model specs (60% of tests): Fast, focused tests covering business logic, validations, associations, and scopes. These run in milliseconds and catch the majority of bugs.

# spec/models/property_spec.rb
RSpec.describe Property do
  describe "validations" do
    it { should validate_presence_of(:reference) }
    it { should validate_uniqueness_of(:reference).scoped_to(:agency_id) }
  end

  describe "#display_name" do
    it "combines reference and headline" do
      property = build(:property, reference: "ABC123", headline: "Modern Flat")
      expect(property.display_name).to eq("ABC123 - Modern Flat")
    end
  end
end

Controller/Request specs (30%): Medium-speed tests covering HTTP requests, authentication, authorization, and response formats.

# spec/requests/properties_spec.rb
RSpec.describe "Properties" do
  context "when signed in" do
    it "allows creating properties" do
      post properties_path, params: { property: attributes_for(:property) }
      expect(response).to redirect_to(property_path(Property.last))
    end
  end

  context "when signed out" do
    it "redirects to sign in" do
      post properties_path, params: { property: attributes_for(:property) }
      expect(response).to redirect_to(new_user_session_path)
    end
  end
end

System specs (10%): Slower tests using headless Chrome, covering full user workflows including JavaScript interactions.

# spec/system/property_management_spec.rb
RSpec.describe "Property Management", js: true do
  it "allows creating and editing properties" do
    visit properties_path
    click_link "New Property"
    fill_in "Reference", with: "TEST001"
    click_button "Create Property"
    expect(page).to have_content("Property was successfully created")
  end
end

This distribution optimises for speed (most tests are fast) while ensuring comprehensive coverage (system specs catch integration issues).

SimpleCov Configuration

SimpleCov integrates with RSpec to track coverage:

# spec/spec_helper.rb
require 'simplecov'

SimpleCov.start 'rails' do
  add_filter '/spec/'          # Don't measure test code coverage
  add_filter '/config/'        # Don't measure configuration
  add_filter '/vendor/'        # Don't measure dependencies

  add_group 'Models', 'app/models'
  add_group 'Controllers', 'app/controllers'
  add_group 'Helpers', 'app/helpers'
  add_group 'Jobs', 'app/jobs'
  add_group 'Mailers', 'app/mailers'

  minimum_coverage 100         # Fail if coverage drops below 100%
end

After running tests, SimpleCov generates an HTML report (coverage/index.html) showing:

  • Overall coverage percentage
  • Per-file coverage with line-by-line highlighting (green = covered, red = missed)
  • Per-group coverage (models 100%, controllers 100%, etc.)

The minimum_coverage 100 configuration fails the test suite if coverage drops below 100%—preventing accidental regressions. If a developer adds untested code, the CI pipeline fails, prompting them to add tests before merging.

Testing Multi-Tenancy Security

Multi-tenancy introduces critical security concerns: users must not access data from other agencies. Our test suite includes comprehensive security specs:

# spec/security/multi_tenancy_security_spec.rb
RSpec.describe "Multi-Tenancy Security" do
  let(:agency_a) { create(:agency) }
  let(:agency_b) { create(:agency) }
  let(:user_a) { create(:user, agency: agency_a) }
  let(:property_b) { create(:property, agency: agency_b) }

  before { sign_in user_a }

  it "prevents accessing other agencies' properties" do
    ActsAsTenant.with_tenant(agency_a) do
      expect { Property.find(property_b.id) }.to raise_error(ActiveRecord::RecordNotFound)
    end
  end

  it "scopes queries to current agency" do
    ActsAsTenant.with_tenant(agency_a) do
      expect(Property.all).not_to include(property_b)
    end
  end
end

These tests verify that tenant isolation works at the database query level—not just authorization checks that could be bypassed.

API Security Testing

The OAuth API includes extensive security tests:

# spec/requests/api/v1/properties_spec.rb
RSpec.describe "API v1 Properties" do
  let(:agency) { create(:agency) }
  let(:user) { create(:user, agency: agency) }
  let(:application) { create(:oauth_application, owner: agency) }

  context "with valid token" do
    let(:token) { create(:oauth_access_token, application: application, resource_owner_id: user.id, scopes: 'properties_read') }

    it "returns properties" do
      create_list(:property, 3, agency: agency)
      get api_v1_properties_path, headers: { 'Authorization' => "Bearer #{token.token}" }
      expect(response).to have_http_status(:success)
      expect(JSON.parse(response.body).count).to eq(3)
    end
  end

  context "without token" do
    it "returns unauthorized" do
      get api_v1_properties_path
      expect(response).to have_http_status(:unauthorized)
    end
  end

  context "with insufficient scopes" do
    let(:token) { create(:oauth_access_token, application: application, resource_owner_id: user.id, scopes: 'tenancies_read') }

    it "returns forbidden" do
      get api_v1_properties_path, headers: { 'Authorization' => "Bearer #{token.token}" }
      expect(response).to have_http_status(:forbidden)
    end
  end
end

These tests verify:

  • Valid tokens grant access
  • Missing tokens are rejected (401)
  • Insufficient scopes are rejected (403)
  • Tokens from one agency can't access another's data

Continuous Integration: Automated Quality Gates

The CI pipeline (GitHub Actions) runs tests on every commit:

# .github/workflows/ci.yml
name: CI Pipeline

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres

    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.2.9'
          bundler-cache: true

      - name: Run RSpec
        run: bundle exec rspec --format progress

      - name: Check Coverage
        run: |
          coverage=$(ruby -r json -e "puts JSON.parse(File.read('coverage/.last_run.json'))['result']['line']")
          if (( $(echo "$coverage < 100" | bc -l) )); then
            echo "Coverage is ${coverage}%, below 100% threshold"
            exit 1
          fi

      - name: Upload Coverage Report
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

If tests fail or coverage drops below 100%, the CI pipeline fails, preventing the code from merging. This automated quality gate ensures standards remain high even during rapid iteration.

The Value of 100% Coverage

Some argue 100% coverage is excessive—diminishing returns for effort invested. Our experience disagrees:

Confidence for refactoring: We can improve code structure without fear of breaking functionality. Tests catch regressions immediately.

Faster debugging: When a test fails, the specific failing test pinpoints the issue—no guessing which code broke.

Documentation: Tests demonstrate how code is intended to be used—valuable for new developers joining the project.

Prevents regressions: Once fixed, bugs stay fixed—regression tests ensure they don't reappear.

The discipline required to achieve 100% coverage (writing tests for every method, every branch, every edge case) produces higher-quality code. Untestable code is often poorly designed; making code testable improves its architecture.

What This Enables

With 100% test coverage, we can:

  • Deploy confidently: Every deployment passes comprehensive quality checks
  • Iterate rapidly: Add features without breaking existing ones
  • Refactor fearlessly: Improve code structure with safety nets
  • Onboard quickly: New developers can change code confidently, trusting tests catch mistakes

For agencies evaluating software, test coverage should be a non-negotiable question. Software without tests is a time bomb—it works until it doesn't, and no one knows why.


Related articles: