Archive/패캠(초격차)

part5 (ch6). MovieReview XCTest 코드리뷰

lgvv 2022. 2. 1. 13:39

✅ 이번 시간에는 테스트코드 리뷰 고고

 

테스트파일 코드 구조

 

🟠 UITest코드인데 BDD를 기반으로 작성한 코드도 있어

//
//  MovieReviewUITests.swift
//  MovieReviewUITests
//
//  Created by Hamlit Jason on 2022/01/17.
//

import XCTest

// UITest의 경우에는 현업에서는 네트워크에 연결되어있어야 앱이 구동되는점, 또한, 여러번 클릭해서 그 지점으로 가서 사용해야하는 단점으로 인해 Unit테스트만 주로 사용한다.
class MovieReviewUITests: XCTestCase {

    var app: XCUIApplication!
    
    override func setUp() {
        super.setUp()
        
        continueAfterFailure = false // 실패하면 코드 쭉 실행안하고 종료
        
        app = XCUIApplication()
        app.launch()
    }
    
    override func tearDown() {
        super.tearDown()
        
        app = nil
    }
    
    func test_navigationBar의_타이틀이_영화평점으로_설정되어있다() {
        let existNavigationBar = app.navigationBars["영화 평점"].exists
        XCTAssertTrue(existNavigationBar)
    }
    
    func test_SearchBar가_존재한다() {
        // 서치바가 네비게이션의 서치필드에 들어있기 때문에
        let existSearchBar = app.navigationBars["영화 평점"]
            .searchFields["Search"]
            .exists
        XCTAssertTrue(existSearchBar)
    }
    
    func test_SearchBar에_cancel버튼이_존재한다() {
        let navigationBar = app.navigationBars
        navigationBar
            .searchFields["Search"]
            .tap()
        
        let existSearchBarCancelButton = navigationBar
            .buttons["Cancel"]
            .exists
        
        XCTAssertTrue(existSearchBarCancelButton)
    }
    
    func test_녹화버튼을_클릭해서_테스트코드_작성() {
        let app = XCUIApplication()
        let navigationBar = app.navigationBars["영화 평점"]
        let searchSearchField = navigationBar.searchFields["Search"]
        searchSearchField.tap()
        app.tables["Empty list"].swipeUp()
        searchSearchField.swipeDown()
        navigationBar.buttons["Cancel"].tap()
        
        let collectionViewsQuery = app.collectionViews
        collectionViewsQuery.children(matching: .cell).element(boundBy: 3).swipeUp()
        collectionViewsQuery.children(matching: .cell).element(boundBy: 2).swipeDown()
    }
    
    // BDD
    enum CellData: String {
        case existsMovie = "셰도우 컨트리"
        case notExistMovie = "007"
    }
    
    func test_영화가_즐겨찾기_되어있으면() {
        let existsCell = app.collectionViews
            .cells
            .containing(.staticText, identifier: CellData.existsMovie.rawValue)
            .element
            .exists
        
        XCTAssertTrue(existsCell, "Title이 표시된 셀이 존재한다")
    }
    
    func test_영화가_즐겨찾기_되어있지_않으면() {
        let existsCell = app.collectionViews
            .cells
            .containing(.staticText, identifier: CellData.notExistMovie.rawValue)
            .element
            .exists
        
        XCTAssertFalse(existsCell, "Title이 표시된 셀이 존재하지 않는다.")
    }
}

enum으로 처리한게 특이점!

 

🟠 MockMovieSearchManager.swift

//
//  MockMovieSearchManager.swift
//  MovieReviewTests
//
//  Created by Hamlit Jason on 2022/02/01.
//

import XCTest
@testable import MovieReview

final class MockMovieSearchManager: MovieSearchManagerProtocol {
    
    var isCalledRequest = false
    
    var needToSuccessRequest = false
    
    // 핸들러는 리퀘스트 함수가 성공해야만 다룰 수 있음으로 이렇게 작성한다.
    func request(
        from keyword: String,
        completionHandler: @escaping ([Movie]) -> Void
    ) {
        isCalledRequest = true
        
        if needToSuccessRequest {
            completionHandler([])
        }
    }
    
}

