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:
