Leveraging Protocol Oriented Programming for Unit Testing

suggest change

Protocol Oriented Programming is a useful tool in order to easily write better unit tests for our code.

Let’s say we want to test a UIViewController that relies on a ViewModel class.

The needed steps on the production code are:

  1. Define a protocol that exposes the public interface of the class ViewModel, with all the properties and methods needed by the UIViewController.
  2. Implement the real ViewModel class, conforming to that protocol.
  3. Use a dependency injection technique to let the view controller use the implementation we want, passing it as the protocol and not the concrete instance.
protocol ViewModelType {
   var title : String {get}
   func confirm()
}

class ViewModel : ViewModelType {
   let title : String

   init(title: String) {
       self.title = title
   }
   func confirm() { ... }
}

class ViewController : UIViewController {
   // We declare the viewModel property as an object conforming to the protocol
   // so we can swap the implementations without any friction.
   var viewModel : ViewModelType! 
   @IBOutlet var titleLabel : UILabel!

   override func viewDidLoad() {
       super.viewDidLoad()
       titleLabel.text = viewModel.title
   }

   @IBAction func didTapOnButton(sender: UIButton) {
       viewModel.confirm()
   }
}

// With DI we setup the view controller and assign the view model.
// The view controller doesn't know the concrete class of the view model, 
// but just relies on the declared interface on the protocol.
let viewController = //... Instantiate view controller
viewController.viewModel = ViewModel(title: "MyTitle")

Then, on unit test:

  1. Implement a mock ViewModel that conforms to the same protocol
  2. Pass it to the UIViewController under test using dependency injection, instead of the real instance.
  3. Test!
class FakeViewModel : ViewModelType {
   let title : String = "FakeTitle"

   var didConfirm = false
   func confirm() {
       didConfirm = true
   }
}

class ViewControllerTest : XCTestCase {
    var sut : ViewController!
    var viewModel : FakeViewModel!

    override func setUp() {
        super.setUp()

        viewModel = FakeViewModel()
        sut = // ... initialization for view controller
        sut.viewModel = viewModel

        XCTAssertNotNil(self.sut.view) // Needed to trigger view loading
    } 

    func testTitleLabel() {
        XCTAssertEqual(self.sut.titleLabel.text, "FakeTitle")
    }

    func testTapOnButton() {
        sut.didTapOnButton(UIButton())
        XCTAssertTrue(self.viewModel.didConfirm)
    }
}

Feedback about page:

Feedback:
Optional: your email if you want me to get back to you:



Table Of Contents