핸들러를 처리하기 위해 needToSuccessRequest 변수를 따로 둔게 인상적이야.

 

🟠 MockMovieListViewController.swift

//
//  MockMovieListViewController.swift
//  MovieReviewTests
//
//  Created by Hamlit Jason on 2022/02/01.
//
import XCTest
@testable import MovieReview

final class MockMovieListViewController: MovieListProtocol {
    
    var isCalledSetupNavigationBar = false
    var isCalledSetupSearchBar = false
    var isCalledSetupViews = false
    var isCalledUpdateSearchTableView = false
    var isCalledPushToMovieViewController = false
    var isCalledUpdateCollectionView = false
    
    func setupNavigationBar() {
        isCalledSetupNavigationBar = true
    }
    
    func setupSearchBar() {
        isCalledSetupSearchBar = true
    }
    
    func setupViews() {
        isCalledSetupViews = true
    }
    
    func updateSearchTableView(isHidden: Bool) {
        isCalledUpdateSearchTableView = true
    }
    
    func pushToMovieViewController(with movie: Movie) {
        isCalledPushToMovieViewController = true
    }
    
    func updateCollectionView() {
        isCalledUpdateCollectionView = true
    }
}

 

🟠 MockUserDefaultsManager.swift

//
//  MockUserDefaultsManager.swift
//  MovieReviewTests
//
//  Created by Hamlit Jason on 2022/02/01.
//

import XCTest
@testable import MovieReview

final class MockUserDefaultsManager: UserDefaultsManagerProtocol {
    
    var isCalledGetMovies = false
    var isCalledAddMovie = false
    var isCalledRemoveMovie = false
    
    func getMovies() -> [Movie] {
        isCalledGetMovies = true
        return []
    }
    
    func addMovie(_ newValue: Movie) {
        isCalledAddMovie = true
    }
    
    func removeMovie(_ value: Movie) {
        isCalledRemoveMovie = true
    }
}

 

여기까지가 MockList파일인데 특이한 점은 없고 그냥 쓱 보고 지나가자.

 

🟠 MovieListPresenterTest.swift

//
//  MovieListPresenterTest.swift
//  MovieReviewTests
//
//  Created by Hamlit Jason on 2022/02/01.
//

import XCTest
@testable import MovieReview

class MovieListPresenterTest: XCTestCase {
    var sut: MovieListPresenter!
    
    var viewController: MockMovieListViewController!
    var userDefaultManager: MockUserDefaultsManager!
    var movieSearchManager: MockMovieSearchManager!
    
    override func setUp() {
        super.setUp()
        
        viewController = MockMovieListViewController()
        userDefaultManager = MockUserDefaultsManager()
        movieSearchManager = MockMovieSearchManager()
        
        sut = MovieListPresenter(
            viewController: viewController,
            movieSearchManager: movieSearchManager,
            userDefaultsManager: userDefaultManager
        )
    }
    
    override func tearDown() {
        sut = nil
        
        viewController = nil
        userDefaultManager = nil
        movieSearchManager = nil
        
        super.tearDown()
    }
    
    // request 메소드가 성공하면 updateTableView가 실행
    func test_serarchBar_textDidChanger가_호출될때_request가_성공하면() {
        movieSearchManager.needToSuccessRequest = true
        sut.searchBar(UISearchBar(), textDidChange: "")
        
        XCTAssertTrue(viewController.isCalledUpdateSearchTableView, "updateSearchTableView가 실행된다.")
    }
    // request 메소드가 실패하면 updateTableView가 실행되지 않음
    func test_serarchBar_textDidChanger가_호출될때_request가_실패하면() {
        movieSearchManager.needToSuccessRequest = false
        sut.searchBar(UISearchBar(), textDidChange: "")
        
        XCTAssertFalse(viewController.isCalledUpdateSearchTableView, "updateSearchTableView가 실행되지 않는다.")
    }
    
