자동화된 테스트는 iOS 앱 개발의 중요한 부분으로, 재발 방지의 안전망 역할을 하며 코드를 개발하는 견고한 방법을 제공합니다. 이 기사에서는 iOS 앱을 위한 UI 테스트를 효과적으로 작성하는 방법을 살펴보겠습니다.
좋은 테스트의 구조
iOS 앱에 새로운 기능을 개발할 때, 예를 들어 API에서 로컬 날씨 데이터를 가져와 표시하는 버튼을 추가하는 경우, 좋은 테스트를 작성하고 구조화하는 것이 중요합니다. 단위 테스트 작성에 대한 두 가지 접근 방식을 살펴보겠습니다.
클래스 및 함수를 기반으로 한 테스트
한 가지 접근 방식은 작성하려는 클래스 및 함수와 긴밀하게 연결된 테스트를 작성하는 것입니다. 예를 들어, API 요청을 수행하는 네트워크 서비스를 구축 중이라면 해당 서비스에 특화된 테스트를 작성합니다. 그러나 이 방법은 테스트를 더불어 지속 가능한 코드 유지 관리와 리팩터링을 방해할 수 있습니다.
NetworkService:
class NetworkService {
func fetchData(from url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) inif let error = error {
completion(.failure(error))
} else if let data = data {
completion(.success(data))
} else {
let unknownError = NSError(domain: "NetworkService", code: 0, userInfo: nil)
completion(.failure(unknownError))
}
}
task.resume()
}
}
테스트에서는 다양한 시나리오를 커버하기 위해 UrlSession을 모킹해야 합니다. 이를 달성하기 위해 테스트에서 목 인스턴스를 생성하기 위한 중복된 프로토콜 또는 인터페이스를 만들어야 합니다. 더 중요한 것은 이러한 방식으로 테스트를 코드에 묶어버린다는 것입니다. 이것은 큰 응용 프로그램을 가로지르는 경우 상상해보세요. 각각 자체 테스트가 있는 100개의 클래스와 예상대로 작동하는지 확인하는 더 많은 테스트가 있을 수 있습니다. 이제 작은 변경을 가했다고 상상해보세요. 더 나은 방법을 발견하고 몇 개의 클래스를 리팩터링했습니다. 이제 얼마나 많은 테스트가 실패할까요? 아마도 많을 것입니다. 앱의 동작이 변경되지 않았다면 왜 테스트가 실패해야 하죠? 이러한 접근 방식을 따르면 리팩터링을 어렵게 만들고 코드베이스 개선을 어렵게 합니다.
동작을 기반으로 한 테스트
먼저 일반적인 조건 하에서 앱이 예상대로 작동하는 "happy path" 테스트를 작성하는 것으로 시작합니다:
버튼을 누르고 맑은 날씨 데이터가 반환되면, 맑은 날씨 아이콘이 표시되는지 확인합니다.
버튼을 누르고 비오는 날씨 데이터가 반환되면, 비오는 날씨 아이콘이 표시되는지 확인합니다.
이러한 동작 기반의 테스트를 통해 에지 케이스에 빠지지 않고 앱의 대부분 기능을 구축할 수 있습니다. 핵심 기능이 갖춰진 후에는 더 복잡한 시나리오를 탐구할 수 있습니다:
버튼을 누르고 API 오류가 발생하면, 오류 메시지가 표시되는지 확인합니다.
버튼을 누르고 API가 데이터를 반환하지 않으면, 친절한 기본 메시지가 표시되는지 확인합니다.
이러한 테스트는 앱의 내부 구현 세부사항을 파헤치지 않고 기대되는 결과에 중점을 둡니다.
앱 함수 삽입
동작을 기반으로 한 테스트를 작성하려면 부작용 함수를 삽입할 수 있는 방식으로 앱을 구조화해야 합니다. 함수로 의존성을 삽입함으로써 코드의 유지 관리와 테스트 가능성을 보장할 수 있습니다.
func buttonPressed(appF: AppFunctions) {
let weatherResponse = appF.fetchWeatherData()
if(weatherResponse.successful) {
// render weather
} else {
// show error message
}
}
우리의 예에서, fetchWeatherData는 외부 API를 호출하는 부작용 함수로 작용합니다. 따라서 우리는 이를 테스트를 위해 조작할 수 있는 기능을 삽입하고 싶습니다. 이것은 "제어의 역전"이라는 깨끗한 코딩 원칙이며, 함수 스타일로 쓰는 것을 좋아하기 때문에 주입하는 종속성은 함수입니다.
iOS에서 의존성 주입 수행 방법
iOS에서 UI 테스트에서 의존성 주입을 수행하는 것은 진입점 부재로 인해 어려울 수 있습니다. 대신, 클래스를 JSON으로 인코딩하고 테스트 목적으로 AppDelegate에서 가로채는 방법을 사용할 수 있습니다. iOS는 함수의 직접적인 주입을 지원하지 않지만 인코딩 가능한 필드가 있는 클래스를 인코딩하여 함수 동작을 모방할 수 있습니다. 여기에 우리가 그것을 하는 방법이 있습니다. AppFunctions 프로토콜을 가지고 있습니다:
protocol AppFunctions {
func fetchWeatherData() -> Repsonse
}
그런 다음 실제 AppF:
class AppF: AppFunctions {
func fetchWeatherData() -> Repsonse {
// code that makes api call
return response
}
}
그런 다음 우리의 테스트 버전인 Response를 설정할 수 있게 해주는 TestAppF가 있습니다:
class TestAppF: AppFunctions, Codable {
private var response: Response? = nil
init(response: Response) {
self.response = response
}
func fetchWeatherData() -> Response {
return response!
}
}
우리의 테스트에서는 앱을 시작하고 인코딩된 TestAppF를 삽입합니다:
let testAppF = createTestF()
app.launchEnvironment["InjectedAppF"] = testAppF.json
app.launch()
여기서 .json은 TestAppF를 JSON 문자열 표현으로 인코딩합니다:
extension Encodable {
var json: String? {
guard let data = data else {
return nil
}
return String(data: data, encoding: .utf8)
}
var data: Data? {
return try! JSONEncoder().encode(self)
}
}
그런 다음 우리의 AppDelegate에서 JSON을 추출하고 AppFunctions으로 디코딩합니다:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var appF: AppFunctions?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
appF = ProcessInfo.processInfo.decodeAppF() ?? AppF()
return true
}
...
}
여기서 decodeAppF는 ProcessInfo에 대한 확장(extension)입니다:
extension ProcessInfo {
func decodeAppF() -> TestAppF? {
guard
let environment = environment["InjectedAppF"],
let codable = TestAppF.decode(from: environment) else {
return nil
}
return codable
}
}
이러한 원칙을 따라 iOS 앱에 대한 깨끗하고 동작 중심의 테스트를 작성하여 코드 품질과 테스트 가능성을 보장할 수 있습니다.
지원 방법
이 콘텐츠는 항상 무료로 유지될 것이며, 가치 있다고 판단된다면 다른 사람과 공유하는 것을 고려해 주십시오. 게다가, 우리의 게임을 다운로드하고 솔직한 리뷰를 남겨 주시면 저희를 크게 지원해 주실 것입니다. 궁금한 점이나 피드백이 있다면 언제든지 문의해 주시고, 최선을 다해 답변 드리겠습니다.
오늘 Apple App Store에서 Falling Sky를 다운로드하세요: https://apps.apple.com/app/id6446787964
이 블로그 게시물에 감사드립니다: https://medium.com/egym-developer/painless-ui-testing-in-ios-part-1-mocking-the-network-ffbd6ab4809a