인앱 결제 상태 머신에서 가장 재현하기 어려운 것은 보통 ‘구매 성공’이 아니라 취소, 보류, 환불, 그리고 이전 거래가 완료되지 않은 상태입니다. 이러한 분기를 외부 거래 환경에 그대로 맡기면 네트워크, 상품 구성, 테스트 계정 상태가 결과에 영향을 미칩니다. 더 안정적인 방법은 먼저 클라우드 Mac에서 StoreKit 구성 파일로 상품을 고정한 다음, SKTestSession으로 거래 조건을 제어하여 클라이언트 로직을 반복 실행 가능한 회귀 테스트로 만드는 것입니다.
오프라인 테스트의 범위부터 명확히 하기
StoreKit 구성은 상품 식별자 매핑, 구매 상태 전환, 권한 갱신, 오류 안내, 거래 완료 로직을 검증하는 데 적합합니다. 외부 결제 경로에 접근할 필요가 없으며, 실패도 더 쉽게 재현할 수 있습니다.
하지만 프로덕션 상품이 올바르게 구성되었는지는 증명할 수 없으며, 서버 알림, 실제 영수증 검증, 최종 결제 화면도 다루지 못합니다. 테스트를 다음 세 계층으로 나누는 것이 좋습니다.
| 계층 | 주요 목표 | 실행 빈도 |
|---|---|---|
| 단위 테스트 | 권한 계산, 상태 매핑 | 커밋마다 |
| StoreKit 통합 테스트 | 구매, 취소, 환불, 미완료 거래 | 병합마다 |
| 외부 환경 인수 테스트 | 상품 구성, 알림 및 영수증 경로 | 출시 전 |
오프라인 테스트의 가치는 피드백 루프를 단축하는 데 있으며, 모든 거래 위험을 하나의 녹색 체크 표시로 위장하는 데 있지 않습니다.
상품과 테스트 진입점 고정하기
Xcode에서 StoreKit Configuration File을 만들고 테스트에 필요한 상품만 남깁니다. 상품 식별자는 코드 상수와 일치해야 합니다. 가격은 UI 테스트에만 사용하고, 비즈니스 로직에서 현지화된 가격 문자열을 기준으로 권한을 판단해서는 안 됩니다.
구성 파일을 테스트 Scheme에 추가하고 StoreKitRegression.xctestplan과 같은 별도의 테스트 계획을 만듭니다. 개발자가 평소 실행하는 Scheme을 재사용하지 마십시오. 수동 변경 한 번으로 CI 조건이 눈에 띄지 않게 달라질 수 있습니다.
테스트 대상은 setUpWithError()에서 세션을 새로 만들 수 있습니다.
import StoreKitTest
import XCTest
final class PurchaseRegressionTests: XCTestCase {
private var session: SKTestSession!
override func setUpWithError() throws {
session = try SKTestSession(configurationFileNamed: "Products")
session.disableDialogs = true
session.clearTransactions()
session.failTransactionsEnabled = false
}
override func tearDownWithError() throws {
session.clearTransactions()
session = nil
}
}
구성 파일 이름에는 확장자를 포함하지 않습니다. 초기화에 실패하면 먼저 파일이 테스트 대상에 포함되어 있는지 확인하고, Scheme이 동일한 구성을 사용하는지도 점검합니다.
거래 분기를 독립적인 테스트 케이스로 분리하기
하나의 긴 테스트 케이스에서 성공, 환불, 취소를 연달아 테스트하지 마십시오. 상태가 많을수록 실패 후 어느 단계에서 오염이 발생했는지 판단하기 어렵습니다. 최소한 다음 케이스로 분리해야 합니다.
정상 구매와 복원
구매 직후 권한이 갱신되는지 확인하고, 비즈니스 계층 객체를 다시 생성한 후에도 유효한 권한을 읽을 수 있는지 검증합니다. 테스트가 끝나기 전에 앱이 거래를 처리하고 완료했는지 확인하여, 다음 테스트 케이스가 이전 업데이트를 다시 받지 않도록 합니다.
취소와 실패 주입
취소 시 ‘결제 실패’가 표시되어서는 안 되며, 잠금 해제 상태가 기록되어서도 안 됩니다. 네트워크 오류나 일반적인 거래 오류가 발생한 경우에는 재시도 경로를 유지해야 합니다. 세션 속성으로 오류를 주입한 뒤에는 현재 테스트가 끝나기 전에 기본값으로 되돌려야 하며, 다음 테스트 케이스의 정리 작업에 의존해서는 안 됩니다.
환불과 미완료 거래
환불 후에는 해당 권한을 취소하되, 여전히 유효한 다른 상품까지 잘못 삭제해서는 안 됩니다. 미완료 거래의 경우 버튼 콜백을 한 번 확인하는 데 그치지 말고, 앱을 재시작한 후에도 처리를 계속할 수 있는지 검증해야 합니다.
동일한 상품 상태를 다루는 테스트는 직렬로 실행하는 것이 좋습니다. 병렬 테스트가 시뮬레이터와 구성을 공유하면 서로의 거래를 정리하기 쉬워 CI에서만 나타나는 간헐적 실패로 이어질 수 있습니다.
명령줄에서 실행 조건 고정하기
먼저 사용 가능한 시뮬레이터를 확인한 다음, 기기 이름, 시스템 버전, Xcode 경로를 CI 변수에 지정합니다. xcodebuild가 임의의 대상을 자동으로 선택하게 두지 마십시오.
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
RESULT_DIR="$PWD/TestResults"
rm -rf "$RESULT_DIR"
mkdir -p "$RESULT_DIR"
xcodebuild test \
-workspace App.xcworkspace \
-scheme App-StoreKitTests \
-testPlan StoreKitRegression \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath "$RESULT_DIR/StoreKit.xcresult"
OakVM에서 여러 파이프라인을 동시에 실행한다면 작업마다 별도의 작업 디렉터리와 시뮬레이터를 할당하고, DerivedData, 결과 디렉터리 또는 이미 실행 중인 기기를 공유하지 마십시오. 시작 전에 xcrun simctl shutdown all을 실행할 수 있지만, 같은 노드의 다른 작업에도 영향을 주므로 노드 하나를 작업 하나가 독점하는 실행 방식에만 적합합니다.
증거 자료 보관 및 실패 기준 설정하기
테스트가 실패했을 때 콘솔의 마지막 수십 줄만 저장해서는 대개 충분하지 않습니다. xcresult, 커밋 해시, StoreKit 구성 파일 체크섬, Xcode 버전, 시뮬레이터 런타임 버전을 보관해야 합니다. 구성 파일 변경도 코드 리뷰 대상에 포함하여 상품 식별자가 의도치 않게 삭제되지 않도록 해야 합니다.
먼저 환경 요약을 기록할 수 있습니다.
xcodebuild -version
xcrun simctl list runtimes
shasum -a 256 Tests/StoreKit/Products.storekit
점검 목록에는 다음 항목이 포함되어야 합니다.
- 각 테스트가 자체
SKTestSession을 생성하고 정리하는가 - 무인 작업이 중단되지 않도록 자동 대화상자를 비활성화했는가
- 취소, 실패, 환불에 대해 UI 상태와 권한 상태를 각각 검증하는가
- 테스트가 실행 순서나 이전 거래에 의존하는가
- 실패 후에도
xcresult가 업로드되는가 - 로그에서 민감한 자격 증명과 전체 거래 데이터를 제외하는가
이러한 조건을 고정하면 인앱 결제 회귀 테스트는 더 이상 수동 클릭에 의존하지 않습니다. 외부 환경은 반드시 외부에서 검증해야 하는 부분만 담당하고, 일상적인 커밋은 빠르고 격리되어 있으며 추적 가능한 테스트로 클라이언트 상태 머신을 보호할 수 있습니다.
자주 묻는 질문
StoreKit 오프라인 테스트만으로 실제 결제 검증을 끝낼 수 있나요?
아니요. 클라이언트 상태 처리와 오류 분기는 검증할 수 있지만 실제 상품 설정, 서버 알림, 영수증 처리와 결제 화면은 별도의 테스트 환경에서 확인해야 합니다.
전체 테스트에서만 인앱 결제 테스트가 간헐적으로 실패하는 이유는 무엇인가요?
이전 테스트의 거래 상태가 남았는지 먼저 확인해야 합니다. 테스트마다 새 SKTestSession을 만들고 거래를 초기화하며 동일 상품을 다루는 테스트는 직렬로 실행하는 편이 안전합니다.
CI 실패 시 어떤 자료를 보관해야 하나요?
xcresult, 테스트 로그, StoreKit 구성 버전, 커밋 해시, Xcode 버전과 시뮬레이터 런타임 버전을 함께 보관해야 원인을 재현하기 쉽습니다.
다음 빌드를 독립 물리 노드에서 실행하세요
OakVM M4 또는 OakVM M4 Pro를 선택하고, 가상화되지 않은 클라우드 Mac을 일·주·월·분기 단위로 대여하세요. 노드의 실제 이용 가능 여부는 콘솔에서 실시간으로 확인할 수 있습니다.