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] Today Unit test 작성 (#197) #201

Merged
merged 9 commits into from
Dec 3, 2023
Merged

[TEST] Today Unit test 작성 (#197) #201

merged 9 commits into from
Dec 3, 2023

Conversation

ffalswo2
Copy link
Contributor

@ffalswo2 ffalswo2 commented Dec 2, 2023

[#197] TEST : Today Unit test code 작성

🌱 작업한 내용

  • Today관련 API의 Unit test code를 작성했습니다.
  • TodayViewModel의 로직을 검증하는 Unit test code를 작성했습니다.
  • TodayViewController의 로직을 검증하는 Unit test code를 작성했습니다.
  • 커버리지 목표 70% 이상 달성

🌱 PR Point

객체의 Unit test를 하기전 우선 테스트하고자 하는 객체가 가진 책임이 무엇인지를 먼저 생각하고, 해당 책임을 수행하는 로직들이 제대로 수행되고 있는지를 검증했습니다.

공통적으로 APIServiceStub을 사용

Unit test를 진행할 시에 고려해야하는 FIRST principle이 존재합니다. 여기서 F(Fast)테스트는 빠르게 실행될 수 있어야함을 말하고, I(Isolated)테스트들이 각각 독립적으로 수행되어야함을 말합니다.

실제로 API 요청을 보내게 되면 네트워크 환경에 따라 속도가 매우 느려질 수 있으며, 실제 API 요청을 보내게 된다면 각 테스트들이 독립적이라고 할 수 없습니다. 따라서 가짜 객체를 return시켜주는 Stub 방식으로 테스트를 위한 APIService를 만들어서 테스트를 진행합니다.
#196

TodayManager Unit test

Manager의 책임

TodayManager의 책임은 API를 요청하고, 그 결과값을 앱 내에서 사용하기 위한 데이터인 AppData로 변환해서 ViewModel에게 리턴해주거나 API 요청 과정에서 에러가 발생했다면 에러를 전달하는 책임을 가지고 있습니다.

따라서 다음과 같은 테스트를 진행했습니다.

  1. API를 통해 올바른 데이터를 가져와 AppData로 성공적으로 변환하는지
  2. 클라이언트 에러 발생 시 알맞은 에러를 던지는지
  3. 서버 에러 발생 시 알맞은 에러를 던지는지

TodayViewModel Unit test

ViewModel의 책임

ViewModel은 Manager로부터 넘겨받은 데이터 혹은 에러를 그에 따른 적절한 작업을 Operator를 통해 수행하고 Output을 통해 ViewController에게 전달하는 책임을 가지고 있습니다.

따라서 다음과 같은 테스트를 진행했습니다.

  1. Manager로부터 올바른 데이터를 전달받았을 때 Output으로 잘 나가고 있는지
  2. Manager로부터 에러가 발생했을 때 placeholder값으로 대체해서 Output으로 잘 나가고 있는지
  3. bookmarkButton이 눌렸을때 올바른 flow type이 전달되는지
  4. mypageButton이 눌렸을때 올바른 flow type이 전달되는지
  5. mainArticleView가 눌렸을 때 올바른 flow type이 전달되는지

이 때 Manager unit test를 통해 Manager의 로직을 검사하기 때문에, ViewModel를 테스트할 시에는 ManagerStub을 사용합니다.

final class TodayManagerStub: TodayManager {
    
    var todayArticle: TodayArticle?
    
    func inquiryTodayArticle() throws -> LionHeart_iOS.TodayArticle {
        guard let todayArticle else { return TodayArticle.emptyArticle }
        return todayArticle
    }
}

TodayViewController Unit test

ViewController의 책임

TodayViewController는 user로부터 받는 action을 ViewModel에게 잘 전달해주고, Output stream을 observing하고 있다가 값이 변화면 UI에 데이터를 업데이트하는 책임을 가지고 있습니다.

따라서 다음과 같은 테스트를 진행했습니다.

  1. viewWillAppear 이벤트가 ViewModel로 잘 전달되고 있는지
  2. articleTapped 이벤트가 ViewModel로 잘 전달되고 있는지
  3. bookMarkButtonTapped 이벤트가 ViewModel로 잘 전달되고 있는지
  4. myPageButtonTapped 이벤트가 ViewModel로 잘 전달되고 있는지
  5. Output TodayArticle 데이터가 UI에 잘 반영되고 있는지

테스트 방식

final class TodayViewModelSpy: TodayViewModel {
    
    var isViewWillAppeared = PassthroughSubject<Bool, Never>()
    var isArticleTapped = PassthroughSubject<Bool, Never>()
    var isNaviLeftButtonTapped = PassthroughSubject<Bool, Never>()
    var isNaviRightButtonTapped = PassthroughSubject<Bool, Never>()
    
    var todayArticle: TodayArticle = TodayArticle.emptyArticle
    
    private var cancelBag = Set<AnyCancellable>()
    
    func transform(input: LionHeart_iOS.TodayViewModelInput) -> LionHeart_iOS.TodayViewModelOutput {
        
        input.viewWillAppearSubject
            .sink { [weak self] _ in
                self?.isViewWillAppeared.send(true)
            }
            .store(in: &cancelBag)
        
        input.navigationLeftButtonTapped
            .sink { [weak self] _ in
                self?.isNaviLeftButtonTapped.send(true)
            }
            .store(in: &cancelBag)
        
        input.navigationRightButtonTapped
            .sink { [weak self] _ in
                self?.isNaviRightButtonTapped.send(true)
            }
            .store(in: &cancelBag)
        
        input.todayArticleTapped
            .sink { [weak self] _ in
                self?.isArticleTapped.send(true)
            }
            .store(in: &cancelBag)
        
        
        let output = Just(todayArticle).eraseToAnyPublisher()
        
        return Output(viewWillAppearSubject: output)
    }
}

행동과 상태를 검증하는 TodayViewModelSpy를 생성했습니다. ViewController에게 중요한 것은 user action이 ViewModel로 잘전달이 되었는지입니다.

실제로 버튼 액션을 발생시키고 난 후에 ViewModel의 transform메서드에서 해당 event를 수신하는 시점에서 expectation fulfill을 해줘야하기 때문에 Output stream이 아닌 별도의 stream을 생성해서 해당 stream으로 이벤트를 전달합니다.

func test_viewModel_viewWillAppear_이벤트_전달_성공() {
    // Given
    let todayViewController = TodayViewController(viewModel: self.viewModel)
    let expectation = XCTestExpectation(description: "viewWillAppear가 불렸을 때")
    
    // When
    var isEventOccured: Bool!
    viewModel.isViewWillAppeared
        .sink { isTapped in
            isEventOccured = isTapped
            expectation.fulfill()
        }
        .store(in: &cancelBag)
    
    todayViewController.viewWillAppear(false)
    
    // Then
    wait(for: [expectation], timeout: 0.5)
    XCTAssertTrue(isEventOccured!)
}

이를 ViewController test 코드 쪽에서 observing하고 있다가 이벤트를 받게 된다면(viewModel이 이벤트를 수신했다면) expectation.fulfill() 메서드를 통해 wait을 빠져나갑니다.

📸 스크린샷

image

📮 관련 이슈

@ffalswo2 ffalswo2 added 🦁민재 민재's 🧪Test 테스트 labels Dec 2, 2023
@ffalswo2 ffalswo2 added this to the 🦁3차 Refactor🦁 milestone Dec 2, 2023
@ffalswo2 ffalswo2 self-assigned this Dec 2, 2023
Copy link
Contributor

@kimscastle kimscastle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생했습니다~ mock stub spy에 대한 기준을 같이 정해서 분리해봅시다


@testable import LionHeart_iOS

final class TodayViewModelSpy: TodayViewModel {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 ViewController를 test할때 ViewModelStub를 만들어서 사용했는데
코드를 보니 Spy를만들어서 사용한걸보고 공부를해보니
우리가 단순히 output이 잘나오는지만 확인할거면 Stub가 맞는데
지금처럼 output에 대한 검증을 포함해 실제 어떤 내부 stream이 값이 잘들어가거나 어떤 메서드가 호출이되었다는 행위에대한 검증이 함께 필요해서 spy를 사용하신게 맞나용

제가 이해한 방식입니다

mock은 어떤 메서드가 잘호출되었는지 여부를 검증 -> 행위에대한 검증 객체
stub는 어떤 output이 잘 return되었는지 여부를 검증 -> 상태에대한 검증 객체
spy는 어떤 메서드가 잘 호출되었는지 + output이 잘return되었는지 여부를 검증 -> 상태와 행위에 대한 검증 객체

만약에 제가 이해한게 맞으면 제 ViewController를 test할때 ViewModel도 Spy라는 네이밍이 적절해보이네요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 말씀해주신게 맞습니다! 저도 동일하게 이해했습니다 😊

@testable import LionHeart_iOS


final class TodayNavigationSpy: XCTestCase, TodayNavigation {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 위에 남긴 리뷰가 맞다면
해당 navigation구현객체는 단순히 어떤 메서드의 행위에 대한 검증을 진행하는 객체이므로 Spy보다는 Mock이라는 네이밍이 맞지 않을까요??

Copy link
Contributor Author

@ffalswo2 ffalswo2 Dec 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다시 로직을 확인해보니 저희는 TodayNavigation을 정말 자리만 채워줄뿐인 용도로 사용하고 있어서 SpyMock이 아닌 Dummy가 맞아보이네요! 수정하겠습니다!


func test_투데이_아티클_조회_API_올바른데이터_성공() throws {
// Given
let expectation = XCTestExpectation(description: "Today Article API 성공적으로 Output으로 전달")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XCTestExpectation를 저는 when에다가 두긴 헀는데

Before you perform an asynchronous task in your test method, create an instance of XCTestExpectation with a description of the task.

공식문서를 보니 비동기처리를 하기전에 XCTestExpectation객체를 생성하라는걸보니 given에 둬서 해당 함수가 비동기적으로 호출할 준비를 해준다는 느낌으로 가져가는게 맞을거같네요

@ffalswo2 ffalswo2 merged commit 078ab4a into main Dec 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🦁민재 민재's 🧪Test 테스트
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[TEST] Today Unit test
2 participants