MVC doesn’t lend itself well to unit testing

Every beginning iOS developer learns to build their first app using MVC because that’s the way it is taught in guides and tutorials. But as the app evolves out of a prototype, there are more and more new features added and bugs discovered. Over time, all code is just added to the view controller because it’s convenient. You don’t need to create a new file, choose which folder/group to put the file in, design new classes and models, think about how they interact. Putting all code in the view controller becomes a bad habit. And we all know how difficult it is to change habits. As a result, massive view controllers are born.

A typical massive view controller has the following things:

  • View lifecycle events such as viewDidLoad(), viewDidAppear(_:), …etc.
  • Table view data source and delegate methods
  • IBOutlet variables to change UIView subclasses attributes
  • IBAction methods to handle user driven touch events
  • More if your view controller also deals with maps, notifications, …etc.
  • And any business logic you leave in the vie controller such as Core Data and networking

In the end, a massive view controller has everything in it. When the view controller gets to this kind of complexity, the beginning iOS developers may start looking into writing unit tests, or doing TDD for the app. That’s where the problem is. Their view controllers are already hooked up to a breathing tube. They are trying to write tests for untestable code. It ain’t gonna work. Tests are hard to write and they are fragile. So they finally give up! Going back to adding even more to the view controller.

So, what can you do about it?

Architecture comes before testing. Forget about writing unit tests for untestable code. I guarantee you’ll waste time and energy. You’ll just end up hating everything about testing and TDD. And then declare that’s just the way software development is.

But it doesn’t have to be like this.

In the Clean Swift architecture, the subject under test can be viewed as a black box, independent of any other component. There are many of these black box components, and they don’t depend on one another directly. In order to decouple the dependence between the components in an architecture, you add a protocol between them. This results in code independence.

Component A doesn’t interact with component B directly. A doesn’t own a direct reference to B. Instead, A has an indirect reference to B through protocol P. A invokes methods declared in P. B conforms to P by implementing its methods. That’s why both arrows point toward P. Components A and B don’t know anything about each other. They communicate by protocol P. Both A and B depend on P.

In Clean Swift, code independence between the VIP components is achieved by defining the business, presentation, and display logic protocols. These protocols are represented by the blue circles.

But code independence alone is not enough. Many developers pass the same entity models around throughout the entire app. For example, you may have a User model to represent a logged in user. Your app may display the user’s profile in one screen. Another screen may display the post feed. The settings may allow the user to change his password and notification settings. If you pass the same User object around and making changes, many places will be out of sync.

This leads us to data independence. Dedicated data models are contained in structs and passed between the components at the boundaries. Different boundaries have different data models. The entity models such as User stay in the business logic layer and never spill out to the view controller.

When component A sends a message to component B, it also passes any required data in the data model X. If model X needs to change, only components A and B need to change to accommodate. Model Y stays intact, so component C doesn’t need to change. This means your unit tests for C are not being fragile.

This is an excerpt from my new Effective Unit Testing book:

Now, if you want the change in X to ultimately affect the behavior coming out of C, you’ll probably need to change Y and C at some point. However, the change in X won’t break the tests for C. This is the key to having C’s unit tests not being fragile. You can then unit test each black box independently. When you’re testing one black box, you can stub out other black boxes. Instead of having every single test that uses X scream failure at you, you only need to update the tests for A and B in order to get back to green. When the time comes to make the change to Y, then and only then, you update the tests for C. Therefore, the tests for C are not fragile. They still pass when you’re making changes at an unrelated boundary.

In the Clean Swift architecture, data independence is provided by defining the request, response, and view models at the boundaries between the view controller, interactor, and presenter components.

I recorded over 4.5 hours of video to show you, step-by-step, how I re-architect this Apple sample project from MVC to Clean Swift. I then followed up by writing unit tests to cover every feature in the app. The end result is a beautiful, clean, decoupled app that is also easily testable.

Want to watch every single step I took to convert an existing MVC project to Clean Swift and add unit tests to cover all the features? There are two ways to get it.

I've been developing in iOS since the iPhone debuted, jumped on Swift when it was announced. Writing well-tested apps with a clean architecture has been my goal.

Leave a Comment

Your email address will not be published.