MVP — Model View Presenter

The Untestable Screen

I inherited an Android-style codebase (this was a React Native project with no state management) where the component file for the order screen was 800 lines long. The component fetched data directly, managed loading and error state, applied business rules, and rendered JSX. There was no way to test any of the business logic without mounting the whole component with a mocked network.

MVP solves exactly this. The Presenter contains all logic and is a plain class — no DOM, no component lifecycle, no framework dependencies. It is easy to test. The View is made deliberately passive: it has no decision-making power; it only calls the presenter when something happens and renders what the presenter tells it to.

Table of Contents


The Three Roles

┌──────────────────────────────────────────────────────────┐
│                          View                            │
│  - Renders what it is told                               │
│  - Calls presenter methods on user events                │
│  - NO business logic, NO data fetching                   │
└──────────────┬───────────────────┬───────────────────────┘
               │ events            │ render commands
               ▼                   ▲
┌──────────────────────────────────────────────────────────┐
│                        Presenter                         │
│  - One plain class (no framework dependencies)           │
│  - Handles events from view                              │
│  - Calls model / service                                 │
│  - Calls view methods to update display                  │
└──────────────────────────┬───────────────────────────────┘
                           │ data requests

┌──────────────────────────────────────────────────────────┐
│                          Model                           │
│  - Data, domain rules, persistence                       │
│  - No knowledge of UI                                    │
└──────────────────────────────────────────────────────────┘

The critical difference from MVC: the Presenter holds a reference to the View interface, not vice versa. The View knows the Presenter exists (it calls presenter methods), but the Presenter only depends on a View interface — making it mockable.


Passive View Contract

The view implements an interface that the presenter drives:

The presenter calls methods on this interface. In production, the React component implements it. In tests, a mock class implements it. The presenter never changes — only the view implementation swaps.


How MVP Differs from MVC

Aspect
MVC
MVP

View updates

Controller picks view after action

Presenter explicitly calls view methods

View has model reference?

Often yes

No — view only knows presenter

Presenter/Controller testability

Needs HTTP context

Presenter is a plain class, fully unit-testable

View logic

May contain some

None — view is passive

Two-way communication

Controller → View (indirect)

Presenter ↔ View (via interface)


TypeScript Example: Order Summary Screen


Testing the Presenter Without a DOM

Because the presenter depends only on the OrderListView interface, tests use a plain mock without any rendering library:

No mounting, no DOM, no network. All business logic tested in milliseconds.


Python Example: CLI Presenter

MVP works equally well in terminal applications where the "view" is stdout:


When to Use MVP

MVP is a good fit when:

  • Presenter logic needs to be unit-tested without a rendering framework

  • The view is likely to change (switching from React to React Native, CLI to web) while the logic stays the same

  • Working with inexperienced contributors — the passive view constraint prevents logic from sneaking back in

Consider MVVM instead when:

  • The framework supports reactive data bindings natively (React, Vue, SwiftUI)

  • The team is more comfortable with observables than with explicit presenter-to-view method calls


Lessons Learned

  • The View interface is the contract. Keep it small — one method per distinct visual state change. Fat view interfaces signal that the presenter is doing too much.

  • Presenters should have no framework imports. If a react import appears in a presenter file, the abstraction has leaked.

  • One presenter per screen, not per component. Sharing a presenter across components introduces hidden coupling between UI parts.

Last updated