Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

클린아키텍처 Test코드 작성기 1편 with Rxtest #145

Closed
Youngminah opened this issue Mar 19, 2022 · 0 comments
Closed

클린아키텍처 Test코드 작성기 1편 with Rxtest #145

Youngminah opened this issue Mar 19, 2022 · 0 comments

Comments

@Youngminah
Copy link
Owner

Youngminah commented Mar 19, 2022

image

  • 클린아키텍처의 가장큰 장점 Testable한 코드라는점!
  • 그럼 테스트 코드 작성은 필수라능 ❗️🥸🥸


테스트 코드 도전기

  • 간단한 ViewModel에 관한 테스트 코드를 작성해보았다.
  • 이를 위해 아키텍쳐적으로 의존성을 외부에서 주입하고,
  • 인터페이스 (프로토콜)을 사용한 부분이 테스트 코드에서 매우 편리하게 작용하였다.
  • RxTest를 이용한 테스트코드를 작성하였음!


SearchViewModel 테스트 코드 작성을 위한 선행 작업

  • 이부분은 테스트 코드영역이 아닌
  • 테스트 코드를 효과적으로 짜기위한
  • 프로젝트 본래의 코드영역임!!

1. SearchUseCase프로토콜을 만들어 인터페이스로 채택하도록

import Foundation
import RxCocoa
import RxSwift

protocol SearchUseCase {

    var successReqeustSearch: PublishRelay<Repos> { get set }
    var failGithubError: PublishRelay<GithubServerError> { get set }

    func requestSearch(searchName: String, page: Int)
}

final class DefaultSearchUseCase: SearchUseCase {

    private let githubRepository: GithubRepositoryType
    private let disposeBag = DisposeBag()

    var successReqeustSearch = PublishRelay<Repos>()
    var failGithubError = PublishRelay<GithubServerError>()

    init(
        githubRepository: GithubRepositoryType
    ){
        self.githubRepository = githubRepository
    }
}

extension DefaultSearchUseCase {

    func requestSearch(searchName: String, page: Int) {
        let query = ReposQuery(searchName: searchName)
        self.githubRepository.requestSearch(query: query, page: page) { [weak self] response in
            guard let self = self else { return }
            switch response {
            case .success(let repos):
                self.successReqeustSearch.accept(repos)
            case .failure(let error):
                self.failGithubError.accept(error)
            }
        }
    }
}
  • 이를 객체 지향 원칙중 DIP (의존관계 역전 원칙 법칙)이라고 함
  • 그게 뭔지 모르겠다고? 그럼 여기 링크 참고 SOLID원칙 보러가기

2.SearchViewModel에선 UseCase를 프로토콜 형식으로 선언.

final class SearchViewModel: ViewModelType {

    private weak var coordinator: TabBarCoordinator?
    private let useCase: SearchUseCase

    struct Input {
        let pullRefresh: Signal<Void>
        let didScrollToBottom: Signal<Void>
        let searchBarText: Signal<String>
    }
    struct Output {
        let repoList: Driver<[RepoItem]>
        let refreshAction: Signal<Bool>
        let bottomSpinnerAction: Signal<Bool>
    }
    var disposeBag = DisposeBag()
    ...
}
  • SearchUseCase란 프로토콜 형식으로 유즈케이스가 선언되어있음!
  • DIP원칙을 지키며 의존성을 외부에서 주입하였다.




MockSearchUseCase

  • 여기서부터는 테스트 코드 영역임
  • 우선 Mock(더미데이터를 위한 UseCase 생성)
  • ViewModel에 관한 테스트 코드를 짤것인데
  • 이과정에서 유즈케이스가 필요하다.
  • 우리는 DIP를 지키며 코드를 짯기 때문에 MockUseCase를 생성하기 아주 수월하였음!
import Foundation

import RxCocoa
import RxSwift
@testable import GithubRepos

final class MockSearchUseCase: SearchUseCase {

