Should I test that?

Should I test controllers?

Verdict

Yes

Give each controller endpoint one HTTP-level test that sends a request and checks the status code and the response body; do not unit test controller methods by calling them with a mocked service.

Why

Test controllers minimally: give each endpoint one test that sends a request through routing, binding and JSON serialization. In the typical case, a REST controller that calls one service method and returns JSON, Blast radius is users and Change frequency is regularly, because a broken endpoint breaks a client screen and endpoints change with most features. Detectability is same-day and Reversibility is with-effort, because error alerts report a broken endpoint within hours and a wrongly bound write field saves records that need repair. Test cost is moderate, because an HTTP-level test starts the framework's web layer and changes with the response shape.

When the decision changes
WhenDecisionWhy
The endpoint charges a card or issues a refundTest mandatory: the main path, every known failure and the boundary amountsBlast radius rises to money and Reversibility to costly
The controller decides whether the caller may read or change a recordTest mandatory, including requests that must be deniedBlast radius rises to safety-or-legal and Detectability to never, because a missing check returns data without an error
Installed app versions you cannot update call the endpointTest minimally: one test per endpoint that asserts the field names old app versions readDetectability moves to eventually, because only users on old versions see a break, and Change frequency falls to rarely, because those fields change only with a new API version
The controller method calculates a value itself, such as a shipping estimateTest the calculation, after moving it into a service classDetectability moves to eventually, because a wrong estimate looks plausible
The endpoint passes several request fields of one type to the service, such as a start date and an end dateTest: send a distinct value in each field and assert each value reaches the right service parameterDetectability moves to eventually, because a swapped field returns a plausible result of the right type
An end-to-end test on every CI run calls the endpoint with a distinct value in each fieldDo not add a separate controller test for the endpointDetectability drops to immediately and Reversibility to trivial, because a broken route, binding or field mapping fails CI before merge
Testing the web layer needs the application server, a database and a message brokerDo not test the controller; watch the endpoint's error rate after each deployTest cost rises to heavy while Detectability stays same-day

What breaks if you don't test

A controller breaks where service tests cannot see it. A typo in a route, a Spring parameter without @RequestBody, or a renamed JSON property compiles and passes every service test. The result is a 404, a request object with every field null, or a response without the field the client reads. Error alerts show the failure the same day, and a write endpoint leaves records with empty fields to repair.

What you lose if you over-test

The common over-test calls the controller method with a mocked service and asserts it returned what the mock returned. That test never touches the route, the binding or the serializer, so it stays green when they break and turns red when you rename a service method. A test per validation message fails whenever someone rewords one.

How to test

Write one test per endpoint at the HTTP level: MockMvc in Spring, WebApplicationFactory in ASP.NET Core, integration tests in Rails, or supertest for Express. Send the request a client sends and assert the status code and the body fields it reads. Mock the service when it has its own tests. Skip error branches until a bug in one reaches a client, then add a regression test for it.

When the answer changes

  • The endpoint moves money or decides who may see a record.
  • The endpoint passes two request fields of one type, such as two account IDs, to the service.
  • The controller method calculates values itself.

Real incident + Code example

The controller tests that never produced JSON

On a Spring Boot backend I worked on, controller tests called methods directly with a mocked service. A developer set spring.jackson.property-naming-strategy=SNAKE_CASE for a new partner integration, and deliveryDate became delivery_date in every response, including the ones the Android app read. All controller tests stayed green because none of them produced JSON, and the app showed empty delivery dates until support tickets arrived the next morning. We scoped the naming to the partner endpoints and replaced the unit tests with one MockMvc test per endpoint:

@WebMvcTest(OrderController.class)
class OrderControllerTest {
    @Autowired MockMvc mvc;
    @MockitoBean OrderService orders;

    @Test
    void getOrderReturnsFieldsTheAppReads() throws Exception {
        when(orders.find(42L))
            .thenReturn(new Order(42L, LocalDate.of(2026, 9, 20)));

        mvc.perform(get("/orders/42"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.deliveryDate").value("2026-09-20"));
    }
}

The test serializes the response with the production Jackson configuration, so a global naming change fails it in CI.

FAQ

Should I unit test controllers?

No, test controllers through the framework at the HTTP level instead of calling their methods directly. A unit test with a mocked service skips routing, request binding and JSON serialization, where controllers break.

Should I test my controllers (MVC)?

Yes, give each MVC controller endpoint one test that sends a request and checks the status code and the response the client reads. Add more tests for endpoints that move money or check permissions.

Should controller tests mock the service layer?

Yes, mock the service in a controller test when the service has its own unit tests. When no service test exists, use the real service and a test database, so one test covers both layers.

Is unit testing skinny controllers necessary?

No, a skinny controller that passes the request to a service needs no unit test, only one HTTP-level test of its main path. A unit test of a skinny controller repeats its one line of delegation and passes when the route or JSON mapping breaks.