Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - Swift] XCTest로 비동기 테스트 방법 (RxSwift 테스트 방법) 본문

iOS 응용 (swift)

[iOS - Swift] XCTest로 비동기 테스트 방법 (RxSwift 테스트 방법)

jake-kim 2022. 12. 22. 23:42

테스트 대상

  • 플러스 버튼을 누르면 1초 이후에 값이 증가하는 앱
  • 테스트: 버튼 누름 -> 1초 후에 값이 변화하는지 확인

plus 버튼 클릭 시, 1초 후 카운트 증가

  • 샘플 프로젝트에서 사용한 라이브러리 - MVVM을 템플릿화 해놓은 ReactorKit 사용
target 'ExAsyncRx' do
  use_frameworks!
  
  pod 'ReactorKit'
  pod 'RxCocoa'
  
  target 'ExAsyncRxTests' do
    inherit! :search_paths
  end
end
  • 예제에 사용될 ViewController
import UIKit
import ReactorKit
import RxSwift
import RxCocoa

class ViewController: UIViewController, ReactorKit.View {
    private let plusButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("plus", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    private let label: UILabel = {
        let label = UILabel()
        label.text = "0"
        label.font = .systemFont(ofSize: 30, weight: .bold)
        label.numberOfLines = 1
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    var disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        
        [plusButton, label]
            .forEach(view.addSubview(_:))
        
        NSLayoutConstraint.activate([
            plusButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            plusButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        ])
        
        NSLayoutConstraint.activate([
            label.leftAnchor.constraint(equalTo: plusButton.rightAnchor, constant: 16),
            label.centerYAnchor.constraint(equalTo: plusButton.centerYAnchor),
        ])
    }
    
    func bind(reactor: ViewReactor) {
        // Action
        plusButton.rx.tap
            .map(Reactor.Action.tapPlusButton)
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        // State
        reactor.state.map(\.cnt)
            .observe(on: MainScheduler.asyncInstance)
            .map(String.init)
            .bind(to: label.rx.text)
            .disposed(by: disposeBag)
    }
}
  • 예제에 사용할 ViewReactor
import ReactorKit

final class ViewReactor: Reactor {
    enum Action {
        case tapPlusButton
    }
    enum Mutation {
        case setPlusCnt(Int)
    }
    struct State {
        var cnt = 0
    }
    
    let initialState = State()
    
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .tapPlusButton:
            return .just(.setPlusCnt(1))
                .delay(.seconds(1), scheduler: MainScheduler.asyncInstance)
        }
    }
    
    func reduce(state: State, mutation: Mutation) -> State {
        var state = state
        switch mutation {
        case let .setPlusCnt(int):
            state.cnt += int
        }
        return state
    }
}

테스트 코드 작성

  • @testable로 ViewController, ViewReactor가 있는 모듈 임포트
  • 테스트에 사용될 XCTest와 RxSwift 임포트
  • 아래 코드 준비
//  ExAsyncRxTests.swift

@testable import ExAsyncRx
import XCTest
import RxSwift

final class ExAsyncRxTests: XCTestCase {
    var disposeBag: DisposeBag!
    var sut: ViewReactor!

    override func setUp() {
    }
    
    func test_WhenTapPlusButton_ThenPlusCount() {
    }
}
  • setup에서 기본적인 인스턴스 생성
    override func setUp() {
        disposeBag = DisposeBag()
        sut = ViewReactor()
    }
  • 1) Given
    • reactor와 expectation 준비
    func test_WhenTapPlusButton_ThenPlusCount() {
        // Given
        let reactor = sut!
        let expectation = XCTestExpectation(description: "test_WhenTapPlusButton_ThenPlusCount")
    }
  • 2) When
    • reactor의 tapPlusButton 액션을 인풋
        // When - tap plus button
        reactor.action.onNext(.tapPlusButton)
  • 3) Then
    • reactor의 cnt 상태를 구독하고 있다가 값이 변경되었을때 1로 제대로 변경되는지 확인
    • 비동기이므로 XCTest의 wait(for:)를 이용하여 테스트
        // Then - cnt is one
        reactor.state.map(\.cnt)
            .subscribe { item in
                guard item == 1 else { return }
                expectation.fulfill()
            }
            .disposed(by: disposeBag)
        
        wait(for: [expectation], timeout: 3)

(테스트 부분 전체 코드)

@testable import ExAsyncRx
import XCTest
import RxSwift

final class ExAsyncRxTests: XCTestCase {
    var disposeBag: DisposeBag!
    var sut: ViewReactor!
    
    override func setUp() {
        disposeBag = DisposeBag()
        sut = ViewReactor()
    }
    
    func test_WhenTapPlusButton_ThenPlusCount() {
        // Given
        let reactor = sut!
        let expectation = XCTestExpectation(description: "test_WhenTapPlusButton_ThenPlusCount")
        
        // When - tap plus button
        reactor.action.onNext(.tapPlusButton)
        
        // Then - cnt is one
        reactor.state.map(\.cnt)
            .subscribe { item in
                guard item == 1 else { return }
                expectation.fulfill()
            }
            .disposed(by: disposeBag)
        
        wait(for: [expectation], timeout: 3)
    }
}

 

* 전체 코드: https://github.com/JK0369/ExAsyncTesting

Comments