You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS电影应用XCTest断言失败:搜索模块单元测试问题

iOS电影应用搜索功能单元测试断言失败问题

我正在开发一款iOS电影应用,核心功能已完成,但编写单元测试时遇到问题。应用包含两个核心页面:影片列表主页面和影片详情页。详情页测试运行正常,但主页面搜索功能的单元测试出现以下错误:

testSearchMovie_whenAPISuccess_showsMovie(): XCTAssertEqual failed: ("nil") is not equal to ("Optional("Blender")")

我无法定位问题所在,以下是测试代码,testSearchMovie_whenAPISuccess_showsMovie()无法正常运行,而详情页测试可正常执行:

final class TheMovieAppTests: XCTestCase {
    
    private var movieViewModel: MovieViewModel!
    private var detailViewModel: DetailViewModel!
    
    private var movieService: MockMovieService!
    
    private var movieOutput: MockMovieViewModelOutput!
    private var movieDetailOutput: MockDetailViewModelOutput!
    
    override func setUpWithError() throws {
        movieService = MockMovieService()
        movieViewModel = MovieViewModel(webService: movieService)
        movieOutput = MockMovieViewModelOutput()
        movieViewModel.movieOutput = movieOutput
        
        detailViewModel = DetailViewModel(webService: movieService)
        movieDetailOutput = MockDetailViewModelOutput()
        detailViewModel.detailOutput = movieDetailOutput
    }
    
    override func tearDownWithError() throws {
        movieViewModel = nil
        detailViewModel = nil
        movieService = nil
    }
    
    func testSearchMovie_whenAPISuccess_showsMovie() throws{
        
        let mockMovieResult = MovieSearchResponse(search: [Movie(Poster: "www", Title: "Blender", Year: "2012", Rated: "123", Released: "1234", Runtime: "111", Genre: "Sci-Fi", Director: "berk berk", Language: "English", Country: "USA", BoxOffice: "12323", Metascore: "123321", imdbRating: "21332", imdbVotes: "123213", imdbID: "1", Plot: "234")])
    
        movieService.fetchMovieMockResult = .success(mockMovieResult)
        print(mockMovieResult)
      
        
        movieViewModel.fetchMovieSearchs(movieName: "Avengers")
        
        print(movieOutput.searchMovie?.search[0].Title)
        
        print(movieOutput.searchMovie?.search[0].Title ?? "Nil Geliyor")
        XCTAssertEqual(movieOutput.searchMovie?.search[0].Title, "Blender")
        
    }
    func testFetchMovieDetail_whenAPISuccess_showsMovieDetail() throws{
        let mockMovieDetails = Movie(Poster: "www", Title: "Avengers", Year: "2014", Rated: "", Released: "", Runtime: "", Genre: "", Director: "", Language: "", Country: "", BoxOffice: "", Metascore: "", imdbRating: "", imdbVotes: "", imdbID: "", Plot: "")
        
        movieService.fetchMovieMockDetails = .success(mockMovieDetails)
        
        detailViewModel.getMovie(id: "1")
        
        XCTAssertEqual(movieDetailOutput.movie?.Poster, "www")
        XCTAssertEqual(movieDetailOutput.movie?.Title, "Avengers")
        XCTAssertEqual(movieDetailOutput.movie?.Year, "2014")
    }
} 

class MockMovieService : MovieService {
    
    var fetchMovieMockResult : Result<MovieSearchResponse, TheMovieApp.CustomError>?
    var fetchMovieMockDetails : Result<Movie, TheMovieApp.CustomError>?
    
    func searchMovies(query: String, completion: @escaping (Result<TheMovieApp.MovieSearchResponse, TheMovieApp.CustomError>) -> ()) {
        if let result = fetchMovieMockResult {
            print("reuslt giriş")
            print(result)
            print("result çıkış")
            completion(result)
        }
    }
    
    func getMovieDetails(imdbID: String, completion: @escaping (Result<TheMovieApp.Movie, TheMovieApp.CustomError>) -> ()) {
        if let result = fetchMovieMockDetails {
           
            completion(result)
        }
    }
} 

class MockMovieViewModelOutput : MovieViewModelOutput {
    var searchMovie : MovieSearchResponse?
    func setSearchMovie(movieList: MovieSearchResponse) {
        print("output giriş")
        print(movieList)
        self.searchMovie = movieList
        print("output çıkış")
        
    }
} 

class MockDetailViewModelOutput : DetailViewModelOutput {
    var movie : Movie?
    func setMovieDetails(movie: TheMovieApp.Movie?, error: String?) {
        self.movie = movie
    }
}

问题原因

搜索功能的测试中,fetchMovieSearchs内部依赖异步回调逻辑——即使Mock服务同步执行completion,若ViewModel将回调切换到主队列或其他串行队列执行,测试代码在调用fetchMovieSearchs后立即执行断言时,movieOutput.searchMovie还未被赋值,导致断言失败。详情页测试可能因ViewModel的实现细节(比如回调在当前队列同步执行)侥幸通过,但这不是稳定的测试方式。

解决方案

使用XCTestExpectation等待异步操作完成后再执行断言,修改后的测试代码如下:

func testSearchMovie_whenAPISuccess_showsMovie() throws{
    let expectation = XCTestExpectation(description: "搜索电影成功后更新数据")
    
    let mockMovieResult = MovieSearchResponse(search: [Movie(Poster: "www", Title: "Blender", Year: "2012", Rated: "123", Released: "1234", Runtime: "111", Genre: "Sci-Fi", Director: "berk berk", Language: "English", Country: "USA", BoxOffice: "12323", Metascore: "123321", imdbRating: "21332", imdbVotes: "123213", imdbID: "1", Plot: "234")])

    movieService.fetchMovieMockResult = .success(mockMovieResult)
    
    // 重写MockOutput,添加闭包触发测试期望
    movieOutput = MockMovieViewModelOutput()
    movieOutput.setSearchMovieClosure = { _ in
        expectation.fulfill()
    }
    movieViewModel.movieOutput = movieOutput
    
    movieViewModel.fetchMovieSearchs(movieName: "Avengers")
    
    // 等待异步操作完成,1秒超时足够测试使用
    wait(for: [expectation], timeout: 1.0)
    
    XCTAssertEqual(movieOutput.searchMovie?.search[0].Title, "Blender")
}

// 修改MockMovieViewModelOutput,新增闭包用于触发测试期望
class MockMovieViewModelOutput : MovieViewModelOutput {
    var searchMovie : MovieSearchResponse?
    var setSearchMovieClosure: ((MovieSearchResponse) -> Void)?
    
    func setSearchMovie(movieList: MovieSearchResponse) {
        print("output giriş")
        print(movieList)
        self.searchMovie = movieList
        setSearchMovieClosure?(movieList)
        print("output çıkış")
    }
}

额外建议

  • 详情页测试也应该添加异步等待逻辑,避免因队列调度变化导致测试不稳定。
  • 测试中尽量减少print语句,改用XCTest的断言和期望来验证行为,保持测试代码简洁。

内容的提问来源于stack exchange,提问作者Berkay Sazak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 06:25:22