다음 글을 번역하고 적용한 내용입니다.
Unit Testing View Controllers and Views in Swift
TDD 관련한 영상을 보다가 Unit Test로 View, ViewController를 테스트하는 장면을 보았다. View를 테스트한다고? 의문이 들었지만 해당 글을 읽으면서 정리가 되었다. 다음 세 가지 내용을 정리하며 View 테스트에 대해 알아보자.
ViewController와 View는 codebase의 아주 큰 부분을 차지하기 때문에 아주 중요하다. 결국 우리 앱의 퀄리티를 증진시키는 중요한 요소다. View를 테스트하는 일반적인 방법은 user interface를 snapshot test로 테스트하는 것이다. 시각적으로는 좋지만 그 안에 들어있는 contents에 대한 테스트는 할 수 없다. UI는 테스트될 필요가 없다. 다만 contents는 테스트되어야 하고 이를 위해서 무엇을 테스트해야 할지를 명확하게 아는 것이 중요하다.
보기 좋은 것을 테스트하는 건 뭐.. 불가능하다. 의미도 없다.
다음은 적절한 테스트 대상이다.
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")
}
끝!