UISearchController 的 isActive 属性 无法在单元测试中设置为 true
UISearchController's isActive property can't be set to true in unit tests
我正在为我的 UITableViewController 的数据源编写单元测试,它有一个用于过滤结果的 UISearchController。我需要测试 NumberOfRowsInSection 中的逻辑,以便当搜索控制器处于活动状态时,数据源 returns 来自过滤数组而不是普通数组的计数。
该函数通过检查搜索控制器的 'isActive' 是否为 true/false 来控制这一点。
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchController.isActive ? filteredResults.count : results.count
}
所以我的单元测试是这样写的
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))
}
因此 'isActive' 属性 在单元测试中将其设置为 true 后不会立即保持为 true。这很奇怪,因为在应用程序的常规 运行 期间,我可以在视图控制器中将其设置为 true 并保持活动状态。
override func viewDidLoad() {
super.viewDidLoad()
// ...
searchController.isActive = true // Makes search bar immediately visible
// ...
}
文档显示它是可设置的 属性,那么为什么不将其设置为 true 在单元测试中执行任何操作?我也试过将它附加到导航栏,但这并没有改变任何东西。如果我不能在单元测试中像这样测试它,我将不得不模拟该功能,这会很烦人,因为测试它应该很简单。
更新示例:
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))
}
}
当我面临这样的挑战时,我会使用两个工具:
- 正在执行 运行 循环
- 将视图控制器的视图变成真实的window
我写了一个没有用的单元测试,然后尝试了这些技巧。 window 技巧奏效了。
func test_canSetWhetherSearchControllerIsActive() throws {
putInViewHierarchy(sut)
sut.searchController.isActive = false
XCTAssertFalse(sut.searchController.isActive)
sut.searchController.isActive = true
XCTAssertTrue(sut.searchController.isActive)
}
func putInViewHierarchy(_ vc: UIViewController) {
let window = UIWindow()
window.addSubview(vc.view)
}
注意:当你使用 window 技巧时,视图控制器不会在测试结束时被释放,除非我们也使用 运行 循环。这必须在测试功能结束后在拆卸中完成:
override func tearDownWithError() throws {
sut = nil
executeRunLoop()
try super.tearDownWithError()
}
func executeRunLoop() {
RunLoop.current.run(until: Date())
}
在没有让 UIWindow 工作之后,我的解决方案是使用协议模拟 UISearchController。
// 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))
}
}
我正在为我的 UITableViewController 的数据源编写单元测试,它有一个用于过滤结果的 UISearchController。我需要测试 NumberOfRowsInSection 中的逻辑,以便当搜索控制器处于活动状态时,数据源 returns 来自过滤数组而不是普通数组的计数。
该函数通过检查搜索控制器的 'isActive' 是否为 true/false 来控制这一点。
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchController.isActive ? filteredResults.count : results.count
}
所以我的单元测试是这样写的
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))
}
因此 'isActive' 属性 在单元测试中将其设置为 true 后不会立即保持为 true。这很奇怪,因为在应用程序的常规 运行 期间,我可以在视图控制器中将其设置为 true 并保持活动状态。
override func viewDidLoad() {
super.viewDidLoad()
// ...
searchController.isActive = true // Makes search bar immediately visible
// ...
}
文档显示它是可设置的 属性,那么为什么不将其设置为 true 在单元测试中执行任何操作?我也试过将它附加到导航栏,但这并没有改变任何东西。如果我不能在单元测试中像这样测试它,我将不得不模拟该功能,这会很烦人,因为测试它应该很简单。
更新示例:
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))
}
}
当我面临这样的挑战时,我会使用两个工具:
- 正在执行 运行 循环
- 将视图控制器的视图变成真实的window
我写了一个没有用的单元测试,然后尝试了这些技巧。 window 技巧奏效了。
func test_canSetWhetherSearchControllerIsActive() throws {
putInViewHierarchy(sut)
sut.searchController.isActive = false
XCTAssertFalse(sut.searchController.isActive)
sut.searchController.isActive = true
XCTAssertTrue(sut.searchController.isActive)
}
func putInViewHierarchy(_ vc: UIViewController) {
let window = UIWindow()
window.addSubview(vc.view)
}
注意:当你使用 window 技巧时,视图控制器不会在测试结束时被释放,除非我们也使用 运行 循环。这必须在测试功能结束后在拆卸中完成:
override func tearDownWithError() throws {
sut = nil
executeRunLoop()
try super.tearDownWithError()
}
func executeRunLoop() {
RunLoop.current.run(until: Date())
}
在没有让 UIWindow 工作之后,我的解决方案是使用协议模拟 UISearchController。
// 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))
}
}