본문 바로가기

난 iOS개발자/iOS

클린아키텍처 적용하기-02 테스트

728x90

클린아키텍처 적용하기-01 개념과 흐름이해
 
이전 글에 이어서 클린아키텍처를 적용하고 테스트 코드를 어떻게 작성할 수 있는지, 코드를 보고 따라가면서 그 장점을 알아보겠습니다.
 
클린아키텍처를 적용하고 나니 테스트할때 용이하다는 말이 무엇인지 와닿게 되었습니다.
아래 조금 지루한 내용일테지만 차근 차근 봐주시길 바랍니다.


 
앱의 버전을 최신 버전과 비교하고 업데이트 필요여부를 확인하는 요구사항이 생겼습니다. 설치버전의 숫자가 현재 앱 버전보다 최신버전이 높은경우 팝업을 띄우고 강제 업데이트(앱스토어 이동)를 진행하고자 합니다.
최신 버전은 LatestVersionDatasource을 채택한 구현체가 서버로부터 값을 받아와서 반환하고 있습니다.

protocol LatestVersionDatasource {
    func getVersion() -> String
}

위 프로토콜을 채택하고 getVersion()을 구현한 객체라면 누구든 Datasource의 자리로 들어갈수 있습니다.

final class LatestVersionDatasourceImpl: LatestVersionDatasource {
    func getVersion() -> String {
        "1.0.0" /// from server
    }
}

현재 앱의 버전은 1.0.0 입니다.
서버에서는 최신버전을 1.0.0 으로 내려주고 있기 때문에 현재 앱 버전과 동일합니다.
때문에 요구사항인 메이저업데이트 필요 여부에 대한 테스트를 확인할 수 없는 상황이네요.
 
우린 getVersion의 반환값이 더 다양하길 원합니다.
 
이를 위해서 약속된 값을 반환하는 객체를 만들어 보겠습니다. LatestVersionDatasourceImpl대신 미리 약속한 fake value를 반환하는 FakeLatestVersionDatasourceImpl을 구현합니다.

final class FakeLatestVersionDatasourceImpl: LatestVersionDatasource {
    private let fakeVersion: String
    init(fakeVersion: String) {
        self.fakeVersion = fakeVersion
    }

    func getVersion() -> String {
        fakeVersion
    }
}

LatestVersionDatasource 프로토콜을 채택하고 있기 때문에 사용하는 입장에서는 LatestVersionDatasourceImpl , FakeLatestVersionDatasourceImpl두 타입을 따로 구별하지 않습니다.
LatestVersionDatasource 를 사용하는 Repository 와 UseCase 를 마저 작성해보겠습니다.

protocol VersionRepository {
    func getInstalledVersion() -> String
    func getLatestVersion() -> String
}

final class VersionRepositoryImpl: VersionRepository {
    private let latestVersionDatasource: LatestVersionDatasource
    private let installedVersionDataSource: InstalledVersionDataSource

    init(_ latestVersionDatasource: LatestVersionDatasource,
			 installedVersionDataSource: InstalledVersionDataSource
	) {
        self.latestVersionDatasource = latestVersionDatasource
        self.installedVersionDataSource = installedVersionDataSource
    }
	
    func getInstalledVersion() -> String {
        installedVersionDataSource.execte()
    }

    func getLatestVersion() -> String {
        latestVersionInfoDatasource.getVersion()
    }
}

두 개의 datasource를 갖는것은 중요한게 아닙니다. 중요한것은 이러한 인터페이스가 고수준의 비즈니스 로직과 저수준 구현 세부사항을 분리하는데 도움이 되어야 한다는 것입니다.
최신정보를 가져올 UseCase 도 추가합니다.

protocol GetLatestVersionUseCase {
    func extcute() -> String
}

final class GetLatestVersionUseCaseImpl: GetLatestVersionUseCase {
    private let repository: VersionRepository
    init(_ repository: VersionRepository) {
        self.repository = repository
    }

    func execute() -> String {
        repository.getLatestVersion()
    }
}

마지막으로 버전을 비교하여 알맞은 업데이트 정책을 반환하는 UseCase 도 추가합니다.

enum UpdatePolicy {
    case forced
    case recommended 
    case ignore 
}

protocol DetermineUpdatePolicyUseCase {
    func execute(with installedVersion: String, latestVersion: String) -> UpdatePlolicy
}

final class DetermineUpdatePolicyUseCaseImpl: DetermineUpdatePolicyUseCase {
    func execute(
        with installedVersion: String, 
        latestVersion: String
        ) -> UpdatePlolicy {
        // 비교 로직 후 반환
    }
}

이렇게 추가된 객체들은 다음과 같이 사용할 수 있습니다.

final class ViewModel {
    private let determineUpdatePolicy: DetermineUpdatePolicyUseCase = .init()
    private let getInstalledVersion: GetInstalledVersionnUseCase = ...
    private let getLatestVersion: GetLatestVersionUseCase = ...

    func checkAppUpdate() {
        let installedVersion = getInstalledVersion.execute()
        let latestVersion = getLatestVersion.execute()
        let policy = determineUpdatePolicy.execute(with: installedVersion, latestVersion: latestVersion)
        /// 이하 policy에 대한 처리
    }
}

