RT Blog
Programming

Unit Testing with Viper

For anyone who hasn’t used the Viper Architecture pattern before it’s certainly an interesting experience to say the least. Viper stands for.

V – View – this controls the user inputs by passing them on to the presenter. It also receives information back from the presenter so it can update itself.

I – Interactor – normally contains any business logic. I tend to use interactors to do any data fetching from the local cache and/or network.

P – Presenter – this handles the inputs from the view and then delegates them out to either the interactor or router.

E – Entity – is your model objects.

R– Router – handles all navigation between view controllers.

That was a quick overview on how each layer of the Viper stack works, but I would like to show you how I use it in my projects, and how it makes unit testing easier. Below is an example of how I would construct a module using Viper. I’m not going to focus on the Entity layer in this section as we do not need any model objects for this example.

Viper project structure

Contract

Not officially part of the Viper architecture, however, I use a contract to group all the interfaces together. It makes it easier to see exactly what is included in any given module.

import UIKit

/* Within this presenter we have a dependency for the view,
router and interactor as the presenter normally does all 
this communication. In the example I also have a signIn 
function which allows the view to tell the presenter that 
the button has been pressed. */
protocol SignInPresentation {
  var view: SignInView? { get set }
  var router: SignInRoutable! { get set }
  var interactor: SignInInteraction! { get set }
  
  func signIn(username: String)
}

/* The view is pretty straightforward with just a link to 
the presenter so it can forward any interaction on. For this
example I also have a method so that the view can update a 
loading spinner.*/
protocol SignInView: class {
  var presenter: SignInPresentation! { get set }
  
  func updateLoading(loading: Bool)
}

/* The interactor has an output which in our case will tell 
the presenter when it is finished. There is also a property 
for the network fetcher so we can choose different ways to 
fetch the data. Finally a signIn function so the presenter 
can start the interactor.*/  
protocol SignInInteraction: class {
  var output: SignInInteractorOutput? { get set }
  var fetcher: NetworkDataFetcher! { get set }
  
  func signIn(username: String)
}

/* As mentioned before the output will feed the status back 
to the presenter, but this doesn't always have to be the 
case.*/
protocol SignInInteractorOutput: class {
  func success()
  func failed(error: Error)
}

/* Finally the router which will have a weak refrence to the
current view controller. In this example I also have a 
method to show any alerts.*/
protocol SignInRoutable: class {
  var viewController: UIViewController? { get set }
  
  func showAlert(for error: Error)
}

Dependency Injection

To link all the parts of the module together I inject all the dependencies when I first need to use the module.

To do this, I add a class function within the router which allows me to do any setup I need. As my example doesn’t need anything extra, my create function does not have any parameters.

Router

import UIKit

class SignInRouter: SignInRoutable {
  
  /* The create function it pretty straightforward it 
    returns the initial controller for the module, and sets 
    up all the dependencies.*/
  class func create() -> UIViewController {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc =  storyboard.instantiateViewController(withIdentifier: "SignInIdentifier") as! SignInView & UIViewController
    let presenter = SignInPresenter()
    let router = SignInRouter()
    router.viewController = vc
    
    let interactor = SignInInteractor()
    interactor.output = presenter
    
    presenter.view = vc
    presenter.router = router
    presenter.interactor = interactor
    
    vc.presenter = presenter
    
    return vc
  }
  
  var viewController: UIViewController?
  
    /* For this example I also have a function which shows 
    an error message on the current controller.*/
  func showAlert(for error: Error) {
    let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "Ok", style: .cancel, handler: nil))
    viewController?.present(alert, animated: true, completion: nil)
  }
}

The router is again pretty straightforward. It has one function to set up the module and then conforms to our routable delegate which handles showing the alert.

Now you know how I create my modules and the interfaces which bind them together. I will do a run-through of my current example and show you how easy it is is unit test each section.

Interactor

I’m going to start off by showing the tests and then how I wrote the code to make them pass.

The interactor tests consist of four tests. Three in which I expect to catch errors based on the local validation which I apply. The final test just makes sure a valid username passes. I’ve also mocked the data fetcher just so we’re not hitting the network. The interactor output is also mocked as we’re not interested in passing the results, we just want to see what they are.

