Unit Testing Model View Controller on iOS with Swift

Ali Aga
11 min readDec 12, 2018

--

As part of my first software engineering job at Motorola, I was told to investigate MVC design pattern in Apache Struts, and that was way back in 2004. Since then, this design pattern has become common on many platforms and frameworks and is oftentimes touted on the resumes of software engineers. Despite all of this, I have often seen MVC incorrectly implemented, leading to poor design decisions, poor testability, and ultimately poor quality of delivered products and limited code re-usability. Maybe it is because there is limited time to truly understand this pattern, given the urgency to produce functioning software rather quickly. So, my goal here is to share with you techniques on designing your MVC Swift App the correct way and making it testable without requiring you to download and use third party libraries. We will not need any fancy tool to make your unit tests work and get unit test coverage. We will solely rely on the native iOS platform and good design principles.

So first things first …

1. Create a Single View App Using Xcode …

Swift Single App View Project Setup

So this is straightforward, just open Xcode, click “Create a new Xcode Project”, select “iOS”, and “Single View App”. Then click “Next”. I named my project “Account” but you can call it whatever you want. Select the appropriate team from the drop-down box. Then, uncheck “Include UI Tests”, and “Use Core Data”. We are interested in Unit tests, so keep “Include Unit Tests” checked. Then, click “Next”.

At this point, we have boilerplate code that we can start building upon.

Let’s take a second to note all of the files that Xcode has created for us. These include an AppDelegate, a Main Storyboard, a ViewController under the Account Group Folder, and an AccountTests file that has some boilerplate unit test cases.

Our ViewController has the following code:

What is missing here is a view, a model, and the logic in the controller. So let’s add that in. But before we do that let’s also understand what we should not do: we should not create multiple data-type references in the view controller directly and think of that as our model.

We really instead should have the following in our design —

1. One model-type reference in the ViewController

2. If needed, the model should allow for a composition of smaller data types and relevant business rules.

3. The model-type reference in the ViewController should be a model protocol.

Anti-pattern Example — Using data type(s) directly instead of a unified model protocol
Best Practice Example — Using a unified model protocol in the ViewController that allows for composition of a model.

The model needs to be created for use in the ViewController. For this, you MUST create a protocol for the Model before you create the implementation of the model. This step will allow us to decouple the controller from the actual implementation. In our case, our controller should only be concerned about depositing and withdrawing cash from our Account Model. Let’s provide that in the form of a protocol.

Protocol example: account model

3. Implement a Concrete Model

Once you have thought through the protocol that you want the controller to interface with, we can compose a concrete class that implements that protocol. In our case, we create a data type of Double to hold the account balance and a function to withdraw, deposit, and return the balance.

Implementation of protocol for account model example.

4. Write the Model Unit Test Case

At this point, we can easily write a unit test case as we have separated out our business rules and data from our controller. So let’s create a unit test file under the AccountTests folder and name it AccountModelTest.swift. See the unit test file I created listed below. I am testing the model function transact via the unit test testTransact.

Unit test for testing model.

Great! We have implemented a unit test case that will test the model. At this point, I would encourage you to build and run your unit test case. You can easily start the unit test run by clicking the run symbol (▶) in the line number column next to the class declaration line.

Next, we will test the ViewController, but before we do that we will need to setup the ViewController with a View and a Model. So let’s tackle the View next.

5. Design your view using a simple protocol.

Before we bust out the InterfaceBuilder to build a view, let’s take a minute to understand the user interface we want and jot it down in a protocol. I named mine AccountViewProtocol. This protocol mainly outlines a very passive view where we set a balance value to assign to a textview somewhere, obtain values from input fields, and set a controller to pass on events to.

How and where we use this protocol is key to understanding the MVC pattern and will help ease testing. So let’s take a minute to understand why this step is important.

Anti-pattern Example — Using multiple views directly in your controller rather than a single view protocol that allows for composition of the user interface.
Best Practice Example — Using a unified view protocol in the ViewController that allows for composition of a view.

Okay, so following that general idea in the example above we will use composite view protocol in our controller.

6. Compose the View Controller with Protocols

In the ViewController class, add the AccountViewProtocol as accountView variable, and also add the AccountViewModel as accountModel variable.

