SwiftUI 테스트 - 실행 환경(Launch Environment) 설정하기
RECOMMEND POSTS BEFORE THIS
0. 들어가면서
이전 글 예제를 보면 테스트에서 주입한 환경 변수가 구현 코드에서 사용됐다. 이 부분에 대한 내용을 자세히 다루지 않았기 때문에 이번 글에서 정리한다.
1. UI Test Launch Environment
launchEnvironment는 XCUITest가 앱을 실행할 때 테스트 전용 환경 변수를 주입하는 기능이다. UI 테스트에서 이번 애플리케이션 실행에 필요한 조건을 알려주는 방법이다. 테스트 코드에서는 다음과 같이 환경 변수를 설정한다.
let app = XCUIApplication()
app.launchEnvironment["APP_ENV"] = "uiTest"
app.launchEnvironment["INITIAL_ROUTE"] = "call"
app.launchEnvironment["API_BASE_URL"] = "http://localhost:8080"
app.launch()
실제 애플리케이션에서는 다음과 같이 ProcessInfo 객체에서 꺼내 사용한다.
let environment = ProcessInfo.processInfo.environment
let appEnv = environment["APP_ENV"]
let initialRoute = environment["INITIAL_ROUTE"]
let apiBaseURL = environment["API_BASE_URL"]
여러 가지 용도로 사용할 수 있다.
- 특정 화면에서 애플리케이션이 시작하도록 라우팅 제어
- 피처 플래그(feature flag), 서버 주소처럼 환경마다 구분이 필요한 환경 변수 제어
UI 테스트는 단위 테스트(unit test)와 달리 직접 원하는 상태나 테스트 더블(test double)을 주입할 수 없다. 에뮬레이터에서 애플리케이션을 실행하는 방식이기 때문에 앱 내부 메모리에 접근할 수 없기 때문이다. 애플리케이션과 UI 테스트 사이의 경계를 넘어 필요한 컨텍스트를 설정하기 위해 launchEnvironment를 사용한다.
UI Test Process
│
│ launchEnvironment
│
▼
App Process
2. How to use launch environment?
launchEnvironment를 어떻게 사용하면 좋을까? 나는 다음과 같은 방식을 사용했다.
- 애플리케이션을 시작할 때 한 번 읽어서 하나의 모듈에 격리한다.
- environment() 함수를 통해 이전 단계에서 만든 설정 모듈을 애플리케이션 환경 값으로 등록한다.
- 뷰(view)에서 필요한 환경 값을 꺼내 사용한다.
지금부터 예시 코드를 살펴보자. 테스트 코드에서 다음과 같이 환경 변수를 설정했다고 가정한다.
let app = XCUIApplication()
app.launchEnvironment["POKEMON_URL"] = url
app.launch()
launchEnvironment를 불러오는 AppConfig 구조체를 만드는 것부터 시작한다. AppConfig 객체에 필요한 프로퍼티를 설정한 후 반환하는 loadEnvironment() 정적 함수를 만든다.
- 환경 변수를 통해 주입받고 싶은 프로퍼티를 만든다. 기본값은 운영 환경(혹은 개발 환경)에서 사용하는 값으로 지정한다.
- 컴파일 설정(Build Configuration)이 디버그(debug)인 경우에만 실행되도록
#if DEBUG블록으로 환경 변수를 불러오는 코드를 감싼다. 테스트의 컴파일 설정은 디버그로 지정된다.
struct AppConfig {
var url: String = "https://pokeapi.co"
static func loadEnvironment() -> AppConfig {
var appConfig = AppConfig()
#if DEBUG
let environment = ProcessInfo.processInfo.environment
if let url = environment["POKEMON_URL"] {
appConfig.url = url
}
#endif
return appConfig
}
}
위에서 만든 loadEnvironment() 정적 함수를 호출해서 뷰의 환경 값으로 등록해야 한다. 이때 environment() 함수를 사용한다. EnvironmentKey 타입의 구조체를 하나 만들고, EnvironmentValues 구조체를 확장해야 한다. 먼저 EnvironmentKey 타입의 새로운 구조체를 선언한다.
- AppConfigurationKey 구조체는 Environment에 값을 저장하고 꺼낼 때 사용하는 키 타입이다.
- 이름표 역할만 수행하는 것이 아니라, 해당 키에 값이 아직 주입되지 않았을 때 사용할 기본값까지 함께 정의한다.
private struct AppConfigurationKey: EnvironmentKey {
static let defaultValue = AppConfig()
}
다음으로 EnvironmentValues 구조체를 확장해서 appConfig이라는 커스텀 환경 값 접근 프로퍼티를 추가한다. environment() 함수를 통해 환경 값을 주입하려면 EnvironmentValues 구조체를 확장한 프로퍼티가 필요하다.
- EnvironmentValues 객체는 내부적으로 키-값 저장소처럼 동작한다.
- EnvironmentValues 객체의 appConfig 프로퍼티를 통해 AppConfigurationKey 타입에 해당하는 환경 값을
get/set할 수 있도록 만든다.
extension EnvironmentValues {
var appConfig: AppConfig {
get { self[AppConfigurationKey.self] }
set { self[AppConfigurationKey.self] = newValue }
}
}
두 타입의 정의를 마치면 뷰에 AppConfig 객체를 주입할 수 있다.
@main struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.appConfig, .loadEnvironment()) // this
}
}
}
환경 값을 주입한 후에는 @Environment 프로퍼티 래퍼(property wrapper)를 통해 환경 값을 꺼내 사용할 수 있다.
struct ContentView: View {
@Environment(\.appConfig) private var appConfig
...
func fetchPokemons() async -> [Pokemon] {
do {
let (data, _) = try await URLSession.shared.data(
from: URL(string: "\(appConfig.url)/api/v2/pokemon")!
)
...
} catch {
return []
}
}
var body: some View {
...
}
}
댓글남기기