ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TCA - Testing Effects (feat. unimplemented)
    TCA 2022. 10. 31. 11:30

    안녕하세요. 그린입니다🍏

    이번 포스팅에서는 TCA에서 Effect를 테스터블한 환경으로 구성할 수 있는 unimplemented라는 메서드에 대해 알아보겠습니다🙌

     

    umimplemented라는 뜻은 우선 implemented의 반대어죠.

    개발자들은 항상 구현을 하잖아요?

    구현이라는 단어인 implement는 익숙할거라 생각해요.

    그와 반대는 구현되지 않았다는 뜻의 unimplement입니다.

    (그냥 제가 요즘 영어 공부도 간간히 하려해서 주절주절 써봤네요..!)

     

    결국 "구현되지 않은, 실행되지 않는" 뜻을 가진 메서드일거라 추측합니다.

     

     

    TCA를 사용하면서 리듀서에서 Effect를 방출하면서 흔히 아시는 구현으로 사용하잖아요?

    근데 이 Effect를 테스트 환경에서는 굳이 결과까지 테스트할 필요가 없이 넘어가는것이 필요할때도 있습니다.

    그럴때는 아래와 같이 이전 포스팅에서 학습한 fireAndForget을 사용하곤 해요.

    struct CounterEnvironment {
      let playAlertSound: () -> EffectPublisher<Never, Never>
    }
    
    let counterReducer = AnyReducer<
      CounterState, 
      CounterAction, 
      CounterEnvironment
    > { state, action, environment in
      switch action {
        case .decrementButtonTapped:
          if state > 0 {
            state.count -= 0
            return .none
          } else {
            return environment.playAlertSound()
              .fireAndForget()
          }
        ...
      }
    }

    호옥시 fireAndForget이 생소하시면 아래 포스팅을 먼저 학습하고 오시면 도움이 됩니다!
    (앞으로 펼쳐질 간단한 이야기가 이 바탕이 될거라서요!)

    https://green1229.tistory.com/293

     

    TCA - fireAndForget

    안녕하세요. 그린입니다🍏 이번 포스팅에서는 TCA의 fireAndForget에 대해 학습해보겠습니다🙌 우선 fireAndForget이라는 용어가 생소할 수 있습니다! 그냥 대충 추측을 해보면 "불지르고 잊어버리기"

    green1229.tistory.com

     

    자 그런데 매번 이렇게 구현부를 손대는것 자체가 아이러니입니다.

    왜냐하면 이렇다는건 매번 테스트 환경을 위해 별도 리듀서를 구현해 Effect를 갈아끼는것은 참 불편할거에요.

    그렇다면 보다 좋은건 Store 자체에서 이러한 env에 대한 live와 test를 갈아끼우면서 코드 하나만 바꿔준다면 편할것 같아요.

    그걸 조금 더 사용할 수 있게 도와주는것이 unimplemented 메서드입니다.

     

    그럼 이제 본격적으로(거의 얘기가 끝났지만) unimplemented 메서드가 무엇인지 어떻게 저는 사용하고 있는지 보시죠!

     

    unimplemented 메서드 정의

     extension EffectPublisher {
       public static func unimplemented(_ prefix: String) -> Self {
        .fireAndForget {
          XCTFail("\(prefix.isEmpty ? "" : "\(prefix) - ")An unimplemented effect ran.")
        }
      }
    }

    해당 메서드의 정의는 이렇게 구현되어 있습니다.

    EffectPublisher를 확장한것이죠.

    즉 해당 타입을 받아 fireAndForget 실행을 하는 Effect로 반환합니다.

    앞서 봤던 fireAndForeget을 붙인 구현을 그냥 조금 쓰기 쉽게 편하게 구현한거라 봐도 무방해요.

     

    그럼 실제적으로 어떻게 사용할 수 있을까요?

     

    unimplemented 메서드 사용

    리듀서에서 실행 시 실패할 수 있는 Effect를 Store 환경으로 구성해 사용할 수 있습니다.

    @MainActor
    func testIncrement() async {
      let store = TestStore(
        initialState: CounterState(count: 0)
        reducer: counterReducer,
        environment: CounterEnvironment(
          playSound: .unimplemented("playSound")
        )
      )
      
      await store.send(.increment) {
        $0.count = 1
      }
    }

    요런 느낌으로 env를 Store에서 구성할때 테스트 서비스 로직을 넣어 구성할 수 있습니다.

     

    조금 더 저희 정육각 iOS팀에서는 이 테스트 코드의 사용까지 고려해 아래와 같이 사용하고 있습니다.

     

    정육각 iOS팀에서 사용하는 unimplemented 메서드 활용

    예를들어 어떠한 네트워킹을 하는 Service가 있다고 가정해볼께요.

    이 Service를 live(실 사용)과 test 두 환경으로 필요한 코어 혹은 내부 단에서 extension하여 구성해줍니다.

    // APIService.swift
    
    public struct APIService {
      ...
    }
    
    public extension APIService {
      public static let live = Self(
        user: { 유저 네트워킹 실 구현 },
        address: { 주소 네트워킹 실 구현 }
      )
      
      public static let unimplemented = Self(
        user: { _ in Effect.unimplemented("\(Self.self).user") },
        address: { _ in Effect.unimplemented("\(Self.self).address") }
      )
    }

    해당 APIService라는 파일이 있고 네트워킹 구현이 있습니다.

    extension으로 말한것과 같이 live와 test일 시 구현을 확장시켜 놓습니다.

     

    그럼 이걸 어떻게 실제적으로 상용 코드 및 Test 코드에서 사용할까요?

    struct AppEnvironment {
      static let live = Self(apiService: .live)
    }

    실제적으로 상용되는 코드에서는 이렇게 live로 구현해둔 static 메서드로 호출하여 주입시켜주면됩니다.

    class APITest: XCTestCase {
      func test_API() {
        var apiService = APIService.unimplemented
        apiService.user = Effect(value: 테스팅 로직)
        
        let store = TestStore(
          initialState: APIState(),
          reducer: apiReducer,
          environment: APIEnvironment(apiService: apiService)
        )
      }
    }

    이렇게 test를 위한 unimplemented 서비스 인스턴스를 가져옵니다.

    그리고 각 실제 네트워킹 구현이 아닌 원하는 fake 구현을 넣어 테스트 할 수 있게 구성할 수 있습니다.

     

    마무리

    자 이렇게 이번엔 test를 위한 TCA에서의 한걸음을 나아가봤습니다!

    테스트 코드를 무조건 작성해야한다라는 것에서 벗어나 테스트 환경은 꼭 필요하다고 생각합니다.

    그렇기에 이러한 서비스 로직을 구성할때 조금이라도 편리하게 테스트 해볼 수 있도록 분리시키는 밑거름 작업은 필수적이라고 느껴지네요!

     

    [참고 자료]

    https://github.com/pointfreeco/swift-composable-architecture/blob/main/Sources/ComposableArchitecture/Effect.swift

     

    GitHub - pointfreeco/swift-composable-architecture: A library for building applications in a consistent and understandable way,

    A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. - GitHub - pointfreeco/swift-composable-architecture: A library for bu...

    github.com

    'TCA' 카테고리의 다른 글

    TCA 1.0 - Hello, TCA (ch.01)  (114) 2024.02.01
    TCA - ReducerProtocol  (8) 2023.01.31
    TCA - fireAndForget  (0) 2022.10.27
    TCA - concatenate & merge (여러 Effect를 단일 Effect로 만들기)  (2) 2022.10.25
    TCA - Throttling  (2) 2022.10.20
Designed by Tistory.