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
相关产品推荐
相关产品推荐

