HireVM 엔지니어링 기록

클라우드 Mac에서 반복 가능한 StoreKit 구매 회귀 테스트 구축

클라우드 Mac에서 반복 가능한 StoreKit 구매 회귀 테스트 구축

인앱 구매 모듈에서 가장 재현하기 어려운 문제는 대개 “버튼이 눌리지 않는” 현상이 아니라 테스트 케이스 간 거래 상태 누수입니다. 첫 번째 테스트에서 구매를 완료한 뒤, 두 번째 테스트는 미구매 화면을 검증해야 하는데 이미 잠금 해제된 상태를 읽어 버리는 식입니다. 클라우드 Mac에서 회귀 테스트를 장기간 실행하려면 시뮬레이터, StoreKit 세션, 앱의 영속 데이터를 함께 격리해야 합니다. 그렇지 않으면 개별 테스트는 통과하지만 전체 테스트 스위트는 실패하는 일이 반복됩니다.

테스트 경계를 먼저 세 계층으로 나누기

첫 번째 계층은 순수 비즈니스 상태 머신입니다. “미구매, 처리 중, 구매 완료, 환불 완료, 만료”를 입력하고 실제 거래를 시작하지 않은 채 기능 접근 권한과 화면 상태를 확인합니다. 두 번째 계층에서는 StoreKit Testing을 사용해 상품 조회, 구매 콜백, 거래 리스너, 구독 상태 변경을 검증합니다. 세 번째 계층은 출시 전 제한된 인수 테스트로, 서버 알림과 실제 상품 설정을 점검할 때만 사용합니다.

이렇게 나누면 대부분의 회귀 테스트를 로컬에서 완료할 수 있으며, 실패 원인이 비즈니스 로직인지, 클라이언트의 거래 연동 계층인지, 외부 설정인지도 명확히 구분할 수 있습니다. 모든 UI 테스트를 구매 버튼 클릭부터 시작하지 마세요. 상태 머신 단위 테스트가 대부분을 차지해야 하며, StoreKit 통합 테스트는 핵심 경로만 다뤄야 합니다.

StoreKit Testing은 제어 가능한 거래 환경을 제공할 뿐, 프로덕션 거래 흐름을 대체하지는 않습니다. 테스트 통과는 주어진 이벤트에 클라이언트가 올바르게 반응한다는 뜻이지, 외부 설정에 대한 인수 검증까지 완료되었다는 뜻은 아닙니다.

검토 가능한 상품 카탈로그 만들기

프로젝트에 StoreKit/Local.storekit을 만들고, 상품 식별자를 코드에서 사용하는 식별자와 정확히 일치시켜야 합니다. 식별자가 뷰와 테스트 곳곳에 흩어지지 않도록 한곳에 모아 정의하는 것이 좋습니다.

enum ProductID {
    static let proMonthly = "com.example.app.pro.monthly"
    static let proYearly = "com.example.app.pro.yearly"
}

설정 파일은 버전 관리에 포함해야 하며, 테스트 가격, 표시 이름, 구독 기간을 변경할 때도 코드 리뷰를 거쳐야 합니다. 팀에서 흔히 겪는 실수는 기존 설정을 복사한 뒤 표시 이름만 바꾸고 상품 식별자는 그대로 두는 것입니다. 이 경우 상품 조회 결과가 빈 배열로 반환됩니다.

테스트 전용 Scheme을 만들고 이름을 StoreKitRegression과 같이 지정한 다음, Scheme의 Run 및 Test 옵션에서 동일한 .storekit 파일을 선택하세요. 개발자 개인 Scheme에 의존해서는 안 됩니다. xcodebuild가 인식하려면 공유 Scheme이어야 합니다.

점검 항목 예상 결과
상품 식별자 설정 파일, 코드, 어설션이 완전히 일치
Scheme Shared로 표시하고 저장소에 커밋
구독 그룹 동일 그룹 내 업그레이드 및 다운그레이드 관계가 명확함
현지화 테스트 어설션이 변경되기 쉬운 표시 문구에 의존하지 않음

모든 테스트 케이스를 깨끗한 세션에서 시작하기

StoreKitTest로 테스트 세션을 만들고 setUp에서 시스템 대화상자를 비활성화한 뒤 거래를 모두 지웁니다. 테스트 메서드가 끝난 후에는 앱이 자체적으로 저장한 권한 캐시도 정리해야 합니다.

import XCTest
import StoreKitTest

final class PurchaseRegressionTests: XCTestCase {
    private var session: SKTestSession!

    override func setUpWithError() throws {
        session = try SKTestSession(
            configurationFileNamed: "Local.storekit"
        )
        session.disableDialogs = true
        session.clearTransactions()
        UserDefaults.standard.removeObject(forKey: "cachedEntitlements")
    }

    func testMonthlyPurchaseUnlocksPro() async throws {
        try await session.buyProduct(
            identifier: ProductID.proMonthly
        )
        let unlocked = await EntitlementStore.shared.refresh()
        XCTAssertTrue(unlocked)
    }
}

거래 리스너는 구매를 시작하기 전에 반드시 실행해야 합니다. 구매가 매우 빠르게 완료되면 리스너 태스크가 업데이트를 놓쳐 테스트가 간헐적으로 시간 초과될 수 있습니다. 환불, 취소, 구독 만료도 세션에서 직접 트리거한 다음 권한을 새로 고친 결과를 검증해야 합니다. 구매 API가 성공을 반환했는지만 확인해서는 안 됩니다.