    func test_viewDidLoad가_호출되면() {
        sut.viewDidLoad()
        
        XCTAssertTrue(viewController.isCalledSetupNavigationBar)
        XCTAssertTrue(viewController.isCalledSetupSearchBar)
        XCTAssertTrue(viewController.isCalledSetupViews)
    }
    
    func test_viewWillAppear가_호출되면() {
        sut.viewWillAppear()
        
        XCTAssertTrue(userDefaultManager.isCalledGetMovies)
        XCTAssertTrue(viewController.isCalledUpdateCollectionView)
    }
    
    func test_searchBarDidBeginEditing이_호출되면() {
        sut.searchBarTextDidBeginEditing(UISearchBar())
        
        XCTAssertTrue(viewController.isCalledUpdateSearchTableView)
    }
    
    func text_searchBarCancelButton이_호출되면() {
        sut.searchBarCancelButtonClicked(UISearchBar())
        
        XCTAssertTrue(viewController.isCalledUpdateSearchTableView)
    }
}

// Cuckoo 테스트 파일을 자동으로 만들어주는 외부 프레임워크도 존재

Cuckoo라는 외부 프레임워크도 있다는데 이것도 조사해보자

또한 핸들러를 처리하기 위한 변수를 이용하는 것이 인상적인데 이도 공부해보자

 

🟠 MovieDetailPresenterTest.swift

//
//  File.swift
//  MovieDetailPresenterTest
//
//  Created by Hamlit Jason on 2022/02/01.
//

import XCTest
@testable import MovieReview

class MovieDetailPresenterTest: XCTestCase {
    var sut: MovieDetailPresenter!
    
    var viewController: MockMovieDetailViewController!
    var movie: Movie!
    var userDefaultManager: MockUserDefaultsManager!
    
    override func setUp() {
        super.setUp()
        
        viewController = MockMovieDetailViewController()
        movie = Movie(title: "", imageURL: "", userRating: "", actor: "", director: "", pubDate: "")
        userDefaultManager = MockUserDefaultsManager()
        
        sut = MovieDetailPresenter(
            viewController: viewController,
            movie: movie,
            userDefaultsManager: userDefaultManager
        )
    }
    
    override func tearDown() {
        sut = nil
        
        viewController = nil
        movie = nil
        userDefaultManager = nil
        
        super.tearDown()
    }
    
    func test_viewDidLoad() {
        sut.viewDidLoad()
        
        XCTAssertTrue(viewController.isCalledSetViews)
        XCTAssertTrue(viewController.isCalledSetRightBarButton)
    }
    
    func test_didTapRightBarButtonItem이_호출될때_isLiked가_true가되면() {
        movie.isLiked = false
        sut = MovieDetailPresenter(
            viewController: viewController,
            movie: movie,
            userDefaultsManager: userDefaultManager
        )
        
        sut.didTapRightBarButtonItem()
        
        XCTAssertTrue(userDefaultManager.isCalledAddMovie)
        XCTAssertFalse(userDefaultManager.isCalledRemoveMovie)
        
        XCTAssertTrue(viewController.isCalledSetRightBarButton)
    }
    
    func test_didTapRightBarButtonItem이_호출될때_isLiked가_false가되면() {
        movie.isLiked = true
        sut = MovieDetailPresenter(
            viewController: viewController,
            movie: movie,
            userDefaultsManager: userDefaultManager
        )
        
        sut.didTapRightBarButtonItem()
        
        XCTAssertFalse(userDefaultManager.isCalledAddMovie)
        XCTAssertTrue(userDefaultManager.isCalledRemoveMovie)
        
        XCTAssertTrue(viewController.isCalledSetRightBarButton)
    }
}

movie 데이터를 직접 주기위한 처리방법을 여기서 사용하였다. 이 부분이 상당히 인상적인데, 한번 같이 알아보자.

sut에 movie값을 새로 할당해주는데, 이를 프로토콜로 처음에 작성할 때 만들었기에 이게 가능하다고 보인다.

또한 toggle을 사용했기에 isLiked를 반대로 주어야 didTapRight메소드가 실행되면 toggle을 적용받아 원하는 boolean값을 갖게 되어서 흐름을 처리할 수 있다.

데이터를 넣어주는 것에 주목하자