ViewController와 View 테스트하기

박형석·2021년 12월 22일
0

Unit Test

목록 보기
3/3
post-thumbnail

다음 글을 번역하고 적용한 내용입니다.
Unit Testing View Controllers and Views in Swift

시작하며

TDD 관련한 영상을 보다가 Unit Test로 View, ViewController를 테스트하는 장면을 보았다. View를 테스트한다고? 의문이 들었지만 해당 글을 읽으면서 정리가 되었다. 다음 세 가지 내용을 정리하며 View 테스트에 대해 알아보자.

  1. ViewController와 View를 테스트하는 이유
  2. 무엇을 테스트하고 또 하지 말아야 하는가
  3. UIViewController를 testable하게 설계하는 방법

ViewController와 View를 테스트하는 이유

ViewController와 View는 codebase의 아주 큰 부분을 차지하기 때문에 아주 중요하다. 결국 우리 앱의 퀄리티를 증진시키는 중요한 요소다. View를 테스트하는 일반적인 방법은 user interface를 snapshot test로 테스트하는 것이다. 시각적으로는 좋지만 그 안에 들어있는 contents에 대한 테스트는 할 수 없다. UI는 테스트될 필요가 없다. 다만 contents는 테스트되어야 하고 이를 위해서 무엇을 테스트해야 할지를 명확하게 아는 것이 중요하다.

무엇을 테스트할 것인가?

보기 좋은 것을 테스트하는 건 뭐.. 불가능하다. 의미도 없다.

  • background color
  • UILabel text color, font, size
  • authlayout

다음은 적절한 테스트 대상이다.

  • 테이블뷰의 row의 갯수
  • UILabel의 text 정확도
  • BUtton의 enable
  • UIView의 frame

ViewController Testable하게 만들기

ViewController를 testable하게, passive하게 만들기 위해서는 DI가 필요하다. 유저와의 상호작용과 UI를 rendering하는데 있어서 수동적이라면 testable한 vc라고 볼 수 있다. 예를 들어, MVP, MVVM사용예를 들 수 있다.

protocol ArtistDetailPresenter {
    func onViewLoaded()
    func onEdit()
}

class ArtistDetailViewController: UIViewController {
    
    // IBOutlets
    
    var presenter: ArtistDetailPresenter!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.onViewLoaded()
    }
    
    @IBAction func onEdit(_ sender: UIBarButtonItem) {
        presenter.onEdit()
    }
}
struct ArtistDetailProps {
    let title: String
    let fullName: String
    let numberOfAlbums: String
    let numberOfFollowers: String
}

// 핵심은 ViewController가 스스로를 랜더링하지 않는다는 것이다. 그래서 수동성을 확보한다. 
// 이건 MVP에서 Presenter의 책임이다. 
protocol ArtistDetailComponent: AnyObject {
    func render(_ props: ArtistDetailProps)
}

extension ArtistDetailViewController: ArtistDetailComponent {
    func render(_ props: ArtistDetailProps) {
        navigationItem.title = props.title
        fullNameLabel.text = props.fullName
        numberOfAlbumsLabel.text = props.numberOfAlbums
        numberOfFollowersLabel.text = props.numberOfFollowers
    }

테스트하기

이제 남은건 present에 해당하는 의존성을 주입해주는 것이다.
먼저, ViewController에 넣을 Presenter를 Mocking한다.

class ArtistDetailPresenterMock: ArtistDetailPresenter {
    
    private(set) var onViewLoadedCalled = false
    
    func onViewLoaded() {
        onViewLoadedCalled = true
    }
    
    private(set) var onEditCalled = false
    
    func onEdit() {
        onEditCalled = true
    }
}

테스트를 위해 초기화와 종속성은 연결해보자.

class ArtistDetailViewControllerTests: XCTestCase {
    let presenter = ArtistDetailPresenterMock()
    // ViewController를 만들어주는 
    func makeSUT() -> ArtistDetailViewController {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let sut = storyboard.instantiateViewController(identifier: "ArtistDetailViewController") as! ArtistDetailViewController
        // property 의존성 주입
        sut.presenter = presenter
        sut.loadViewIfNeeded()
        return sut
    }
}

이제 테스트를 작성해보자.

func testViewDidLoadCallsPresenter() {
    let sut = makeSUT()
    
    sut.viewDidLoad()
    
    XCTAssertTrue(presenter.onViewLoadedCalled)
}

// 프레젠터의 함수 정상작동 확인
func testOnEditCallsPresenter() {
    let sut = makeSUT()
    
    sut.onEdit(.init())
    
    XCTAssertTrue(presenter.onEditCalled)
}

// View가 잘 랜더링 되었는지 확인
func testRender() {
    let props = ArtistDetailProps(title: "TITLE", fullName: "NAME", numberOfAlbums: "1", numberOfFollowers: "2")

    let sut = makeSUT()

    sut.render(props)

    XCTAssertEqual(sut.navigationItem.title, "TITLE")
    XCTAssertEqual(sut.fullNameLabel.text, "NAME")
    XCTAssertEqual(sut.numberOfAlbumsLabel.text, "1")
    XCTAssertEqual(sut.numberOfFollowersLabel.text, "2")
}

끝!

profile
IOS Developer

0개의 댓글