I am writing unit tests for my UITableViewController's data source, which has a UISearchController for filtering results. I need to test the logic in NumberOfRowsInSection so that when the search controller is active, the data source returns the count from the filtered array rather than the normal array.
The function controls this by checking if the search controller's 'isActive' is true/false.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchController.isActive ? filteredResults.count : results.count
}
So my unit test is written like this
func testNumberOfRowsInSection() {
XCTAssertFalse(searchController.isActive)
XCTAssertEqual(4, dataSource.tableView(tableView, numberOfRowsInSection: 0))
searchController.isActive = true
XCTAssertTrue(searchController.isActive) // Fails right after setting it true
XCTAssertEqual(0, dataSource.tableView(tableView, numberOfRowsInSection: 0)) // Fails
searchController.isActive = false
XCTAssertFalse(searchController.isActive)
XCTAssertEqual(4, dataSource.tableView(tableView, numberOfRowsInSection: 0))
}
So the 'isActive' property is not staying true immediately after setting it to true in unit tests. This is strange because during the regular run of the app I can set it to true in the view controller and it stays active.
override func viewDidLoad() {
super.viewDidLoad()
// ...
searchController.isActive = true // Makes search bar immediately visible
// ...
}
The documentation shows that it's a settable property, so why doesn't setting it to true do anything in unit tests? I've also tried attaching it to a navigation bar but that didn't change anything. If I can't test it like this in unit tests, I'm going to have to mock that functionality, which would be annoying since it should be simple to test this.
Updated example:
class MovieSearchDataSourceTests: XCTestCase {
private var window: UIWindow!
private var controller: UITableViewController!
private var searchController: UISearchController!
private var sut: MovieSearchDataSource!
override func setUp() {
window = UIWindow()
controller = UITableViewController()
searchController = UISearchController()
sut = MovieSearchDataSource(tableView: controller.tableView,
searchController: searchController,
movies: Array(repeating: Movie.test, count: 4))
window.rootViewController = UINavigationController(rootViewController: controller)
controller.navigationItem.searchController = searchController
}
override func tearDown() {
window = nil
controller = nil
searchController = nil
sut = nil
RunLoop.current.run(until: Date())
}
func testNumberOfRowsInSection() {
window.addSubview(controller.tableView)
controller.loadViewIfNeeded()
XCTAssertFalse(searchController.isActive)
XCTAssertEqual(4, sut.tableView(controller.tableView, numberOfRowsInSection: 0))
searchController.isActive = true
XCTAssertTrue(searchController.isActive)
XCTAssertEqual(0, sut.tableView(controller.tableView, numberOfRowsInSection: 0))
searchController.isActive = false
XCTAssertFalse(searchController.isActive)
XCTAssertEqual(4, sut.tableView(controller.tableView, numberOfRowsInSection: 0))
}
}
After not getting UIWindow to work, my solution was mocking the UISearchController with a protocol.
// Protocol in main project
protocol Activatable: AnyObject {
var isActive: Bool { get set }
}
extension UISearchController: Activatable { }
final class MovieSearchDataSource: NSObject {
private var movies: [Movie] = []
private var filteredMovies: [Movie] = []
private let tableView: UITableView
private let searchController: Activatable
init(tableView: UITableView, searchController: Activatable, movies: [Movie] = []) {
self.tableView = tableView
self.searchController = searchController
self.movies = movies
super.init()
}
}
extension MovieSearchDataSource: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchController.isActive ? filteredMovies.count : movies.count
}
}
// Unit test file
class MockSearchController: Activatable {
var isActive = false
}
class MovieSearchDataSourceTests: XCTestCase {
private var tableView: UITableView!
private var searchController: MockSearchController!
private var sut: MovieSearchDataSource!
override func setUp() {
tableView = UITableView()
searchController = MockSearchController()
sut = MovieSearchDataSource(tableView: tableView,
searchController: searchController,
movies: Array(repeating: Movie.test, count: 4))
}
override func tearDown() {
tableView = nil
searchController = nil
sut = nil
}
func testNumberOfRowsInSection() {
XCTAssertFalse(searchController.isActive)
XCTAssertEqual(4, sut.tableView(tableView, numberOfRowsInSection: 0))
searchController.isActive = true
XCTAssertTrue(searchController.isActive)
XCTAssertEqual(0, sut.tableView(tableView, numberOfRowsInSection: 0))
searchController.isActive = false
XCTAssertFalse(searchController.isActive)
XCTAssertEqual(4, sut.tableView(tableView, numberOfRowsInSection: 0))
}
}