3

I'm building a simple iOS app in Swift according to the Model-View-Controller pattern. I can test the Model class by giving it input data and asserting the output result against my expectation. But I was wondering how I could test the Controller class?

It seems that if I do want to test the Controller class, the testing logic would be much more complicated. Is there a standard way of testing the Controller class?

halfer
  • 19,824
  • 17
  • 99
  • 186
Thor
  • 9,638
  • 15
  • 62
  • 137
  • 2
    A controller is merely the glue between your model and your UI. Give it input data as if it were given by the model, and test the output data that would've gone to the UI – Alexander Jul 24 '16 at 04:18
  • 1
    https://cocoapods.org/pods/FBSnapshotTestCase is a good tool for testing layouts and https://cocoapods.org/pods/KIF can be used to automate touch events by hijacking the accessibility feature (and bridging it to Swift has some pitfalls). Both take some prerequisite skill in testing to implement though. I'd suggest studying Quick & Nimble as unit testing tools if you haven't already – markedwardmurray Jul 24 '16 at 05:23
  • 2
    This question is too broad and potentially primarily opinion-based. – JAL Jul 26 '16 at 19:45

1 Answers1

8

Don't test your UIViewControllers. There is a lot of stuff that happens to them that you don't see and/or have no control over. Instead, keep as much logic in other objects such as View Models, and not your UIViewControllers. You can then test your View Models, just like you would test your Models.

Edit:

How you might want to structure your UIViewControllers and test Models & View Models:

The main takeaway from this code is:

  1. Use Dependency Injection
  2. Give real dependencies to classes in Release, and fake dependencies in tests

View Controller

// this class is super simple, so there's hardly any reason to test it now.
class SomeViewController: UIViewController {
    @IBOutlet weak var someLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        // we give the *real* user accessor in the view controller
        let viewModel = SomeViewModel(userAccessor: DBUserAccessor())
        someLabel.text = viewModel.welcomeMessage
    }
}

User Model

struct User {
    var name: String
}

User Accessor

This is some dependency that your View Model needs. Give a real one during Release (in your View Controller). Give a fake one during tests, so that you can control it.

protocol UserAccessor {
    var currentUser: User? { get }
}

// since we're using the fake version of this class to test the view model, you might want to test this class on its own
// you would do that using the same principles that I've shown (dependency injection).
class DBUserAccessor: UserAccessor {
    var currentUser: User? {
        // some real implementation that's used in your app
        // get the user from the DB
        return User(name: "Samantha") // so not really this line, but something from CoreData, Realm, etc.
    }
}

class FakeUserAccessor: UserAccessor {
    // some fake implementation that's used in your tests
    // set it to whatever you want your tests to "see" as the current User from the "DB"
    var currentUser: User?
}

View Model

This is where the actual logic lives that you want to test.

class SomeViewModel {
    let userAccessor: UserAccessor

    init(userAccessor: UserAccessor) {
        self.userAccessor = userAccessor
    }

    var welcomeMessage: String {
        if let username = self.username {
            return "Welcome back, \(username)"
        } else {
            return "Hello there!"
        }
    }

    var username: String? {
        return userAccessor.currentUser?.name
    }
}

Tests

And finally, how you want to test it.

class SomeViewModelTest: XCTestCase {
    func testWelcomeMessageWhenNotLoggedIn() {
        let userAccessor = FakeUserAccessor()
        let viewModel = SomeViewModel(userAccessor: userAccessor) // we give the *fake* user accessor to the view model in tests
        userAccessor.currentUser = nil // set the fake UserAccessor to not have a user "logged in"

        // assert that the view model, which uses whatever you gave it, gives the correct message
        XCTAssertEqual(viewModel.welcomeMessage, "Hello there!")
    }

    func testWelcomeMessageWhenLoggedIn() {
        let userAccessor = FakeUserAccessor()
        let viewModel = SomeViewModel(userAccessor: userAccessor)
        userAccessor.currentUser = User(name: "Joe") // this time, the use is "logged in"

        XCTAssertEqual(viewModel.welcomeMessage, "Welcome back, Joe") // and we get the correct message
    }
}
solidcell
  • 7,639
  • 4
  • 40
  • 59
  • Thank you so much for the suggestion! I was wondering if you could elaborate a bit more on "you can test your view models". What difference and similairity does it have to testing only models? Thanks again for your help! – Thor Jul 28 '16 at 12:58
  • 2
    My pleasure. I've added a bunch of sample code that shows how you'd set it all up. – solidcell Jul 28 '16 at 14:04