관리 메뉴

찹모찌의 기록일지

Clean Swift로 Unit Test 작성하기 본문

iOS

Clean Swift로 Unit Test 작성하기

찹모찌 2024. 4. 28. 19:43

Clean Swift를 채택한 위치기반 숏폼 플랫폼 Layover에서 작성했던 Unit Test에 관한 이야기.

1. Clean Swift란?

Clean Swift란?

Clean Swift를 처음 공부할 때 만들었던 발표 자료에서 발췌했다.

VIP Cycle을 단방향으로 돌며 동작한다. Interactor에서는 Business Logic만 처리하고 Presenter에서는 데이터를 가공하여 ViewControlelr에서는 UI Logic만 동작할 수 있도록 책임을 분리한 것이 특징이다.

이러한 구조속에서 Unit Test를 작성하려면 어떻게 해야 할까? Unit Test를 통해 어떤 동작을 확인해야 할까?

 

2. Clean Swift 속 Unit Test

이번 Unit Test의 목적을 두 가지로 정했다.

1) Business Logic이 올바르게 동작하는지 확인

2) VIP Cycle이 올바르게 순환하는지 확인

먼저 Business Logic이 올바르게 동작하는지 확인하기로 했다. 먼저 테스트해야 할 로직은 다음과 같다.

무한 스크롤 Logic

UICollectionView에서 기존 1, 2, 3 Cell로 구성되어있을 때, 무한 스크롤 효과를 주기 위해 3, 1, 2, 3, 1로 Cell 구성을 변경하고 마지막 1과 첫 번째 3에 도달했을 때 기존 Cell로 setContentOffset을 이용해 이동할 수 있도록 만들어 두었다. 이때 마지막 Cell을 삭제하게 된다면 Cell의 구조는 2, 1, 2, 1로 변경되어야 한다.

이 로직을 테스트 하기 위해서는 기존 3, 1, 2, 3, 1의 구조가 삭제 동작 시 2, 1, 2, 1로 변경되는지 확인해야 한다.

// MARK: Subject under test

var sut: PlaybackInteractor!

// MARK: Properties

typealias Models = PlaybackModels

// MARK: - Test lifecycle

override func setUp() {
    super.setUp()
    setupPlaybackInteracotr()
}

override func tearDown() {
    super.tearDown()
}

// MARK: - Test setup

func setupPlaybackInteracotr() {
    sut = PlaybackInteractor()
    sut.worker = MockPlaybackWorker()
}


func test_map일_때_마지막_무한스크롤셀을_지울_경우_deleteVideo를_호출하면_presentDeleteVideo를_호출한다() async {
    // Arrange
    let spy = PlaybackPresentationLogicSpy()
    let testPost: Post = Seeds.Posts.post1
    let playbackVideo: Models.PlaybackVideo = Models.PlaybackVideo(
        displayedPost: Models.DisplayedPost(
            member: Models.Member(
                memberID: testPost.member.identifier,
                username: testPost.member.username,
                profileImageURL: testPost.member.profileImageURL),
            board: Models.Board(
                boardID: testPost.board.identifier,
                title: testPost.board.title,
                description: testPost.board.description,
                videoURL: testPost.board.videoURL!,
                latitude: testPost.board.latitude,
                longitude: testPost.board.longitude),
            tags: Seeds.Posts.post1.tag))
    sut.parentView = .map
    sut.presenter = spy
    sut.posts = [Seeds.Posts.thumbnailImageNilPost, Seeds.Posts.post1, Seeds.Posts.post2, Seeds.Posts.thumbnailImageNilPost ,Seeds.Posts.post1]
    sut.playbackVideoInfos = [Models.PlaybackInfo(memberID: 0, boardID: 3), Models.PlaybackInfo(memberID: 0, boardID: 1), Models.PlaybackInfo(memberID: 0, boardID: 2),  Models.PlaybackInfo(memberID: 0, boardID: 3), Models.PlaybackInfo(memberID: 0, boardID: 1)]

    // act
    await sut.deleteVideo(with: Models.DeletePlaybackVideo.Request(playbackVideo: playbackVideo, indexPathRow: 3))

    // assert
    XCTAssertTrue(spy.presentDeleteVideoDidCalled, "deleteVideo가 presentDeleteVideo를 호출하지 않았습니다")
    XCTAssertEqual(sut.playbackVideoInfos[0].boardID, 1)
    XCTAssertEqual(sut.playbackVideoInfos[1].boardID, 2)
    XCTAssertEqual(sut.playbackVideoInfos[2].boardID, 1)
    XCTAssertEqual(sut.playbackVideoInfos[3].boardID, 2)
    XCTAssertEqual(sut.posts?[0].board.identifier, Seeds.Posts.post1.board.identifier)
    XCTAssertEqual(sut.posts?[1].board.identifier, Seeds.Posts.post2.board.identifier)
    XCTAssertEqual(sut.posts?[2].board.identifier, Seeds.Posts.post1.board.identifier)
    XCTAssertEqual(sut.posts?[3].board.identifier, Seeds.Posts.post2.board.identifier)
    XCTAssertEqual(sut.playbackVideoInfos.count, 4)
    XCTAssertEqual(sut.posts?.count, 4)
}

코드는 다음과 같다.

Mock Data와 sut를 활용하여 deleteVideo가 수행될 수 있도록 한다. 수행을 하고 나면 3, 1, 2, 3, 1의 구조가 2, 1, 2, 1로 변경되었는지 확인한다(비즈니스 로직 테스트의 대상이 PlaybackInteractor이므로 해당 컴포넌트가 sut가 된다).

이러한 방법을 통해 Business 로직이 제대로 동작하는지 확인할 수 있다.

다음으로 VIP Cycle이 올바르게 순환하는지 확인한다.

VIP Cycle 순환 테스트

동작의 주체를 sut로 놓고, VIP Cycle상 다음에 올 컴포넌트를 spy로 둔다.

Interactor의 경우 Presenter를 spy로 두고 테스트한다.

final class PlaybackPresentationLogicSpy: PlaybackPresentationLogic {
    var presentDeleteVideoDidCalled = false
    var presentDeleteVideoResponse: Models.DeletePlaybackVideo.Response!

    func presentDeleteVideo(with response: Layover.PlaybackModels.DeletePlaybackVideo.Response) {
        presentDeleteVideoDidCalled = true
        presentDeleteVideoResponse = response
    }
}

이런 식으로 PresentationLogicSpy를 두고

위의 테스트 코드 상에서 DidCalled가 true가 되었는지를 확인해 다음 컴포넌트가 제대로 호출되었는지를 확인한다.

이를 통해 Business Logic이 제대로 동작하는지, VIP Cycle이 제대로 순환하는지를 확인할 수 있다!


위와 같은 방식으로 Clean Swift 디자인 패턴에서 Unit Test를 작성했다. Unit Test를 작성하며 코드의 안정성과 다른 컴포넌트에 끼치는 영향을 확인하며 작업을 진행할 수 있었다. 진행한 프로젝트가 궁금하다면 아래 Repository를 참고하면 좋을 듯하다.

https://github.com/boostcampwm2023/iOS09-Layover

 

GitHub - boostcampwm2023/iOS09-Layover: 내 여정의 경유지들을 기록하다🧑‍🚀👩‍🚀👨‍🚀 - 위치기

내 여정의 경유지들을 기록하다🧑‍🚀👩‍🚀👨‍🚀 - 위치기반 숏폼 플랫폼. Contribute to boostcampwm2023/iOS09-Layover development by creating an account on GitHub.

github.com