RxCleanSwift icon indicating copy to clipboard operation
RxCleanSwift copied to clipboard

Clean Swift 연구일지 #6

Open GeekTree0101 opened this issue 5 years ago • 1 comments

레이몬드 아저씨의 CleanSwift와의 타협 및 실제 프로덕션에 적용하면서 배운점

이번 남는 시간동안 Vingle에 CleanSwift 적용해봤습니다. 레이몬드 아저씨의 CleanSwift는 해당 링크를 통하여 자세히 확인하 실 수가있습니다.

Interactor

CommunityCardLabelEditorInteractor.swift


protocol CommunityCardLabelEditorInteractorLogic: class {
    
    func getLabelList(_ req: CommunityCardLabelEditorModels.LabelList.Request)
    func createLabel(_ req: CommunityCardLabelEditorModels.Create.Request)
    func removeLabel(_ req: CommunityCardLabelEditorModels.Remove.Request)
    func save(_ req: CommunityCardLabelEditorModels.Save.Request)
}

protocol CommunityCardLabelEditorDataStore: class {
    
    var beforeCardLabels: [VingleLabel] { get set }
    var interestName: String? { get set }
}

class CommunityCardLabelEditorInteractor: CommunityCardLabelEditorDataStore & CommunityCardLabelEditorInteractorLogic {
    
    public var presenter: CommunityCardLabelEditorPresenterLogic?
    
    var beforeCardLabels: [VingleLabel] = []
    var interestName: String?

    private var worker: InterestServiceProtocol
    
    init(interestServiceWorker: InterestServiceProtocol) {
        self.worker = interestServiceWorker
    }
    
    func getLabelList(_ req: CommunityCardLabelEditorModels.LabelList.Request) {
        // ...
    }
    
    func createLabel(_ req: CommunityCardLabelEditorModels.Create.Request) {
       // ...
    }

    
    func removeLabel(_ req: CommunityCardLabelEditorModels.Remove.Request) {
      // ...
    }

    
    func save(_ req: CommunityCardLabelEditorModels.Save.Request) {
       //....
     }
}

Presenter

CommunityCardLabelEditorPresenter.swift

protocol CommunityCardLabelEditorPresenterLogic: class {
    
    func renderLabelList(_ response: CommunityCardLabelEditorModels.LabelList.Response)
    func presentCreateLabel(_ response: CommunityCardLabelEditorModels.Create.Response)
    func presentRemoveLabel(_ response: CommunityCardLabelEditorModels.Remove.Response)
    func presentSaveLabels(_ response: CommunityCardLabelEditorModels.Save.Response)
}

class CommunityCardLabelEditorPresenter: CommunityCardLabelEditorPresenterLogic {
    
    public weak var viewController: CommunityCardLabelEditorDisplayLogic?
    
    func renderLabelList(_ response: CommunityCardLabelEditorModels.LabelList.Response) {
        
        // ...
    }
    
    func presentCreateLabel(_ response: CommunityCardLabelEditorModels.Create.Response) {
        
        // ...
    }
    
    func presentRemoveLabel(_ response: CommunityCardLabelEditorModels.Remove.Response) {
        
        // ...
    }
    
    func presentSaveLabels(_ response: CommunityCardLabelEditorModels.Save.Response) {
        // ...
   }
}


ViewController

CommunityCardLabelEditorViewController.swift

protocol CommunityCardLabelEditorDisplayLogic: class {
    
    func displayLabelList(_ viewModel: CommunityCardLabelEditorModels.LabelList.ViewModel)
    func displayCreateLabelResult(_ viewModel: CommunityCardLabelEditorModels.Create.ViewModel)
    func displayRemoveLabelResult(_ viewModel: CommunityCardLabelEditorModels.Remove.ViewModel)
    func displaySaveLabelsResult(_ viewModel: CommunityCardLabelEditorModels.Save.ViewModel)
}

class CommunityCardLabelEditorController: ASViewController<ASDisplayNode> {

}

extension CommunityCardLabelEditorController: protocol CommunityCardLabelEditorDisplayLogic {

    //...
}

위의 코드와 같이 레이몬드아저씨가 제안한 그대로 정석대로 Interactor를 만들었습니다. 이전에는 Interactor의 Input과 Output을 정의해서 Test할 때 Input에 Stubbing해서 Ouput값을 체크하는 식으로 테스트 했지만 레이몬드 아저씨의 CleanSwift를 Unit Test하게 되면 다음과 같이 테스트를 정의하게됩니다.

1. Worker는 WorkerTests 따로하되 Worker Input에 대해서 Stubbing해서 Output 테스트하기

(참고로 Worker는 RxSwift로 작성되어 있습니다.)

class Worker {

     func getLabelList(_ input: Input) -> Observable<Output> {
         // ....
     }
}