FakeLatestVersionDatasourceImpl을 사용하여 버전정보에 따른 동작이 요구사항과 잘 맞는지 확인할 수 있었습니다.
그러나 UI관련 로직이 없음에도 매번 코드를 실행하고 시뮬레이터나 기기로 앱을 켜서 동작을 확인하는것은 귀찮은 일이 분명합니다. 테스트코드를 작성해서 원하는 로직에 대한 검증을 하는것이 효율적입니다.
 
각 계층은 독립적이고 특정 역할에 충실합니다. 데이터 접근계층, 비즈니스 로직, 프레젠테이션 계층으로 분리되어있기 때문에 독립적으로 테스트할수 있게 만듭니다. 예를 들어 비즈니스 로직 계층은 데이터 소스나 UI 와 관련된 복잡성 없이 테스트할 수 있습니다.
 
이렇게 분리된 계층덕에 특정 요소의 모의 객체를 쉽게 만들 수 있습니다. 예를 들어 버전 정보를 확인하고 이에 대한 처리를 하는 클래스가 있을때, 실제 네트워크 응답 대신 모의 응답을 제공하는 모의 객체를 사용할 수 있습니다. FakeLatestVersionDatasourceImpl가 그 예시가 될 수 있습니다.
이렇게 하면 네트워크 상태나 외부 서비스 영향을 받지 않고 일관된 테스트 환경을 유지할수 있습니다.
 
테스트 코드를 작성하면서 확인해보겠습니다.
테스트코드로 작성하는 테스트메서드의 이름은 누구나 충분히 이해할 수 있도록 쉽게 짓습니다.
영어도 좋지만 개인적으로는 한글로 작성하는것이 훨씬 이해가 쉬운 느낌이 듭니다.
저는 DetermineUpdatePolicyUseCase 에 대한 검증을 해보려 합니다.
 
BDD 스타일의 테스트 코드를 넣어봅시다.

  • BDD(Behavior Driven Development)란?
    • TDD에서 파생된 개발 방법론
    • 개발자와 비개발자간의 협업 과정을 녹여낼수 있는 방법
    • 사용자의 행위를 작성하고 결과 검증을 진행
    • BDD로 테스트 코드를 작성함에 따라 설계 역시 행위의 중심이 되는 도메인 기반 설계가 됨
func test_앱_major넘버가_최신버전의_넘버보다_낮은경우_강제업데이트_반환하는가() throws {
    //given
    let latestVersion = "2.0.0"
    let installedVersion = "1.0.0"
    let useCase = DetermineUpdatePolicyUseCase()
	
    //when
    let result = useCase.execute(with installedVersion, latestVersion: latestVersion)
	
    //then
    XCTAssertEqual(result, .forecedUpdate)
}

단순히 하드코딩으로도 검증은 쉽게 이뤄질 수 있습니다.
만약 ViewModel에 DetermineUpdatePolicyUseCase 의 구현내용이 있었다면 테스트를 위해 메서드의 접근제어 수준을 공개수준으로 올려야 할테지만, 이처럼 비즈니스 로직을 따로 분리했기 때문에 쉽게 검증할 수 있게 되었습니다.
 
이제 하드코딩으로 만든 테스트가 아닌, 객체간 협력으로 플로우를 만들고 로직검증을 해보겠습니다.

func test_앱_major넘버가_최신버전의_넘버보다_낮은경우_강제업데이트_반환하는가() throws {
    //given
    let latestVersionDataSource: LatestVersionDatasource = FakeLatestVersionDataSourceImpl(fakeVersion: "2.0.0")
    let installedVersionDataSource: InstalledVersionDataSource = InstalledVersionDataSourceImpl()
    let repository: VersionRepository = VersionRepositoryImpl(latestVersionDataSource, installedVersionDataSource: installedVersionDataSource)

    let getLastestVersion: GetLatestVersionUseCase = GetLatestVersionUseCaseImpl(repository: repository)
    let getInstalledVersion: GetInstalledVersionUseCase = GetInstalledVersionUseCaseImpl(repository: repository)
    let latestVersion = getLastestVersion.execute() // "2.0.0"
    let installedVersion = getInstalledVersion.execute() // "1.0.0"
	
    //when
    let result = useCase.execute(with installedVersion, latestVersion: latestVersion)
	
    //then
    XCTAssertEqual(result, .foreceUpdate)
}

버전정보에 따른 테스트를 할 수 있도록 LatestVersionDatasource를 채택한 FakeLastestVersionDataSource를 사용하여 플로우를 연결하고 반환한 버전 정보에 대한 처리까지 검증을 할 수 있게 되었습니다.
 
latestVersionDataSource 변수는 LatestVersionDataSource 프로토콜을 채택한 객체를 할당 할 수 있습니다.
쉽게 교체가 가능해졌죠. 이렇듯 구조의 세부내용을 내가 원하는 대로 쉽게 바꿀수 있기 때문에 여러가지 시나리오를 검증하는데 큰 도움을 줍니다.
 
다음 포스팅에선 클린아키텍처를 적용하면서 겪은 문제점이나, 고민거리들을 정리한 글이 되겠습니다.
 
 

728x90