View Model에 대한 Unit Test를 진행하기 위해 Mock Data가 필요함.
MovieService 프로토콜을 구현하여, APIManager - 프로토콜 준수, ViewModel - 의존성 주입하여 결합도를 낮추고자 하였음.
protocol MovieService {
func fetchMovies(page: Int, completion: @escaping (Result<MovieResponse, Error>) -> Void)
func downloadImage(posterPath: String, completion: @escaping(Result<UIImage, Error>) -> Void)
}
class APIManager: MovieService{
func fetchMovies(page: Int, completion: @escaping (Result<MovieResponse, Error>) -> Void){
let requestURL = URL(string: "https://api.themoviedb.org/3/movie/popular?api_key=\(APIKey.apiKey)&page=\(page)")
guard let url = requestURL else {
let error = NSError(domain: "", code: 0)
completion(.failure(error))
return
}
performRequest(url: url) { result in
switch result {
case .success(let data):
do {
let decoder = JSONDecoder()
let movieResponse = try decoder.decode(MovieResponse.self, from: data)
completion(.success(movieResponse))
} catch let error {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
func downloadImage(posterPath: String, completion: @escaping(Result<UIImage, Error>) -> Void) {
guard let url = URL(string: posterPath) else {
let error = NSError(domain: "", code: 0)
completion(.failure(error))
return
}
performRequest(url: url) { result in
switch result {
case .success(let data):
if let image = UIImage(data: data) {
completion(.success(image))
} else {
let error = NSError(domain: "", code: 0)
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
APIManager는 MovieService 프로토콜을 준수하고 있음.
ViewModel에 MovieService 의존성 주입을 함으로써 해당 프로토콜을 준수하는 구현체를 사용할 수 있고, Unit Test를 위한 Mock 데이터를 주입할 수 있게 됨.
class ViewModel{
let movieService: MovieService
var movie = [Movie]()
init(movieService: MovieService) {
self.movieService = movieService
}
func fetchMovies(page: Int, completion: @escaping () -> Void) {
movieService.fetchMovies(page: page) { [weak self] result in
DispatchQueue.main.async {
switch result{
case .success(let response):
self?.movie = response.results
completion()
case .failure(let error):
print("faile error: \(error)")
}
}
}
}
}
테스트를 위한 Mock 데이터 생성
class MockMovieService: MovieService {
func fetchMovies(page: Int, completion: @escaping (Result<MovieResponse, Error>) -> Void) {
let movie1 = Movie(title: "Movie1", posterPath: "/path1", releaseDate: "2021-01-01", voteAverage: 1.0, overview: "overview1")
let movie2 = Movie(title: "Movie2", posterPath: "/path2", releaseDate: "2021-01-02", voteAverage: 2.0, overview: "overview2")
let movieResponse = MovieResponse(page: 1, results: [movie1, movie2])
completion(.success(movieResponse))
}
func downloadImage(posterPath: String, completion: @escaping (Result<UIImage, Error>) -> Void) {
if let image = UIImage(named: "testImage") {
completion(.success(image))
} else {
let error = NSError(domain: "", code: 0)
completion(.failure(error))
}
}
}
fetchMovies, downloadImage 메소드 테스트 코드 작성
viewModel의 경우, MockMovieService를 사용하였음.
viewModel의 fetchMovies 메소드의 retrun 값이 XCTAssertEqual method를 활용해서 출력되는 값이 동일하게 나오는지 체크할 수 있었음.
final class MovieRankTests: XCTestCase {
var viewModel: ViewModel!
override func setUp() {
super.setUp()
viewModel = ViewModel(movieService: MockMovieService())
}
func testFetchMovies() {
let fetchMoviesExpectation = expectation(description: "fetchMovies")
viewModel.fetchMovies(page: 1) {
XCTAssertEqual(self.viewModel.movie.count, 2)
XCTAssertEqual(self.viewModel.movie[0].title, "Movie1")
XCTAssertEqual(self.viewModel.movie[0].posterPath, "/path1")
XCTAssertEqual(self.viewModel.movie[0].releaseDate, "2021-01-01")
XCTAssertEqual(self.viewModel.movie[0].voteAverage, 1.0)
XCTAssertEqual(self.viewModel.movie[0].overview, "overview1")
XCTAssertEqual(self.viewModel.movie[1].title, "Movie2")
XCTAssertEqual(self.viewModel.movie[1].posterPath, "/path2")
XCTAssertEqual(self.viewModel.movie[1].releaseDate, "2021-01-02")
XCTAssertEqual(self.viewModel.movie[1].voteAverage, 2.0)
XCTAssertEqual(self.viewModel.movie[1].overview, "overview2")
fetchMoviesExpectation.fulfill()
}
waitForExpectations(timeout: 1, handler: nil)
}
func testDownloadImage() {
let downloadImageExpectation = expectation(description: "downloadImage")
viewModel.downloadImage(posterPath: "/path1") { result in
switch result {
case .success(let image):
XCTAssertNotNil(image)
XCTAssertEqual(image.size, CGSize(width: 1280.0, height: 871.0))
case .failure(_):
XCTFail("fail")
}
downloadImageExpectation.fulfill()
}
waitForExpectations(timeout: 1, handler: nil)
}
}
나머지 sort관련 method도 위와 같은 방법으로 작성하였음.
class MockMovieService: MovieService {
static var mockMovies: [Movie]{
let movieA = Movie(title: "Movie1", posterPath: "/path1", releaseDate: "2022-01-01", voteAverage: 1.0, overview: "overview1")
let movieB = Movie(title: "Movie2", posterPath: "/path2", releaseDate: "2022-01-02", voteAverage: 2.0, overview: "overview2")
let movieC = Movie(title: "Movie3", posterPath: "/path3", releaseDate: "2022-01-03", voteAverage: 3.0, overview: "overview3")
let movieResponse = [movieA, movieB, movieC]
return movieResponse
}
// 중략
}
final class MovieRankTests: XCTestCase {
var viewModel: ViewModel!
override func setUp() {
super.setUp()
viewModel = ViewModel(movieService: MockMovieService())
}
// 중략
func testSortMoviesByTitle(){
viewModel.movie = MockMovieService.mockMovies
viewModel.sortMoviesByTitle()
XCTAssertEqual(viewModel.movie[0].title, "Movie1")
XCTAssertEqual(viewModel.movie[1].title, "Movie2")
XCTAssertEqual(viewModel.movie[2].title, "Movie3")
}
func testSortMoviesByReleaseDate() {
viewModel.movie = MockMovieService.mockMovies
viewModel.sortMoviesByReleaseDate()
XCTAssertEqual(viewModel.movie[0].releaseDate, "2022-01-01")
XCTAssertEqual(viewModel.movie[1].releaseDate, "2022-01-02")
XCTAssertEqual(viewModel.movie[2].releaseDate, "2022-01-03")
}
func testSortMoviesByVoteAverage(){
viewModel.movie = MockMovieService.mockMovies
viewModel.sortMoviesByVoteAverage()
XCTAssertEqual(viewModel.movie[0].voteAverage, 3.0 )
XCTAssertEqual(viewModel.movie[1].voteAverage, 2.0)
XCTAssertEqual(viewModel.movie[2].voteAverage, 1.0)
}
}
해당 코드에서 원하는 값이 잘 출력됨을 확인할 수 있었음.