import XCTest
@testable import ViperExample

class SignInInteractorTest: XCTestCase {
  
  var sut: SignInInteractor?
  var mockOutput: MockSignInInteractorOutput?
  
  override func setUp() {
    sut = SignInInteractor()
    mockOutput = MockSignInInteractorOutput()
    sut?.fetcher = MockDataFetcher()
    sut?.output = mockOutput
    super.setUp()
  }
  
  override func tearDown() {
    sut = nil
    mockOutput = nil
    super.tearDown()
  }
   
  func testFailsWithEmptyUsername() {
    sut?.signIn(username: "")
    XCTAssertFalse(mockOutput!.isSuccess)
    XCTAssertEqual(mockOutput!.error?.localizedDescription, SignInErrorCodes.empty.localizedDescription)
  }
  
  func testFailsWithOnlySpacesInUsername() {
    sut?.signIn(username: "    ")
    XCTAssertFalse(mockOutput!.isSuccess)
    XCTAssertEqual(mockOutput!.error?.localizedDescription, SignInErrorCodes.empty.localizedDescription)
  }
  
  func testFailsIfOver20CharacterLimit() {
    sut?.signIn(username: "test.verylongusernamewhichisoverthelimit")
    XCTAssertFalse(mockOutput!.isSuccess)
    XCTAssertEqual(mockOutput!.error?.localizedDescription, SignInErrorCodes.tooLong.localizedDescription)
  }
  
  func testPassesWithCorrectUsername() {
    sut?.signIn(username: "test.viper")
    XCTAssertTrue(mockOutput!.isSuccess)
    XCTAssertNil(mockOutput?.error)
  }
}

class MockSignInInteractorOutput: SignInInteractorOutput {
  
  var isSuccess = false
  var error: Error?
  
  func success() {
   isSuccess = true
  }
  
  func failed(error: Error) {
    self.error = error
  }
}

class MockDataFetcher: NetworkDataFetcher {
  
  var response: NetworkDataFetcherResponse!
  var request: URLRequest!
  
  func start() {
    response.fetcherResponseSuccess()
  }
}

Now you know what the tests are, this is how I implemented it.

Again very simple logic to strip out any whitespace using an extension on String, and then just checking the count is less than 20. As all the business logic is inside the interactor, once this gets passed back to the presenter, the presenter just needs to tell the view what to do.

class SignInInteractor: SignInInteraction {
  
  var output: SignInInteractorOutput?
  var fetcher: NetworkDataFetcher!
  
  func signIn(username: String) {
    let stripWhitespace = username.removeWhiteSpace()
    guard stripWhitespace.count > 0 else {
      output?.failed(error: SignInErrorCodes.empty)
      return
    }
    guard stripWhitespace.count < 20 else {
      output?.failed(error: SignInErrorCodes.tooLong)
      return
    }
    fetcher.response = self
    fetcher.start()
  }
}

extension SignInInteractor: NetworkDataFetcherResponse {

  func fetcherResponseSuccess() {
    output?.success()
  }

  func fetcherResponseFailed(error: Error) {
    output?.failed(error: error)
  }
}

Presenter

As mentioned before the presenter is like a hub to control all communication between view, router and interactor without doing any real business logic. Due to its dependencies on the three interfaces, they require some setup to mock all of these within the tests. However, once mocked we can add tests to make sure the presenter is calling the correct parts of the app.

import XCTest
@testable import ViperExample

class SignInPresenterTest: XCTestCase {
  
  var sut: SignInPresenter?
  var mockView: MockSignInView?
  var mockInteractor: MockSignInInteractor?
  var mockRouter: MockSignInRouter?

  /* We need to do a lot of setup here to make sure all 
    out dependcies are loaded before the tests are run.*/ 
  override func setUp() {
    sut = SignInPresenter()
    mockView = MockSignInView()
    mockInteractor = MockSignInInteractor()
    mockRouter = MockSignInRouter()
    mockInteractor?.output = sut
    sut?.view = mockView
    sut?.interactor = mockInteractor
    sut?.router = mockRouter
    super.setUp()
  }
  