고정 대기로 경쟁 상태를 감추지 않기

Task.sleep은 실패 시점을 늦출 뿐, 상태가 업데이트되었다는 사실을 증명하지 못합니다. 더 안정적인 방법은 권한 저장소가 관찰 가능한 상태를 노출하도록 하고, 테스트에서 명확한 조건이 충족될 때까지 짧은 제한 시간으로 기다리는 것입니다. 시간 초과 로그에는 최소한 상품 식별자, 현재 권한, 미완료 거래 수, 테스트 이름을 기록하되 토큰이나 전체 자격 증명은 출력하지 마세요.

명령줄 실행 진입점 고정하기

먼저 클라우드 Mac에 설치된 시뮬레이터 기기를 확인한 다음 CI에서 고정된 이름을 사용하세요. 이미지에 포함된 기기 이름이 다르다면 스크립트가 임의의 기기를 조용히 선택하게 하지 말고 파이프라인 매개변수를 조정해야 합니다.

xcrun simctl list devices available
xcodebuild test \
  -workspace Example.xcworkspace \
  -scheme StoreKitRegression \
  -destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
  -resultBundlePath Artifacts/StoreKitTests.xcresult \
  CODE_SIGNING_ALLOWED=NO

시뮬레이터 테스트만 실행할 때는 코드 서명을 비활성화하면 거래 로직과 무관한 실패를 줄일 수 있습니다. 실행 전에 결과 디렉터리를 비우거나 작업 번호별 디렉터리를 사용해야 합니다. 기존 xcresult가 남아 있으면 명령이 즉시 오류를 반환합니다.

StoreKit 통합 테스트는 기본적으로 직렬 실행하는 편이 더 안정적입니다. 병렬 실행이 필요하다면 각 실행 단위에 독립된 시뮬레이터, Derived Data, 결과 디렉터리를 할당해야 합니다. 여러 프로세스가 같은 시뮬레이터를 동시에 조작하면 거래 큐와 앱 데이터가 서로 오염됩니다.

실패를 원인 분석이 가능한 증거로 설계하기

최초 구매, 사용자 취소, 보류, 환불, 구독 갱신, 구독 만료의 여섯 가지 경로를 최소한으로 다뤄야 합니다. 각 경로에서는 StoreKit 반환 상태, 권한 저장소 상태, 최종 화면 상태의 세 계층을 모두 검증해야 합니다. 버튼 문구가 바뀌었는지만 확인하면 백그라운드 권한이 업데이트되지 않은 문제를 놓칠 수 있습니다.

커밋하기 전에 다음 순서로 점검할 수 있습니다.

  1. .storekit 파일과 공유 Scheme이 버전 관리에 포함되어 있습니다.
  2. 각 테스트 케이스 시작 전에 거래와 로컬 권한 캐시를 지웁니다.
  3. 거래 리스너를 구매 동작보다 먼저 시작합니다.
  4. 테스트가 고정 대기나 실행 순서에 의존하지 않습니다.
  5. 각 병렬 작업이 독립된 시뮬레이터와 산출물 디렉터리를 사용합니다.
  6. 실패 시 xcresult, 테스트 로그, 화면 첨부 파일을 보존합니다.
  7. 정식 출시 전 서버 알림과 상품 설정을 별도로 점검합니다.

HireVM에서 이러한 작업을 실행할 때는 먼저 콘솔에서 현재 선택 가능한 구성을 확인한 다음 테스트 샤드 수에 맞춰 시뮬레이터를 계획하세요. 인앱 구매 테스트는 대개 단일 빌드 속도보다 상태 격리의 영향을 더 크게 받습니다. 먼저 하나의 직렬 파이프라인이 반복적으로 통과하도록 만든 뒤 병렬성을 높이면 문제 해결 비용을 크게 줄일 수 있습니다.

자주 묻는 질문

StoreKit Testing만으로 실제 구매 검증을 모두 대체할 수 있나요?

아닙니다. 상품 매핑, 거래 상태 전환, UI와 오류 처리는 검증할 수 있지만 출시 전에는 서버 알림, 실제 상품 설정과 전체 거래 경로를 별도로 확인해야 합니다.

단독 실행에서는 통과하지만 전체 테스트에서 실패하는 이유는 무엇인가요?

이전 테스트의 거래, 구독 갱신 상태 또는 앱 저장 데이터가 남았을 가능성이 큽니다. 각 테스트 전에 세션과 앱 상태를 초기화하고 동일 시뮬레이터의 동시 사용을 피해야 합니다.

StoreKit 테스트를 병렬로 실행해도 되나요?

가능하지만 각 실행 단위에 독립된 시뮬레이터와 결과 디렉터리를 배정해야 합니다. 하나의 시뮬레이터나 테스트 세션을 여러 프로세스가 공유하면 재현성이 떨어집니다.

전용 Apple Silicon 물리 노드

다음 원격 Mac 작업을 전용 노드에서 실행하세요

작업 규모에 맞춰 판매 중인 두 가지 구성, 다섯 개 노드와 일·주·월·분기 단위 중에서 선택하세요. 주문 전에 구성과 USD 금액을 모두 확인할 수 있습니다.

구성 선택 후 주문