class ViewController: UIViewController {    var accountView: AccountViewProtocol?
var accountModel: AccountModelProtocol?
// ...
}

7. Add Setter Methods To View Controller For Injecting Dependencies

Exposing the setter methods for the accountView and accountModel will allow us to inject values that can be mocked. This is especially needed when we will have our unit test for the controller to run independently of the user interface. When testing, we can inject the values using the setter functions.

// Use this method to inject the view dependency if we need to.
// Method can be for testing mock view.
func setAccountView(_ aView :AccountViewProtocol){
accountView = aView
accountView?.setController(controller: self)
}


// Use this method to inject the model dependency
func setAccountModel(_ aModel: AccountModelProtocol){
accountModel = aModel
}

8. Set Default Values for Protocols in ViewController

When the views are loaded, we can setup the default values for the protocol variable references — that is if they are not setup already. This will come in handy when we are not testing, and running the app normally.

    override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupModel()
}


fileprivate func setupView() {
// Do any additional setup after loading the view,
// typically from a nib.
if let aView = self.view as? AccountViewProtocol{
if (accountView == nil) {
setAccountView(aView)
}
}
}

fileprivate func setupModel() {
// If model is not injected, inject a default one here
if accountModel == nil {
setAccountModel(AccountModel())
}
}

9. Add ViewController logic for presentation

Ok so far we have had no logic in our controller, but we need to have some processing performed in our controller. We will create function for processing transaction requests. This method will get deposit and withdrawal amounts values in textual format and then convert it to a numeric format before passing it on to the model, which in turn returns the balance upon processing. The balance is then formatted to a currency format from a numeric format. Then the formatted balance value is displayed on the view.

   func processTransactionRequest(){
let depositString = accountView?.getDepositValue()
let withdrawalString = accountView?.getWithdrawalValue()

let deposit = getValue(depositString)
let withdrawal = getValue(withdrawalString)
let balance = accountModel?.transact(deposit: deposit, withdraw: withdrawal)
accountView?.setBalanceValue(balanceAmount: String(format:"$%.02f", balance ?? 0))
}


func getValue(_ text: String?)->Double{
if let text = text{
return Double(text) ?? 0
}
return 0
}

In summary, here are the entire contents of the ViewController.swift file.

10. Add the unit test case for the View Controller

We are ready to add another unit test file that will specifically test controller logic. Let’s create ViewControllerTest.swift file in the AccountTest folder. We will create a class called ViewControllerTest as shown below.

import XCTest
@testable import Account
class ViewControllerTest: XCTestCase {

var controller: ViewController = ViewController()
override func setUp(){
controller.setAccountModel(AccountModel())
}

override func tearDown() {
}

func testProcessTransactionRequest(){

}

}

We created an empty placeholder called testProcessTransactionRequest to test our controller logic in ViewController.processTransactionRequest().

11. Create a Mock View Class

We want to test the ViewController in isolation without the need for the actual view. For that to happen we will mock a view and inject that value in our view controller. We can mock the intended view by just implementing the AccountViewProtocol, and you can simply add that class at the end of the ViewControllerTest. We can easily make this mock behavior as returning 10 for withdrawal value, and 11 for deposit value. The idea being that the controller will extract these values via the protocol as inputs to the processing it needs to do. After the result is processed by the controller, the controller will set the value on the MockView via the setBalanceValue() method. See below.

class ViewControllerTest: XCTestCase {

let controller: ViewController = ViewController()
let mockView: MockView = MockView()
// ...
}
class MockView:AccountViewProtocol{

var balance: String?

func getWithdrawalValue() -> String {
return "10"
}

func getDepositValue() -> String {
return "11"
}

func setBalanceValue(balanceAmount: String) {
balance = balanceAmount
}

}

12. Inject the MockView in the View Controller

Now let’s make the unit test functional by injecting the mock view and an AccountModel in the controller during the setup method. We will also test the processTransactionRequest by invoking controller.processTransactionRequest() in the testProcessTransactionRequest function and then checking the balance amount string which the controller will invoke during the end of the processing.

class ViewControllerTest: XCTestCase {
// ...
let mockView: MockView = MockView()


override func setUp(){
controller.setAccountView(mockView)
controller.setAccountModel(AccountModel())
}

override func tearDown() {
}

func testProcessTransactionRequest(){
controller.processTransactionRequest()
XCTAssertEqual("$1.00", mockView.balance)
}

}