  override func tearDown() {
    sut = nil
    mockView = nil
    mockInteractor = nil
    mockRouter = nil
    super.tearDown()
  }
  
  /* By mocking the interactor to not return a completion 
    or failure we can make sure that we initial setup the 
    view correctly when starting the sign in process.*/
  func testViewLoadingStateChangesWhenSignIn() {
    mockInteractor?.noReturn = true
    sut?.signIn(username: "")
    XCTAssertTrue(mockView?.loading ?? false)
  }
  
  // We can check that the interactor is called when signing in.    
  func testInteractorStartsSignInProcess() {
    sut?.signIn(username: "")
    XCTAssertTrue(mockInteractor?.processing ?? false)
  }
  
  // This test checks to make sure that we tell the view to finish loading when a result comes back as successful.    
  func testUpdatesViewWhenSuccessIsReturned() {
    sut?.signIn(username: "")
    XCTAssertFalse(mockView?.loading ?? true)
  }
  
  // My forcing the interactor mock to fail. We can test that the presenter is still updating the view to finishing loading.     
  func testWhenFailureItUpdatesView() {
    mockInteractor?.fail = true
    sut?.signIn(username: "")
    XCTAssertFalse(mockView?.loading ?? true)
  }
  
  // Again by forcing the interactor mock to fail we can test to make sure the router is called.    
  func testWhenFailureItShowsErrorWithRouter() {
    mockInteractor?.fail = true
    sut?.signIn(username: "")
    XCTAssertTrue(mockRouter?.showAlert ?? false)
  }
}

class MockSignInView: SignInView {
  
  var loading = false
  var presenter: SignInPresentation!
  
  func updateLoading(loading: Bool) {
    self.loading = loading
  }
}

class MockSignInInteractor: SignInInteraction {
  
  var processing = false
  var fail = false
  var noReturn = false
  var output: SignInInteractorOutput?
  var fetcher: NetworkDataFetcher! = MockDataFetcher()
  
  func signIn(username: String) {
    self.processing = true
    guard !noReturn else { return }
    if fail {
      output?.failed(error: SignInErrorCodes.empty)
    } else {
      output?.success()
    }
  }
}

class MockSignInRouter: SignInRoutable {
  
  var showAlert = false
  var viewController: UIViewController?
  
  func showAlert(for error: Error) {
    showAlert = true
  }
}

All these tests are really valuable as they make sure the key layers of the Viper architecture are communicating correctly. When it comes down to the code, its very minimal as all we doing is delegating the responsibility to other parts of the system.

class SignInPresenter: SignInPresentation {
 
  weak var view: SignInView?
  var router: SignInRoutable!
  var interactor: SignInInteraction!
  
  // When the sign in process begins it first tells the view to update its loading state before asking the interactor to sign in.
  func signIn(username: String) {
    view?.updateLoading(loading: true)
    interactor.signIn(username: username)
  }
}

extension SignInPresenter: SignInInteractorOutput {
  
  // Once a success call is fire we update the loading state of the view. If we progressed with this exmaple we may also ask the router to move to the next controller.
  func success() {
    view?.updateLoading(loading: false)
  }
  
  // If the interactor fails for any resason we update the view state and show a loading screen.
  func failed(error: Error) {
    view?.updateLoading(loading: false)
    router.showAlert(for: error)
  }
}

The final testable element would be the router which I already mentioned above. I’m not going to show the test for that but we could easily mock the view controller dependency on the router and make sure it calls present() when showAlert() is called.

We’ve also not added tests for the View layer. This is because all the view is doing is updating UI elements based on the logic from the presenter, which is already tested.

I hope that gives you a better understanding of how to unit test and what to unit test. When breaking your modules down to this scale it is easy to see exactly what the public interfaces are and what areas are testable.

All the code in this post can be found on my Github account.

Related posts

Full-stack introduction using Rails, React, Docker and Heroku

Rowan
5 years ago

WWDC 19 – Swift UI, Combine

Rowan
5 years ago

I don’t want this Class!

Rowan
6 years ago
Exit mobile version