SwiftUI 테스트하기

  • 클라이언트 코드의 단위 테스트를 작성하다 보면 보통 뷰모델을 테스트하게 된다. 그러나 SwiftUI를 사용하면서뷰모델의 존재를 멀리하던 나로서는, 그동안 뷰모델을 대상으로 행해지던 테스트를 다른 객체로 밀어내거나 뷰로 가져왔고, 순수함수만을 가지고 단위테스트를 작성해 왔다.
  • 그냥 뷰 객체를 만들어 테스트하면 안되나? 답답한 마음에 다음과 같은 시도를 해 보았다.
import SwiftUI
 
struct TestView: View {
    @State var text: String = "1"
 
    var body: some View {
        Text(self.text)
    }
 
    func mutate() {
        self.text = "2"
    }
}
 
import Testing
 
struct Tests {
    @Test 
    @MainActor // View의 생성자 등이 메인액터 isolated라서 그냥 만들면 안된다.
    func test() async throws {
        var v = TestView()
        v.mutate()
        print(v.text)
        #expect(v.text == "2")
    }
}

실패

  • 안타깝게도 테스트 케이스의 Expectation은 실패하고, 콘솔에는 mutate하기 전인 초기값으로 출력된다. 이런 식으로 테스트를 작성할 수 없다는 결론이 나온다.
  • 값이 왜 안 바뀌는지 다시 한번 잘 생각해보면, @State가 붙어있기 때문이라고 추측할 수 있다. 나는 값이 변하기를 원했지만 사실 난 View 구조체와 그 프로퍼티를 변경할 수 없다. 코드를 돌아보면 내 mutate 함수는 mutating function이 아니고, 심지어 나는 TestView를 var로 선언하지도 않은 것을 볼 수 있다.
  • 그럼에도 불구하고 내가 text = “2” 의 대입 연산을 사용할 수 있다는 것은, propertyWrapper의 동작인데, SwiftUI의 @State 안에서 어떠한 일이 일어나는지는 나는 정확히 알 수가 없다. 대입하자마자 값을 뽑아서 다르다고 한들 불평할 수가 없다…

Test는 그래서

  • 결국 기존과 크게 달라질게 없다. 다만 요즘 비대해진 뷰와 오갈데 없이 흩뿌려진 객체들을 보면 뷰모델이 조금 그리운데, iOS 17.0부터 사용할 수 있는 옵저버블과 함께라면 상태값을 전부 밀어넣고 테스트할 수 있지 않나 싶다. 뷰 리렌더링 사이클도 개선되었고, 아키텍처를 정하는 입장에서 뷰에서만 사용할 수 있는 어노테이션들의 위치를 완벽히 통일하고 싶었는데 현실적으로 뷰에 남겨둬도 문제가 없을 것 같기도 해서(@AppStorage같은건 그냥 평생 안쓸까 싶기도 하고)…조금 더 고민해 봐야겠다.