Reputation: 6772
So I have a storyboard with a UITableView. There is a prototype cell with a show segue hooked up to another UIViewController
class looks like this:
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withIdentifier: "CellOne", for: indexPath)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected the row")
}
}
Normally I would test it by swizzling prepare for segue to capture the destination ViewController and whatever else I need but calling tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
programmatically doesn't trigger the code in prepare for segue.
Is it possible to test that selecting Cell One triggers the storyboard segue without adding a segue identifier and calling it explicitly from prepareForSegue?
Upvotes: 2
Views: 1318
Reputation: 3243
For just testing a segue you can do:
In your VC:
open let segueToSomewhere = "segueToSomewhere"
open var calledSegue: UIStoryboardSegue!
override open func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
calledSegue = segue
}
In your Tests:
func testYourSegue() {
//Given
let storyboard = UIStoryboard(name: "Your_storyboard", bundle: nil)
let yourVC = storyboard.instantiateViewController(withIdentifier: "YourVC") as? YourVC
//When
yourVC.performSegue(withIdentifier: previousVC.segueToSomewhere, sender: nil)
//Then
XCTAssertEqual(previousVC.calledSegue.identifier, yourVC.segueToSomewhere, "The selected segue should be \(previousVC.segueToSomewhere)")
}
Upvotes: 0
Reputation: 6772
Updated: Long story short there isn't a great way to do this. What makes this difficult is that while most controls we're used to testing have an action and a sender, a touch on a UITableViewCell is a different paradigm.
That said, having a segue identifier is basically a pre-requisite for any strategy.
One way is to get a reference to the cell and call performSegue(withIdentifier:,sender:)
:
class ViewControllerTests: XCTestCase {
func testClickingACell() {
let controller = UIStoryboard(name: "ViewController", bundle: nil).instantiateInitialViewController() as! ViewController
let cell = controller.tableView.dataSource?.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 0))
controller.performSegue(withIdentifier: "MySegue", sender: cell)
XCTAssertNotNil(controller.presentedViewController as? TheDestinationViewController)
}
}
Another (completely overkill) way would be to have a custom cell where you handle all of your own touch logic. This would be insane but it's a possibility and it would open up more options for testing. I'm not going to show this way because it would be an insane way to do it.
Another way is to use a different architecture that gets rid of UIKit
and allows for testing just the performSegue
logic. Example:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
@IBOutlet var myTableView: UITableView!
var navigator: UIViewController?
override func viewDidLoad() {
super.viewDidLoad()
navigator = self
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
navigator?.performSegue(withIdentifier: "MySegue", sender: nil)
}
}
This allows you to do something like this in your tests:
class MockNavigator: ViewController {
var performSegueCalled = false
var performSegueIdentifier: String?
override func performSegue(withIdentifier identifier: String, sender: Any?) {
performSegueCalled = true
performSegueIdentifier = identifier
}
}
func testExample() {
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController
controller.loadViewIfNeeded()
// Need to keep a reference to be able to assert against it
let mockNavigator = MockNavigator()
controller.navigator = mockNavigator
controller.tableView(controller.myTableView, didSelectRowAt: IndexPath(row: 0, section: 0))
XCTAssertTrue(mockNavigator.performSegueCalled)
XCTAssertEqual(mockNavigator.performSegueIdentifier, "MySegue")
}
Another way to structure your code to avoid UIKit is to use something like a view model-coordinator pattern to create and test a viewModel. Basically you'd tell your coordinator that a cell was selected and the coordinator would update a view model with the desired segue identifier. This way you could test your coordinator object to and be mostly sure that you'll trigger the correct segue if the coordinator is hooked up. A simple manual test would tell you that.
In pseudocode:
struct ViewModel {
let labelText: String
let segueIdentifier: String
}
class Coordinator {
var data = [YourObject]()
var viewModel = ViewModel(labelText: "", segueIdentifier: "")
func selectedItem(at row: Int) {
let item = data[row]
// Do some logic to figure out which identifier you want
var segueIdentifer: String
if item == whatever {
segueIdentifier = "something"
}
viewModel = ViewModel(labelText: item.text, segueIdentifier: segueIdentifier)
}
}
Probably the best way is a combination of approaches. Use a coordinator with a view model that's tested on its own. Then have a test where you use UIKit to select a cell and make sure that a mocked implementation of that coordinator is used as expected. The smaller units you're testing at a time the easier it will be.
Upvotes: 1
Reputation: 16327
If your tableViewCell is the only thing that triggers a segue to the destination you can use is or as:
if let destination = segue.destination as? MyViewController,
let indexPath = tableView.indexPathForSelectedCell {
destination.detail = model[indexPath.row]
}
Otherwise if you need to disambiguate you can check the class of the sender with is or as
Upvotes: 1