TDD + Unit Test + Protocol(2)

주방·2023년 5월 6일
0

MovieRank

목록 보기
6/8
post-thumbnail
  • TDD와 아이들(1)에 이어 코드를 통해 어떻게 구현할 지 실습시간을 가져봄

TDD 적용(2) - 코드

  1. View Model에 대한 Unit Test를 진행하기 위해 Mock Data가 필요함.

  2. MovieService 프로토콜을 구현하여, APIManager - 프로토콜 준수, ViewModel - 의존성 주입하여 결합도를 낮추고자 하였음.

  3. 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))
                }
            }
        }
    }
    
  4. APIManager는 MovieService 프로토콜을 준수하고 있음.

  5. ViewModel에 MovieService 의존성 주입을 함으로써 해당 프로토콜을 준수하는 구현체를 사용할 수 있고, Unit Test를 위한 Mock 데이터를 주입할 수 있게 됨.

  6. 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)")
                    }
                }
            }
        }
    }
  7. 테스트를 위한 Mock 데이터 생성

  8. 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))
            }
        }    
    }
    
  9. fetchMovies, downloadImage 메소드 테스트 코드 작성

  10. viewModel의 경우, MockMovieService를 사용하였음.

  11. viewModel의 fetchMovies 메소드의 retrun 값이 XCTAssertEqual method를 활용해서 출력되는 값이 동일하게 나오는지 체크할 수 있었음.

  12. 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)
        }
    }
    
  13. 나머지 sort관련 method도 위와 같은 방법으로 작성하였음.

  14. 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)
        }
    }
  15. 해당 코드에서 원하는 값이 잘 출력됨을 확인할 수 있었음.


정리

  1. TDD와 Unit Test에 대한 혼동이 있었음을 알게됨.
  2. 개발 방법론과 적용 방법이 차이였으며, 실제 적용을 통해 남은 리팩토링 과정에서 어떻게 활용할 수 있을지 생각하게 됨.
  3. TDD를 주제로 작업을 진행했지만, 막상 많은 고민을 하게 된 것은 Protocol 활용을 통한 의존성 주입이라는 개념이었음.
  4. 리팩토링 전까지 클래스에 대한 인스턴스를 사용해 구현을 하였음.
  5. 그러다보니 결합도가 높아졌지만, 많은 기능이 구현되어 있지 않은 탓에 큰 문제가 없었음.
  6. 그러나 이와 같이 테스트를 구현하더라도 의존성 주입이 편리함을 알 수 있었음.
  7. 의존성 주입을 통해 viewModel이 실제 API를 호출하는 것인지, 가짜 데이터로 확인하는 것인지 알지 못하더라도 동작됨을 확인할 수 있었음.
  8. 처음 TDD 학습 및 적용에서 의존성 주입에 대해서 고려하지 못했으나, 예상치 못한 학습 주제였음.

0개의 댓글