MarkUs: Testing
PR #7668 — Achieving full test coverage for CriteriaController.
Context
CriteriaController manages grading rubrics: creating, updating, reordering, and deleting criteria that TAs use to evaluate student submissions. The controller had significant untested code paths, particularly around authorization, edge cases in reordering logic, and interactions with released results.
Test Strategy
RSpec controller tests for Rails follow a pattern: set up state, issue a request, assert on the response and side effects.
describe CriteriaController do
let(:assignment) { create(:assignment) }
let(:criterion) { create(:rubric_criterion, assignment: assignment) }
describe "#update" do
context "when results have been released" do
let!(:released_result) { create(:released_result, assignment: assignment) }
it "rejects the update with 409 Conflict" do
patch :update, params: { id: criterion.id, criterion: { max_mark: 10 } }
expect(response).to have_http_status(:conflict)
end
end
end
endFactory design. Created a released_result factory that composes existing factories (submission, result, grouping) with the released_to_students: true flag. Factory composition is preferable to fixtures: factories express the minimal state needed for each test, making dependencies explicit.
Coverage Patterns
| Path | Test Type | What It Verifies |
|---|---|---|
| Happy path (CRUD) | Request spec | Correct response codes, record creation/update/deletion |
| Authorization | Request spec | Role-based access (student, TA, instructor) |
| Concurrency guard | Request spec | Reject mutations after results are released |
| Reordering | Unit test | Criteria position updates maintain contiguous ordering |
| Validation | Model test | Reject invalid max_mark, duplicate names |
Authorization testing. Each endpoint is tested with three roles: students (should be denied), TAs (limited access), and instructors (full access). This ensures the authorize! calls in the controller correctly delegate to the policy layer.
Concurrency guard. Once grading results are released to students, modifying the rubric would invalidate existing grades. The controller checks for released results and returns HTTP 409 (Conflict) if modifications are attempted. This is an application-level optimistic concurrency check.
Debugging Approach
Several tests initially failed due to incorrect assumptions about MarkUs’s data model. The systematic approach:
- Reproduce. Run the failing test in isolation (
rspec spec/controllers/criteria_controller_spec.rb:42) - Inspect. Add
binding.pryat the controller action entry point, examine the request params and database state - Trace. Follow the code path from controller action through authorization, model mutation, and response rendering
- Fix. Correct the factory setup or assertion to match the actual behavior
The most common failure mode: factories creating records with default associations that don’t match the test’s intended state. The fix is always to make factory usage more explicit, specifying the exact relationships needed.
Lessons
Test coverage as documentation. Each test describes a contract: “when X is true, the system does Y.” For a controller used by thousands of students and hundreds of TAs, these contracts prevent silent regressions during maintenance.
Focused PRs. This PR touched only test files and factory definitions. Zero production code changes. This made review straightforward: the reviewer could evaluate test quality without also evaluating behavioral changes.