Making a Legacy UIViewController Testable
refactoring legacy UIKit for testability.
extract logic into a testable view model, inject dependencies, keep the controller a thin humble object.
rewriting everything at once or testing through the UIKit lifecycle directly.
WHAT THIS TESTS This examines your ability to tame a Massive View Controller and your judgment about safe, incremental change rather than risky rewrites.
A GOOD ANSWER COVERS Start by writing characterization tests at the boundary you can reach, so you capture existing behavior before changing anything. Then incrementally extract logic. Apply MVVM by moving presentation and business logic into a view model that imports no UIKit, exposes plain properties and intent methods, and is therefore trivially unit-testable. Use dependency injection: replace hard-coded singletons and network calls with protocols passed into the view model, so tests substitute mocks. The Humble Object pattern guides the split, leave only thin, hard-to-test glue in the controller that forwards user input to the view model and binds the view model's state back to views, while all decision logic lives in the testable object.
COMMON WRONG ANSWERS Proposing a single big-bang rewrite, which is high-risk with no safety net. Trying to unit test logic that still lives inside viewDidLoad by spinning up the controller and faking the lifecycle. Leaving the view model coupled to UIKit or to concrete singletons, which defeats the isolation.
LIKELY FOLLOW-UPS How do you inject dependencies into a controller instantiated by a Storyboard? What is the difference between MVVM and MVP here? How do you test navigation that the controller still owns? When is snapshot testing more appropriate than unit testing the view model?
ONE CONCRETE EXAMPLE A 900-line ProfileViewController mixes networking, formatting, and validation. You add tests around its observable outputs, then create a ProfileViewModel that takes a ProfileServiceProtocol in its initializer and exposes displayName, isSaveEnabled, and a save() method. The controller is reduced to wiring text fields to view model inputs and observing its outputs to update labels. Now validation and save logic are tested in isolation with a mock service, and the controller is a thin humble shell.
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.