관리 메뉴

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

[iOS - swift] 2. ReactorKit - 테스트 방법 (Storyboard 사용, IBOutlet 테스트 방법) 본문

Architecture (swift)/ReactorKit

[iOS - swift] 2. ReactorKit - 테스트 방법 (Storyboard 사용, IBOutlet 테스트 방법)

jake-kim 2021. 11. 30. 22:25

1. ReactorKit - 개념

2. ReactorKit - 테스트 방법 (Storyboard 사용, IBOutlet 테스트 방법)

3. ReactorKit - `TaskList 구현`, 템플릿 (template), 비동기 처리 transform(mutation:)

4. ReactorKit - `TaskEdit 구현`, 화면전환, 데이터 전달

 

 

* 해당 코드는 ReactorKit git repository 코드를 참고하였습니다.


* Unit Test가 중요한 이유, 클린 코드: https://ios-development.tistory.com/770

* Counter 예제 코드: https://github.com/JK0369/ExReactorKit/tree/BaseComponent

ReactorKit 테스트

  1. View -> Reactor
    • View에서 Reactor에 Action값을 잘 넘기고 있는지 확인
      (view에서 보낸 action이 Reactor에 잘 전달 되었는지 확인)
  2. Reactor -> View
    • View에서 Reactor를 잘 구독하고 있는지 확인
      (Reactor에서 상태 변화가 일어난 경우, View에 반영되는지 확인)
  3. Reactor
    • action을 받으면 비즈니스 로직(Mutation)이 잘 처리되어 State값이 기대하는 값으로 변경되는지 확인

1. 테스트 View -> Reactor

View에서 Reactor에 Action값을 잘 넘기고 있는지 확인

  • 테스트 준비
    • sut: System Under Test (테스트 대상의 변수명)
    • View를 구성할 때 storyboard를 이용했으므로, storyboard 객체 선언 `sut`
import XCTest
@testable import ExCounter

class ExCounterTests: XCTestCase {
    let sut = UIStoryboard(name: "Counter", bundle: nil)
}
  • 테스트 코드 메소드 시그니처 작성
    • 감소 버튼을 탭한 경우, 작업단위 (Mutation) 값이 decrease로 잘 들어오는지 테스트
func testAction_whenDidTapDecreaseButtonInView_thenMutationIsDecreaseInReactor()
  • 구현
    • 주의해야할 점은 ViewController를 storyboard 인스턴스로 생성 후, loadViewIfNeeded()를 호출해주어야 IBOutlet 인스턴스가 생성되므로 호출
func testAction_whenDidTapDecreaseButtonInView_thenMutationIsDecreaseInReactor() {
    // Given
    let counterReactor = CounterViewReactor()
    counterReactor.isStubEnabled = true
    
    let counterViewController = sut.instantiateViewController(withIdentifier: "Counter") as! CounterViewController
    counterViewController.loadViewIfNeeded() // IBOutlet과 Action을 구성하기 위해서 호출
    counterViewController.reactor = counterReactor
    
    // When
    counterViewController.decreaseButton.sendActions(for: .touchUpInside)
    
    // Then
    XCTAssertEqual(counterReactor.stub.actions.last, .decrease)
}

2. 테스트 Reactor -> View

View에서 Reactor를 잘 구독하고 있는지 확인

  • 메소드 시그니처 작성
    • Reactor에서 loding 상태가 바뀐 경우, View에도 반영되는지 확인
func testState_whenChangeLoadingStateToTrueInReactor_thenActivityIndicatorViewIsAnimatingInView()
  • 구현
func testState_whenChangeLoadingStateToTrueInReactor_thenActivityIndicatorViewIsAnimatingInView() {
    // Given
    let counterReactor = CounterViewReactor()
    counterReactor.isStubEnabled = true
    
    let counterViewController = sut.instantiateViewController(withIdentifier: "Counter") as! CounterViewController
    counterViewController.loadViewIfNeeded()
    counterViewController.reactor = counterReactor
    
    // When
    counterReactor.stub.state.value = CounterViewReactor.State(value: 0, isLoading: true)
    
    // Then
    XCTAssertEqual(counterViewController.activityIndicatorView.isAnimating, true)
}

3. 테스트 Reactor

1) action을 받으면 비즈니스 로직(Mutation)이 잘 처리되어 State값이 기대하는 값으로 변경되는지 확인

  • 메소드 시그니처 작성
    • Reactor에서 특정 action을 주고, 내부적으로 mutate(), reduce()를 통해서 state값이 기대하는 값으로 변경되는지 확인
func testReactor_whenExcuteIncreaseButtonTapActionInView_thenStateIsLoadingInReactor()
  • 구현
func testReactor_whenExcuteIncreaseButtonTapActionInView_thenStateIsLoadingInReactor() {
    // Given
    let reactor = CounterViewReactor()
    
    // When
    reactor.action.onNext(.increase)
    
    // Then
    XCTAssertEqual(reactor.currentState.isLoading, true)
}

2) 비동기처리인 경우 테스트 방법 - increase 액션이 발생했을 때, 최종적으로 value값이 변화하는지 테스트

  • 메소드 시그니처 작성
    • Reactor에서 특정 action을 주었을때, value값이 변화하는지 테스트
func testReactor_whenExecuteIncreaseButtonTapActionInView_thenStateValueIsChanged()
  • 구현
// RxSwift관련 코드도 추가

// import RxSwift
// var disposeBag = DisposeBag()

func testReactor_whenExecuteIncreaseButtonTapActionInView_thenStateValueIsChanged() {
  // Given
    let reactor = CounterViewReactor()
    let expectation = XCTestExpectation(description: "Test Description")
    reactor.state.map(\.value)
      .distinctUntilChanged()
      .filter { $0 == 1 }
      .subscribe(onNext: { value in expectation.fulfill() })
      .disposed(by: self.disposeBag)
    
    // When
    reactor.action.onNext(.increase)
    
    // Then
    wait(for: [expectation], timeout: 3.0)
}

* 전체 소스 코드: https://github.com/JK0369/ExReactorKit/tree/BaseComponent

 

* 참고

- https://medium.com/styleshare/reactorkit-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-c7b52fbb131a

 

Comments