The complete unit test case file is listed below.

We can test the controller logic by building 🛠 and running ▶ the unit test case. Note that we have implemented unit test cases for two components without even really providing a real UI. Doing so we have encapsulated our model and controller logic.

13. Create a Real UI View. Use a Xib file.

For us to attach a real iOS UI view, we will need to create a XIB where we can use a composite pattern to keep our views encapsulated together. This way we do not litter our controller with more than one view references, instead we will simply interface the controller with one view, thereby keeping the responsibility of the controller to only managing one Composite View, and one Composite Model.

To add a new Xib file, right click on Account Group folder, select new file, iOS tab, View (under User Interface), and then click “Next”.

Save as AccountView, in file location Account, in Account group, and target Account. Hit “Create”.

This should bring up the interface builder, then click the add component buton and add a vertical “StackView”. Placing the stack view directly under the top container view, we can create a composite view where we have also added the following views as child views :

  1. “Balance” description label and a larger display text value,
  2. “Withdrawal” description label and text edit field,
  3. “Deposit” description label and text edit field.
  4. and a “Submit” button.
AccountView composed with StackView and children views in the interface builder

14. Let’s Create a Companion UIView Swift file.

After we have a created a xib file using the interface builder, we will create a swift file where we can wire up the interface to actual code. To create the swift file right click on the Account group folder, click new file. Select “iOS” tab, “Swift File” under “Source”, and then hit next.

Save this file as AccountView, in the Account folder, under Account group, and target Account. Click “Create”.

Create a class AccountView in the file, extend UIView and implement the AccountView protocol. Let Xcode create the protocol stubs for you, and return some default values for now. Add the required init method, and also add the method to load the Nib via the Bundle.main.loadNibNamed function. See class below.

class AccountView: UIView,
AccountViewProtocol{


required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
commonInit()
}

func commonInit(){
Bundle.main.loadNibNamed("AccountView", owner: self, options: nil)

}

func getWithdrawalValue() -> String {
return ""
}

func getDepositValue() -> String {
return ""
}

func setBalanceValue(balanceAmount: String) {

}

func setController(controller:ViewController){

}

}

To wire up views internally in AccountView we have to let InterfaceBuilder know who the owner of this xib file is. Hence, we associate the XIB with the UI Swift file. Click File Owner, and select AccountView for Class dropdown.

Now, beginning with top parent UIView wire your IBOutlets. In this case we need to UITextFields for input, and a UILabel for the result. Also we need to wire the “Submit” Button action.

class AccountView: UIView,
AccountViewProtocol{

@IBOutlet weak var accountView: UIView!
@IBOutlet weak var balanceValueView: UILabel!
@IBOutlet weak var withdrawalValueField: UITextField!
@IBOutlet weak var depositValueField: UITextField!

var controller: ViewController?
// ...

func commonInit(){
Bundle.main.loadNibNamed("AccountView", owner: self, options: nil)
addSubview(accountView)

}

func getWithdrawalValue() -> String {
return withdrawalValueField.text ?? ""
}

func getDepositValue() -> String {
return depositValueField.text ?? ""
}

func setBalanceValue(balanceAmount: String) {
balanceValueView.text = balanceAmount
}

func setController(controller:ViewController){
self.controller = controller
}

@IBAction func submitClicked(_ sender: Any) {
self.controller?.processTransactionRequest()
}
}

15. Finally use your Composite Custom UI View

Select “Main.storyboard.” Under View Controller Scene and select the UI View and in the far right setting for Class select “AccountView”

Now, let’s run the entire app. Build and then run current scheme by clicking ▶ in the top left of the Xcode IDE in the toolbar.

Account App running on iOS

You should see a working UI view where you can submit a withdrawal and deposit amount, and it should be able to return to you the correct result. See a screen shot of the app.

We have completed implementing an entire a MVC pattern in an app, and also written unit tests for it. And in conclusion I’d like you to remember these few principles when attempting to develop MVC apps

  • Use protocols for model and view in your controller
  • Use composition instead of inheritance for your model and view.
  • Use dependency injection for your controller’s model and view.
  • and always write your unit test cases early on.

Hope this article helped, as this was my first article on medium please do let me know on how I did.

--

--