2. Interactor Test시에는 PresenterLogic을 Spy만들어서 Interactor의 어떤 로직을 호출시 PresenterSpy가 통과 되었는지 정도 테스트

class CommunityCardLabelEditorInteractorSpec: QuickSpec {
    
    override func spec() {
        
        describe("Community Card Label Editor 인터렉터 -> 프리젠터 통과 테스트") {
            
            var interactor: CommunityCardLabelEditorInteractor!
            var presenter: PresenterSpy!
            
            beforeEach {
                interactor = CommunityCardLabelEditorInteractor
                    .init(interestServiceWorker:  InterestServiceStub.init())
                presenter = PresenterSpy.init()
                interactor.presenter = presenter
            }
            
            it("getLabelList가 호출되어야합니다.") {
                interactor.getLabelList(.init())
                expect(presenter.renderLabelListCalled).to(beTrue())
            }
         }
      }

      class InterestServiceStub: InterestServiceProtocol {
        
          func getLabels(interest: String) -> Observable<[VingleLabel]> {
              return Observable.just([VingleLabel.allLabel()!])
          }
        
          func createLabel(_ name: String,
                         labelName: String,
                         priority: Int?) -> Observable<VingleLabel> {
              return Observable.just(VingleLabel.allLabel()!)
          }
      }

      class PresenterSpy: CommunityCardLabelEditorPresenterLogic {
        
        var renderLabelListCalled: Bool = false
        
        func renderLabelList(_ response: Response) {
            renderLabelListCalled = true
        }
      }
}

3. Presenter Test시에는 DisplayLogic을 Spy만들어서 Presenter의 어떤 로직을 호출시 DisplayLogic Spy에서 기대하는 ViewModel을 반환받는지 테스트


class CommunityCardLabelEditorPresenterSpec: QuickSpec {
    
    override func spec() {
        
        describe("CommunityCardLabelEditor Presenter -> ViewController 통과 테스트") {
            
            var presenter: CommunityCardLabelEditorPresenter!
            var displaySpy: DisplaySpy!
            
            beforeEach {
                presenter = CommunityCardLabelEditorPresenter.init()
                displaySpy = DisplaySpy.init()
                presenter.viewController = displaySpy
            }
            
            context("LabelList") {
                
                it("성공") {
                    let labels = [VingleLabel.allLabel()!, VingleLabel.bestVingleLabel()!]
                    presenter
                        .renderLabelList(.init(result: .success(labels)))
                    switch displaySpy.displayLabelListOutput.result {
                    case .items(let items):
                        expect(items.count).to(equal(2))
                    default:
                        XCTAssertTrue(false)
                    }
                }
                
                it("실패") {
                    presenter
                        .renderLabelList(.init(result: .error(nil)))
                    switch displaySpy.displayLabelListOutput.result {
                    case .errorMessage(let message):
                        expect(message).to(equal(L("Failed")))
                    default:
                        XCTAssertTrue(false)
                    }
                }
            }
            
            // ...
        }
    }
    
    class DisplaySpy: CommunityCardLabelEditorDisplayLogic {
        
        var displayLabelListOutput: CommunityCardLabelEditorModels.LabelList.ViewModel!
       // ...
        
        
        func displayLabelList(_ viewModel: CommunityCardLabelEditorModels.LabelList.ViewModel) {
            self.displayLabelListOutput = response
        }

        // ...
    }
}

여기까지가 레이몬드 아저씨가 말씀하신 CleanSwift TDD관련된 부분입니다.

정리요약

  • Worker는 별도로 Stub Test
  • Interactor는 PresetnerLogicSpy를 활용하여 통과정도 테스트
  • Presenter는 DisplayLogicSpy를 활용하여 Response값을 기대하는 ViewModel로 반환하는지 테스트

아쉬운 부분

나름 하면서 아쉬운 부분들이 있는데 특히 RxSwift를 Worker이외에선 온전히 활용하지 못하는 부분이 아쉬웠습니다. 다음 연구일지에서는 온전히 쓰면서 레이몬드 아저씨의 CleanSwift의 장점까지 수렴할 수 있게 노력해볼 예정입니다.

GeekTree0101 avatar May 22 '19 09:05 GeekTree0101

Interactor 의 Logic을 Binder이용해서 처리하고 싶어요.


extension CommunityCardLabelEditorInteractorLogic {
    
    var getLabelList: Binder<CommunityCardLabelEditorModels.LabelList.Request> {
        return Binder(self) { interactor, request in
            interactor.getLabelList(request)
        }
    }
}

protocol CommunityCardLabelEditorInteractorLogic: class {
    
    func getLabelList(_ response: CommunityCardLabelEditorModels.LabelList.Request)
}

문제는 interactor하나씩 늘어날때마다 작성해야되서 귀찮음...

GeekTree0101 avatar May 22 '19 09:05 GeekTree0101