    var successReqeustSearch = PublishRelay<Repos>()
    var failGithubError = PublishRelay<GithubServerError>()

    init() { }

    func requestSearch(searchName: String, page: Int) {
        if page < 30 && page > 0 {
            self.successReqeustSearch.accept(
                Repos(
                    items: [
                        RepoItem(id: 1, fullName: "GodRepos/kingkinggod", description: nil, topics: ["likes", "king", "godofkingking"], star: 1000, fork: 2, language: "Swift", updatedAt: Date())
                    ]
                )
            )
        } else {
            self.failGithubError.accept(.unknown)
        }
    }
}
import XCTest

import RxCocoa
import RxSwift
import RxTest

@testable import GithubRepos

class SearchViewModelTests: XCTestCase {

    private var viewModel: SearchViewModel!
    private var disposeBag: DisposeBag!
    private var scheduler: TestScheduler!
    private var input: SearchViewModel.Input!
    private var output: SearchViewModel.Output!

    private let dummyData = [
        RepoItem(id: 1, fullName: "GodRepos/kingkinggod", description: nil, topics: ["likes", "king", "godofkingking"], star: 1000, fork: 2, language: "Swift", updatedAt: Date())
    ]

    override func setUpWithError() throws {
        self.viewModel = SearchViewModel(
            coordinator: nil,
            useCase: MockSearchUseCase()
        )
        self.disposeBag = DisposeBag()
        self.scheduler = TestScheduler(initialClock: 0)
    }

    override func tearDownWithError() throws {
        self.viewModel = nil
        self.disposeBag = nil
    }

    func test_Search_Success_Case() throws {
        let refreshTestableObservable = self.scheduler.createHotObservable([
                    .next(0, ())
                ])

        let scrollTestableObservable = self.scheduler.createHotObservable([
                    .next(20, ())
                ])

        let textTestableObservable = self.scheduler.createHotObservable([
            .next(10, "29cm")
        ])

        let repoListObserver = self.scheduler.createObserver([RepoItem].self)

        self.input = SearchViewModel.Input(
            pullRefresh: refreshTestableObservable.asSignal(onErrorJustReturn: ()),
            didScrollToBottom: scrollTestableObservable.asSignal(onErrorJustReturn: ()),
            searchBarText: textTestableObservable.asSignal(onErrorJustReturn: "")
        )

        self.viewModel.transform(input: input)
            .repoList
            .drive(repoListObserver)
            .disposed(by: self.disposeBag)

        scheduler.start()
        
        XCTAssertEqual(repoListObserver.events, [
            .next(0, []),
            .next(10, []), // 백그라운드 배경을 띄우려고 나온 빈배열
            .next(10, dummyData)
        ], "The objects should be equal but is not")
    }
}

image

  • 테스트 코드 성공 모습
  • 아주아주 간단하게 HotObservable을 이용한 테스트 코드를 작성해보았는데,
  • 재밋고 생각보다 간편하였음.




간단한 UITest 코드 작성

  • UITest코드는 아주 기초만 써보았슴

UITest 코드

import XCTest

class GithubReposUITests: XCTestCase {

    override func setUpWithError() throws {
        continueAfterFailure = false
    }

    override func tearDownWithError() throws {
    }

    func testSearchRepositoryFlow() throws {
        let app = XCUIApplication()
        app.launch()
        let searchField = app.otherElements["searchBar"]
        searchField.tap()
        searchField.typeText("Youngminah")
        app.buttons["searchButton"].tap()
    }

    func testLaunchPerformance() throws {
        if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
            // This measures how long it takes to launch your application.
            measure(metrics: [XCTApplicationLaunchMetric()]) {
                XCUIApplication().launch()
            }
        }
    }
}

image
image

  • 테스트 성공!

UITest 실행 화면

Simulator.Screen.Recording.-.iPhone.12.Pro.-.2022-03-17.at.09.10.33.mp4



Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant