UISearchController 的 isActive 属性在单元测试中无法设置为 true

问题描述

我正在为我的 UITableViewController 的数据源编写单元测试,它有一个用于过滤结果的 UISearchController。我需要测试 NumberOfRowsInSection 中的逻辑,以便当搜索控制器处于活动状态时,数据源从过滤后的数组而不是普通数组中返回计数。

函数通过检查搜索控制器的“isActive”是否为真/假来控制这一点。

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,numberOfRowsInSection: 0)) // Fails
    
    searchController.isActive = false
    XCTAssertFalse(searchController.isActive)
    XCTAssertEqual(4,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,numberOfRowsInSection: 0))
    
    searchController.isActive = false
    XCTAssertFalse(searchController.isActive)
    XCTAssertEqual(4,numberOfRowsInSection: 0))
}

}

解决方法

当我面临这样的挑战时,我会使用两种工具:

  1. 执行运行循环
  2. 将视图控制器的视图放入真实窗口

我写了一个不起作用的单元测试,然后尝试了这些技巧。窗口技巧奏效了。

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)
}

注意:当你使用窗口技​​巧时,视图控制器不会在测试结束时被释放,除非我们也泵入运行循环。这必须在测试功能结束后在拆卸中完成:

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,numberOfRowsInSection: 0))
    
    searchController.isActive = false
    XCTAssertFalse(searchController.isActive)
    XCTAssertEqual(4,numberOfRowsInSection: 0))
}

}

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...