Why and What to Test
Automated tests exist to answer one question fast: did this change break something? In a system that decides whether a teaching candidate has met subject matter requirements, "we clicked around and it looked fine" is not an acceptable answer. But testing effort is a budget, and spending it well matters more than spending a lot of it.
The test pyramid, MVC edition
The test pyramid is the classic effort model: many fast unit tests at the base, fewer integration tests in the middle, a thin layer of end-to-end/UI tests on top. Mapped onto a layered MVC app:
- Unit tests target your service layer and business rules β pure C# with dependencies mocked. Milliseconds each, hundreds of them, run on every build.
- Integration tests exercise real seams: repositories against a real SQL Server (or SQLite), the MVC pipeline via a test host. Slower, dozens of them.
- UI tests (Selenium/Playwright driving a browser) cover a handful of critical journeys β login, submit, approve. Expensive and brittle; keep this layer thin on purpose.
The pyramid's point is economics: when a unit test fails you know which rule broke; when a UI test fails you know only that something broke, and you'll spend an hour finding out what.
Business rules first
Your highest-value tests sit where the domain logic lives. "A candidate meets the subject matter requirement when every SMR domain is satisfied by qualifying coursework or a passing exam score" β that's a service-layer rule with edge cases (quarter-unit conversion, expired exam scores, partial domain coverage) that will regress during maintenance. Each edge case is one cheap unit test. Aim your effort at code that is (a) decision-heavy and (b) likely to change β for this system, the evaluation/matching rules, status transitions, and anything computing units or dates.
What NOT to unit test
Just as important, because bad tests are a maintenance tax forever:
- Framework code. Don't test that
[Required]makes a field required or that EF saves a row β Microsoft tested that. Test your rules about the data. - Thin controllers. If an action just calls a service and returns a view, a test of it mostly re-asserts the mock setup. (Controllers with real logic are a design smell β move the logic out, then test it.)
- Private methods. Test through the public surface; if a private method begs for its own tests, it's asking to become its own class.
- AutoMapper profiles, property-by-property. One
configuration.AssertConfigurationIsValid()test catches unmapped members; per-property assertions just restate the profile. - Getters, setters, and DTOs. No logic, no test.
The next lessons build the mechanics: unit tests with mocks, then data access and controllers, then test data and CI.