제목: Isolating tasks in Swift, or how to create a testable networking layer.

최근 몇년동안 더욱더 멋져지고있는 iOS 아키텍처가 많다. 모두 유효하고 부분적으로 장단점을 가진다. 이들은 모두 같은 것을 다룬다. 바로 프레젠테이션으로부터 비지니스 로직을 분리하는 것이다. 오늘 나는 여러분이 다음 프로젝트의 아키텍처에 적용시킬 수 있지만 어떤 아키텍처를 사용하더라해도 쓸 수 있는, 간단한 개념을 적어보려 한다.

꽤 일반적인 네트워크 레이어
내 관점을 설명하기 위해, 나는 먼저 보통 네트워크 레이어를 어떻게 구현하는지에대해 이야기할 것이다.

나는 각기 다른 여러 네트워크 레이어를 보아왔다. 대부분 NetworkManager, ConnectionManager 이런 모습이다. 앱에서 한 클래스안에 있는 모든 API 호출을 담고있다. 이게 유효하고 동작할지라도, 소프트웨어 설계에서 핵심 개념인 단일책임(Single Resoponsibility)은 실패한 것이다.

ConnectionManager에는 좋다고 생각되는 책임들을 너무 많이 담고있다. 게다가 종종 싱글톤으로 구현된다. 그리고 나는 싱글톤이 필연적으로 나쁘다고 말하진 않겠지만, 싱글톤은 의존성으로서 주입될 수 없고 테스팅에 쉽게 목(mock)이 될 수도 없다.

네트워크 레이어는 일반적으로 싱글톤으로 구현된다네트워크 레이어는 일반적으로 싱글톤으로 구현된다


이것은 매우 자주있는 방법이다. 나는 이것을 MVVM나 MVP 아키텍처에서도 보았다.

다른 방법
데이터 접근 레이어는 다른 방법으로 구현될 수 있다. 네트워크 패칭에서 그 프로세스를 표현해보자.

네트워크 호출에 포함된 단계네트워크 호출에 포함된 단계


이 방법을 넣어 세단계로 네트워크 호출을 함축한다.
  1. 요청 만들기: 요청 만들기에서는 URL, method, 파라미터(URL 경로든 http 바디에든), HTTP 헤더를 설정한다.
  2. 요청 디스패치하기: 이것은 매우매우 중요한 단계이다. 이전단계에서 만들어지고 설정된 이 요청은 반드시 URLSession이나 이걸 덮는 레이어(예를들면 Alamofire)를 사용하여 디스패치 되야한다.
  3. 응답을 받고 파싱하기: 이 부분은 이전 두 단계와 분리되어 구현되야할 중요한 단계이다. 이것은 JSON이나 XML 응답을 검증하고 유효한 Entity(혹은 Model)에 파싱하는 곳이다.

여러분의 아키텍처가 깔끔해지고 테스트하기 좋아지길 원한다면, 이 세단계는 다른 오브젝트에서 구현되어야한다.

네트워크 레이어는 적어도 세 컴포넌트를 사용하여 구현되어야함네트워크 레이어는 적어도 세 컴포넌트를 사용하여 구현되어야함


  1. Request: Request 오브젝트는 네트워크 요청을 구성하는데 필요한 모든 것들을 가진다. 이 구조체(혹은 클래스)는 하나의 네트워크 요청을 구성하고있는 책임을 갖는다. 그리고 한 네트워크 요청에 한 Request 오브젝트굉장히 중요하다.
  2. NetworkDispatcher: NetworkDispatcherRequest를 받아 Response를 반환하는 역할의 오브젝트이다. 이것은 프로토콜로 구현될 수 있다. 구체적인 클래스(혹은 구조체)가 아닌 프로토콜로 코드를 짤 수 있지만, 절때 싱글톤으로 구현하지 말아야한다. 그렇게 한다면, 이 NetworkDispatcherMockNetworkDispatcher과 대체될 수 잇고, 이 목은 실제 네트워크 요청을 날리지 않는 대신에 JSON 파일로부터 응답을 받아준다. 이것이 자연스럽게 테스트하기 좋은 아키텍처를 만든다.
  3. NetworkTask: NetworkTaskTask라는 제네릭 클래스의 자식클래스이다. 이 Task(좀 있다가 더 설명할것이지만)는 비동기적으로든 동기적으로든 Input 타입을 받아서 Output 타입을 반환하는 책임을 가지는 제네릭 클래스이다. TaskRxSwiftReactiveCocoaHydraMicrofuturesFOTask나 아니면 간단하게 클로저를 이용해서 구현할 수 있다. 여러분에 달려있다. 여기서 중요한 부분은 세부적인 구현이 아니라 개념이다.

요청 만들기
RequestURLRequest를 만드는데 필요한 모든 구성에대한 책임을 가진 오브젝트이다.

네트워크 요청에대한 예시는 다음과같이 생겼을 것이다.
//
//  Request.swift
//
//  Created by Fernando Ortiz on 2/12/17.
//

import Foundation

enum HTTPMethod: String {
    case get, post, put, patch, delete
}

protocol Request {
    var path        : String            { get }
    var method      : HTTPMethod        { get }
    var bodyParams  : [String: Any]?    { get }
    var headers    : [String: String]? { get }
}

extension Request {
    var method      : HTTPMethod        { return .get }
    var bodyParams  : [String: Any]?    { return nil }
    var headers    : [String: String]? { return nil }
}
여기서 중요한 부분은 Request가 분리된 오브젝트로 구현되었다는 점이다. 물론 Moya promotes처럼 열거형으로 구현될 수도 있는데, 여러분이 원하는 것에 달렸다. 나는 개인적으로 객체지향 스타일을 선호하고, BaseRequest 클래스와 AuthenticatedRequest같은 자식클래스나 GetAllUsersRequest, LoginRequest같은 세부적인 요청을 구현하길 선호한다.



NetworkDispatcher 구현하기
NetworkDispatcher는 네트워크 요청을 디스패치하는 책임을 가지는 컴포넌트이다.

주의: 여기서부터 내 예제에는 RxSwift를 사용할 것이지만, 여러분은 여러분이 좋아하는 방법으로 구현하면 된다.
//
//  NetworkDispatcher.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//  Copyright © 2017 Fernando Martín Ortiz. All rights reserved.
//

import Foundation
import RxSwift

protocol NetworkDispatcher {
    func execute(request: Request) -> Observable<Any>
}
NetworkDispatcherRequest 오브젝트를 디스패치하고 응답을 반환하는 단일 책임을 가진다.
"
구체적인 구현대신 이 프로토콜을 사용하는것에서 멋진 점은 프로토콜 기반 구현은 쉽게 교체가능하게 만들어준다. MockNeoworkDispatcher은 실제 "네트워크" 작업을 실항하지는 한고 대신 JSON 파일에서 응답을 반환해주도록 하여 더욱 테스트하기 쉽게 만들어준다.

Task 고립시키기
Task는 하나의 로직 오퍼레이션을 실행시키는 책임의 간단한 오브젝트이다. 뒷단에서 사용자를 받아오거나, 로그인하기, 사용자 등록하기등이 있을 것이다. Task는 동기적으로나 비동기적으로 일어날 수 있지만, 클라이언트단에서 투명해야한다. 나는 편리한 추상화인 RxSwift의 Observable을 사용하길 좋아하는데, Promise, Signal, Future, 혹은 간단한 콜백이 충분할 수 있다.

Task의 간단한 구현은 아래처럼 될 수 있다.
//
//  Task.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//

import Foundation
import RxSwift

class Task<Input, Output> {
    func perform(_ element: Input) -> Observable<Output> {
        fatalError("This must be implemented in subclasses")
    }
}
나는 객체지향 스타일을 사용했지만, 연관 타입이나 타입 erasure같은 것도 좋은 방법으로 사용할 수 있다. 이것도 잘 동작할 것이다. 내가 이런 객체지향 스타일을 좋아하는 이유는 덜 조잡하고 구현하기 단순해 보이기 때문이다.

모든 TaskInput 타입과 Output 타입이라는 두 제네릭 파라미터를 필요로한다. TaskInput 오브젝트를 받아 Output을 반환하는 일을 포함한 작업을 수행하는데, Observable처럼 이것을 추상화하여 사용할 것이다.

Task를 네트워크 작업을 실행시키기위해 특별하게 만들어줘야한다.
//
//  NetworkTask.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//

import Foundation
import RxSwift

class NetworkTask<Input: Request, Output>: Task<Input, Output> {
    let dispatcher: NetworkDispatcher

    init(dispatcher: NetworkDispatcher) {
        self.dispatcher = dispatcher
    }

    override func perform(_ element: Input) -> Observable<Output> {
        fatalError("This must be implemented in subclasses")
    }
}
위에서 볼 수 있듯 ,NetworkTask는 두가지 제네릭 타입이 필요한데, InputOutput이다. Input이 반드시 Request 오브젝트이여야한다는 것은 당연한 일이다. NetworkTaskNetworkDispatcher로만 인스턴트화되어야 하므로 테스트하고 싶을때 MockNetworkDispatcher를 쉽게 보낼 수 있다.

아키텍처 검토하기
이 방법으로 비지니스 로직을 구현하면 복잡성을 설명하거나 테스트 용이함을 증가시키지 않고 결합력을 줄이는데 도움이 된다.

이 방법은 아래처럼 다이어그램으로 표현될 수 있다.

Task 기반 네트워크 레이어Task 기반 네트워크 레이어


결론
분리된 오브젝트에서 비지니스 로직 오퍼레이션을 고립시키는 것은 더욱 테스트하기 좋은 아키텍처로 만들기 때문에 좋은 방법이다. 복잡성을 줄여주고, 여러분이 사용하는 아키텍처를 전반적으로 독립시킨다. 이것은 ViewModel, Presenter, Interactor, Store 혹은 프레젠테이션 로직에서 비지니스 로직을 분리하는데 사용하기위한 어떤것 뒤에서든 사용될 수 있다.

이것이 나만큼 도움이 되었길 바란다. 뭔가 의심스러운 점이 있거나, 더 좋은 방법을 안다면 커멘트를 남겨달라.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret
제목: Keeping XCTest in sync on Linux

갱신이력:
2017.03.30 이 이슈에대해 프로세스를 추적하는 버그를 참조한 꼬릿말 추가
2017.03.30 코드 생성을 사용하는 솔루션을 대신해주는 appendix 를 추가
2017.03.31 생성된 파일의 타겟 패스를 명세하는 Sourcery 양식을 수정. 이것은 수공업의 리네이밍 단계를 절약해준다.

스위프트는 크로스-플랫폼이지만, 애플 플랫폼과 다른 OS에서 다르게 동작하는데, 주로 두가지 이유가 꼽힌다.
  • Objective-C 런타은 애플 플랫폼에서만 가능하다.
  • Foundation과 다른 core library들은 애플OS가 아닌 것의 구현이 따로 되어있다. 이 의미는 Foundation API는 macOS/iOS와 리눅스에서 다른 결과를 만들수도 있고, 혹은 그냥 아직 완전히 구현되지 않았을 수도 있다.
그러므로 라이브러리를 어떤 애플 플랫폼의 특정 기능에 의존하지 않게 짜면, macOS/iOS 그리고 리눅스의 코드를 테스트하기 좋은 전략이 된다.

리눅스에서 테스트 디스커버리
어떤 유닛 테스트 프레임워크는 실행해야 테스트를 찾을수 있게 해놓았따. 애플 플랫폼에는 XCTest 프레임워크가 있는데, 이것은 모든 테스트 수트와 테스트 타겟에있는 메소드를 돌기위해 Objective-C 런타임을 사용한다. Objective-C 런타임은 리눅스에서 사용할 수 없고 스위프트 런타임은 최근에 동등한 기능이 부족하기 때문에, 리눅스의 XCTest는 실행하고자하는 테스트 목록을 명시저으로 제공하도록 개발자들에게 요구한다.

allTests 프로퍼티
이 방식으로 실행하는 방법(스위프트 패키지 매니저에서 만들어진 컨밴션)은 여러분의 각 XCTestCase 서브클래스에 allTests라는 이름의 프로퍼티를 추가하는 방법이다. 이것은 테스트 함수와 그 이름들의 배열을 반환한다. 예를들어 한 테스트를 가지고있는 클래스는 아래처럼 생겼을 것이다.
// Tests/BananaKitTests/BananaTests.swift
import XCTest
import BananaKit
class BananaTests: XCTestCase {
   static var allTests = [
       ("testYellowBananaIsRipe", testYellowBananaIsRipe),
   ]
   func testYellowBananaIsRipe() {
       let banana = Banana(color: .yellow)
       XCTAssertTrue(banana.isRipe)
   }
}

LinuxMain.swift
이 패키지 매니저는 LinuxMain.swift라는 이름의 또다른 파일을 만드는데, 이것은 애플 플랫폼이 아닌 곳에서 동작시키는 테스트 실행자처럼 행동한다. 여기에는 XCTMain(_ :)을 호출하는데 이것은 모든 테스트 수트들의 리스트가 있다.
// Tests/LinuxMain.swift
import XCTest
@testable import BananaKitTests

XCTMain([
   testCase(BananaTests.allTests),
])

수작업의 유지보수는 잊어버리기 쉽다
이 방법은 두곳에서 수작업의 유지보수가 필요하기 때문에 분명 이상적이지 않다.
  1. 새로운 테스트를 추가할때마나 그 클래스의 allTests를 반드시 추가해야한다.
  2. 새로운 테스트 수트를 생성할때마다, LinuxMain.swift에서 XCTMain 호출을 반드시 추가해야한다.
이 두가지 단계 모두 잊어버리기 쉽다. Even worse, 불가피하게 그중 하나를 잊어버릴때, 뭔가 잘못되었음이 조금도 명확하지 않다. 테스트들이 리눅스에서는 통과할 것이고, macOS와 리눅스에서 실행되는 테스트 수를 손수 비교하지 않으면, 몇몇 테스트가 리눅스에서는 돌아가지 않는다는 점을 인지하지 못할수도 있다.

나에게는 이런일이 자주 발생했으므로 이것에대해 뭔가 조처를 취하기로 했다.

리눅스 테스트를 빠트리는 것에대해 보호하기
유지보수 단계중 잊어버릴때 자동으로 테스트 수트를 실패하게 만드는 매커니즘을 만들어보자. 각 XCTesetCase 클래스(와 그들의 allTest 배열)마다 아래 테스트를 추가할 것이다.
class BananaTests: XCTestCase {
   static var allTests = [
       ("testLinuxTestSuiteIncludesAllTests",
        testLinuxTestSuiteIncludesAllTests),
       // Your other tests here...
   ]

  func testLinuxTestSuiteIncludesAllTests() {
       if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
           let thisClass = type(of: self)
           let linuxCount = thisClass.allTests.count
           let darwinCount = Int(thisClass
               .defaultTestSuite().testCaseCount)
           XCTAssertEqual(linuxCount, darwinCount,
               "\(darwinCount - linuxCount) tests are missing from allTests")
       endif
   }
   // Your other tests here...
}
이 테스트는 Objective-C에의해 발견된 테스트 수와 allTest 배열에 항목 수를 비교하여 두 수가 일치하지 않음을 발견하면 실패를 띄워버릴 것이다. 정확히 우리가 원하던 것이다.

(Obj-C 런타임에서의 의존성이라는 의미는 애플 플랫폼에서만 동작하는 테스트라는 뜻이다. 리눅스에서는 컴파일되지 않을 것이고, #if os(macOS) ... 블럭으로 감싼 이유가 바로 그것이다)

allTests에 테스트를 추가하는 것을 잊어버렸을때 실패한 테스트
이것을 테스트 하기위해 다른 테스트를 추가하자, 이 테스트는 allTests 갱신을 깜빡한 경우이다.
import XCTest
import BananaKit

class BananaTests: XCTestCase {
   static var allTests = [
       ("testLinuxTestSuiteIncludesAllTests",
        testLinuxTestSuiteIncludesAllTests),
       ("testYellowBananaIsRipe", testYellowBananaIsRipe),
       // testGreenBananaIsNotRipe is missing!
   ]

   // ...

   func testGreenBananaIsNotRipe() {
       let banana = Banana(color: .green)
       XCTAssertFalse(banana.isRipe)
   }
}
이 테스트을 macOS에서 돌리면 우리의 보호 테스트가 실패할 것이다.

allTests 배열에 테스트를 추가하는 것을 까먹었기 때문에, 보호 테스트가 실패하고있다allTests 배열에 테스트를 추가하는 것을 까먹었기 때문에, 보호 테스트가 실패하고있다


나는 이것을 매우 좋아한다. 분명히, 모든 테스트마다 allTests 배열에 담고 싶을때만 동작할것이다. 즉 위에서 한것처럼 조건부의 컴파일에서 어떤 다윈- 혹은 리눅스 테스트를 감싸야할 것이다. 나는 이것이 많은 코드베이스의 한계를 만족시킬 것이라 믿는다.

LinuxMain.swift를 보호하기
다른 플랫폼은 어떤지 LinuxMain.swift가 완료되었다고 검증할 수 있을까? 이것은 좀 더 힘들다. LinuxMain.swift는 실제 테스트 타겟의 부분이 아니므로(될 수 없다), XCTMain으로 전달되는 내용은 쉽게 확인할 수 없다.

내가본 유일한 솔루션은 Run Script 빌드 단계를 테스트 타겟에 추가하고, LinuxMain.swift에 스크립트 파싱 코드를 넣어, 테스트 타겟에서 테스트 수트의 수에 배열의 항목을 비교한다. 아직 시도해보진 않았지만, 꽤 복잡하게 느껴진다.

업데이트: 코드 생성을 사용한 솔루션으로 appendix를 사용할 수도 있다.

결론
새 테스트에도 불구하고, 여전히 잠재적으로 잊어버릴 수 있는 두가지 때문에 완벽해보이진 않는다. 새로운 XCTestCase 클래스를 생성할때마다 반드시 아래를 행해야한다.
  1. 새 클래스에 testLinuxTestSuiteIncludesAllTests 테스트를 복사하여 붙여넣어야한다.
  2. LinuxMain.swift를 갱신해야한다.
그럼에도 불구하고, 새로운 테스트라는게 가장 일반적인 케이스(현재 테스트 수트를 새로운 테스트를 추가하고는 allTests 배열을 갱신하는 것을 까먹는 케이스)를 커버하므로 현재 상태 보단 상당히 낫다고 생각한다.

스위프트의 반영 능력이 더 강력해지는것을 기다릴 수 없다. 이런 모든것이 불필요하게 되면 좋겠다.


Appendix: Sourcery로 코드 생성
최근에 반복되는 주제로 되는 것처럼 보이는 것에서, Krzysztof Zabłocki가 짚어주었는데, 그의 훌륭한 코드 생성 툴인 Sourcery로 리눅스 테스트 기반을 유지보수할 수도 있다. 이것은 훌륭한 대체물이고 꽤 쉽게 세팅할 수 있다. 아래에 그 방법을 설명해 놓았다.

1. Sourcery 설치하기. 나는 스위프트 패키지 매니저 의존성으로 이것을 추가하는 것에서 동작하지 않았는데(빌드 실패), 스위프트3.1이 새로 나오면서 관련된 일시적인 문제가 아닐까 생각이 된다. 결국 가장 최신 배포을 다운받아서 바이너리를 직접 실행시켰다.

2. LinuxMain.stencil이라는 이름의 파일을 생성하는데, 아래 자료를 함께 넣는다. 여러분의 프로젝트에서 편한 곳에 저장한다. 나는 sourcery/의 하위 폴더에 넣었다.
// sourcery:file:Tests/LinuxMain.swift
import XCTest
{{ argument.testimports }}

{% for type in types.classes|based:"XCTestCase" %}
{% if not type.annotations.disableTests %}extension {{ type.name }} {
  static var allTests = [
  {% for method in type.methods %}{% if method.parameters.count == 0 and method.shortName|hasPrefix:"test" %}  ("{{ method.shortName }}", {{ method.shortName }}),  {% endif %}{% endfor %}]}
{% endif %}{% endfor %}

XCTMain([
{% for type in types.classes|based:"XCTestCase" %}{% if not type.annotations.disableTests %}  testCase({{ type.name }}.allTests),
{% endif %}{% endfor %}])
// sourcery:end
이것은 Ilya Puchka가 작성한 양식을 기반으로 하였다. 나는 그냥 처음과 끝에 //sourcery:... 표시를 추가했다. 이것은 생성된 파일의 경로를 결정한다(Sourcery 0.5.9가 필요함).여러분도 볼 수 있듯, 이것은 양식화 언어(templating language)와 스위프트 코드를 합쳐놓았다. Sourcerey를 호출할때, 여러분의 소스코드에서 타입들을 파싱하여 여러분이 보내온 양식에 맞춰 코드를 생성하는데 쓰일 것이다. 예를들어 {% for type in types.classes|based: "XCTesetCase" %}로 시작하는 루프는 XCTestCase를 상속하는 모든 클래스를 돌면서 allTests 프로퍼티를 담은 익스텐션을 생성할 것이다.

3. 여러분의 테스트 클래스에 있는 allTests 정의를 제거하기. 우리는 다음 단계에서 Sourcery로 이것들을 생성할 것이다. 이미 testLinuxTestSuitIncludesAllTests 메소드를 추가했었다면 이것도 제거하거나 여기서부터 떠나게 만들자. 영향을 주진 않고 여전히 이슈를 찾고 있을 것이다, 예를들어, 테스트를 추가하고나서 Sourcery를 실행시키지 않았을때, 더이상 반드시 엄격하게 하진 않을 것이다.

4. 프로젝트 폴더에서 Sourcery를 실행하기.
$ sourcery --sources Tests/ \
   --templates sourcery/LinuxMain.stencil \
   --args testimports='@testable import BananaKitTests'
Scanning sources...
Found 1 types.
Loading templates...
Loaded 1 templates.Generating code...
Finished.
Processing time 0.0301569700241089 seconds
이것은 현재 있던 Tests/LinuxMain.swift 파일을 생성된 코드로 덮어쓸 것이다.
// Generated using Sourcery 0.5.9 — https://github.com/krzysztofzablocki/Sourcery

// DO NOT EDIT

import XCTest@testable
import BananaKitTests

extension BananaTests {
    static var allTests = [
          ("testYellowBananaIsRipe", testYellowBananaIsRipe),
          ("testGreenBananaIsNotRipe", testGreenBananaIsNotRipe),
    ]
}

XCTMain([
     testCase(BananaTests.allTests),
])
이 작은 예제에선 두 테스트와함께 한 클래스만 있지만, 여러 테스트 클래스에도 역시 동작할 것이다.

5. 마지막(선택적인) 단계에는 더이상 생성된 빌드에서 생겨난 파일이 필요없으므로 그것을 제거한다.
$ rm LinuxMain.generated.swift
그리고 이게 다다. 모든 빌드에 실행되는 스크립트를 위해 두가지 터미널 명령(Sourcery를 호출하고 파일을 제거하는)을 추가하고, 여러분의 리눅스 테스트들은 이제 항상 최신으로 유지될것이다. 매우 멋지다!



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret

안드로이드 프로젝트에서 MVC, MVP, MVVM을 위한 간략한 가이드

Model View Controller(MVC)
MVC 디자인 패턴은 세가지 양상으로 앱을 쪼갠다: Model, View, Controller. 일들을 강제로 분리시켜 도메인 model과 controller 로직을 유저 인터페이스(View)로부터 분리시켜준다. 결과적으로 앱을 유지보수하기 간편하고 테스트하기 쉽게 만들어준다.

Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

Controller
Controller는 들어온 요청을 처리하는 역할을 한다. Vie를 통해 사용자의 입력을 받으면 Model의 도움으로 사용자의 데이터를 처리하고 다시 그 결과를 View에 보내준다. 보통은 View와 Model 사이에 중재자 역할을 한다.

Model View Presenter(MVP)
이 패턴은 Controller 대신 Presenter가 들어간 MVC와 유사한 형태의 패턴이다. 이 디자인 패턴은 세가지 주 양상으로 앱을 쪼갠다: Model, View, Presenter


Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

Presenter
Presenter는 View의 도움으로 모든 UI 이벤트를 다루는 역할이다. view를 통해 사용자로부터  입력을 받고, Model의 도움으로 사용자의 데이터를 처리한 뒤, 다시 View에 결과물을 돌려준다. View와 Controller에서와는 다르게 View와 Presenter는 서로 완전히 분리되있고 인터페이스에의해 서로 소통하는 방식이다.
또한 Presenter는 Controller처럼 들어오는 요청 트래픽을 관리하지 않는다.

MVP 패턴의 요점
  • 사용자는 View에서 상호작용한다.
  • View와 Presenter는 one-to-one 관계를 가진다. 이 의미는 하나의 View는 하나의 Presenter에 맵핑된다.
  • View는 Presenter에 참조하고 있지만, Model에는 참조하지 않는다.
  • View와 Presenter 사이에 두 방향으로 소통할 수 있다.

Model View ViewModel(MVVM)
MVVM은 Model-View-ViewModel로 정의된다. 이 패턴은 View와 ViewModel 사이에서 두방향 데이터 바인딩을 지원한다. 이것은 ViewModel에서 View에게 자동으로 변화를 전달할 수 있다. 일반적으로 ViewModel에서 View로 변화를 알림받는 옵저서 패턴을 사용한다.


Model
Model은 비즈니스 로직(즉 비즈니스 Model)과 데이터 접근 기능(Data Model)을 담은 클래스들의 집합으로 표현한다. 또한 데이터가 어떻게 바뀌고 다뤄지는지에대한 비즈니스 규칙을 정의한다.

View
View는 UI 컴포넌트를 표현한다. View는 Controller로부터 받은 결과의 데이터를 화면에 표시하는 역할만을 가지고 있다. 또한 Model을 UI에 넣어 적용하는 일도 한다.

ViewModel
ViewModel은 View의 상태를 유지, View의 액션 결과로 Model을 다루기, View 자체에서 이벤트를 트리거하는 그런 메소드나 명령, 다른 프로퍼티들을 노출시키는 역할을 한다.

MVVM 패턴의 요점
  • 사용자는 View에서 상호작용한다.
  • View와 ViewModel은 many-to-one 관계를 가진다. 그 의미는 여러 View는 하나의 ViewModel에 맴핑될 수 있다.
  • View는 ViewModel에 참조되지만 ViewModel은 View에대해 모른다.
  • View와 ViewModel의 사이에 두방향 데이터 바인딩을 제공한다.

안드로이드 구현




앞으로 "Controller"라는 용어를 앞에서 말한 Controller, Presenter, ViewModel과 같은 의미로 사용할 것이다.

일반적으로 안드로이드에서는 Activity 클래스가 Controller이고 Fragment 클래스가 View 영역이다. 그러나 이것은 코드의 재사용성을 줄인다. 또한 Fragment와 Activity는 제한적으로 화면 전환 애니메이션이 가능하다.


View 영역(layer)을 위한 UI 클래스(i.e. LinearLayout) 
View 영역은 LinearLayout이나 ViewGroup과 같은 View(UI) 엘리먼트를 상속하여 View를 구현할 수 있다.
  • Activity / 앱 플로우의 독립적인 기능을 재사용할 수 있다.
  • Activity 수를 줄인다.(앱 용량을 덜 잡아 먹는다)
  • Controller와 의존적인 부분을 줄인다.

(독립된) Controller 클래스
Controller 클래스는 어떤 안드로이드의 클래스를 상속받아서도 안된다. Activity와 Fragment로부터 독립적이게 해주어야 재사용이 가능할 것이다.
  • Controller를 가볍게 만들어라, View와 Model을 연결해주는 역할만 하면 된다.(단일 책임 원칙)
  • 이벤트를 다른 Controller에게 넘겨주어라(i.e. analytics)
  • 안드로이드 클래스로부터 분리해라 재사용을 위해

관련된 좋은 습관들
Activity 클래스의 의존성을 줄이기
  • Controller는 추상화에 의존한다(interface)
  • 시스템 구성과 의존성을 컨트롤하기위해 코드 중심에 위치시킨다.
  • 차후에 의존성 주입(dependency injection)을 옮길 수 있게 한다.

Analytics, A/B 테스트 등을 분리하기 위한 파사드(facade)
  • 우리는 여러 기록장치를 사용하기 때문에 파사드를 이용해 이 API들을 한데 모아둘 필요가 있다.
  • A/B 테스트나 다른 임시적인 기능들은 추상화돼있어야하고, 분리된 Controller나 파사드를 통해 접근가능해야한다.

이것들은 어떻게 생겼을까?
테스트에 용이
저렇게 잘 구현하여 Activity로부터 완전히 분리시킨다면, 테스트하기 아주 수훨해 질 것이다. 일반적으로 Activity는 수많은 것(디바이스의 시스템 구성, 네비게이션, 스타일, 액션바..)을 다루며 이것이 테스트의 범위를 너무 크게 만들어버린다.

위와같은 방법으로 Robelectric 테스트는 모든 의존성의 모의 객체(mock)를 만들어주고, 바깥에서 유닛 테스트 할 수 있게 해준다.

사용가능한 라이브러리&프레임워크
아래 안드로이드 프레임워크는 앞서 말한 것들을 구현할 수 있게 해준다. 우리는 지금 시점에서 저것들이 필요하지 않을 수 있으나, 나중에 코드베이스 전반에 걸쳐 적용시켜보려한다면 손쉽게 사용해볼 수 있을 것이다.

  • Square mortar: Activity 라이프 사이클의 행동으로부터 분리시켜, View를 가볍게 만들고 View를 Controller와 한 쌍으로 만들어주는 간편한 라이브러리
  • inloop AndroidViewModel: 엄청난 양의 코드 없이 Fragment나 Activity로부터 데이터와 상태를 분리시켜준다. 벙어리(dumb) View가 되는걸 줄인다.
  • sockeqwe mosby: 현대 안드로이드 앱을 위한 Model-View-Presenter 라이브러리.


'그 외' 카테고리의 다른 글

[번역] 리액티브 프로그래밍이란?  (0) 2017.03.03
(번역)Android Architecture  (0) 2016.10.01

WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret


유닛 테스트에서 가장 힘든 시점은 시작 시점이다. 그 이유는 모든 아키텍처가 유닛 테스트 되지 않기 때문이다. 만약 유닛 테스트를 하고자 한다면(Part1(링크)에서 왜 해야하는지 설명했다.) 앱의 아키텍처를 주의깊게 만들어야 한다.


이 아키텍처에 관해 좀 더 세부적으로 들어가기 전에 먼저 한가지 키포인트를 강조하고 싶다:

우리 아키텍처를 보다 더 테스트하기 쉽게 만드는 과정은, 코드를 다른 방면으로도 더 낫게 만들어 줄 것이다. 보통 테스트 가능한 설계는 좋은 소프트웨어 설계와 직결된다.

테스트 가능한 아키텍처로 바꿀 때, 우리는 앱 컴포넌트들을 더욱더 독립적으로 만들어야 하는데 특히 외부 라이브러리로부터 분리해야한다. 이렇게 바꾸면 더 다루기 쉬워지고, 이 점은 소프트웨어 개발의 불변의 법칙이다. 또한 우리는 앱을 만들때 시작 시점에 어떻게 해야하는지, 긴 개발기간동안 어떻게 시간을 단축시킬 수 있는지에대해 주목해볼 것이다.

이제 이 일을 정확히 어떻게 하는지 보자

ViewController와 함께 다루기
모의 객체로 ViewController를 본따는 일은 쉽지 않다. ViewController를 테스트 하는것도 쉽지 않은 일이다. 따라서 무턱대고 작업해서는 안된다.

음... 뭐라고?

우리는 우리에 맞는 MVVM 아키텍처를 사용할 것이다. MVVM에 대한 글이 수도 없이 많기 때문에(아래 링크 참고) 이 부분에 대해 깊이 들어가지는 않겠지만 기본 원칙은 다음과 같다. 모든 로직을 ViewController 바깥에 두어 UIKit과 로직이 섞이지 않게 한다. 이 로직에는 모든 모델의 변화, 다른 서비스를 호출, 상태 변화, 예외 처리 등이 있다. 이 모든 로직은 ViewModel에 담겨있다. 모든 ViewController는 ViewModel로부터 데이터를 바인딩하고 유저 인터렉션에 그것을 보내며, 유저 인터렉션은 애니메이션이나 뷰를 준비하는 화면 표시의 코드이다. 아래에 MVVM에관한 글이 더 있다:


MVVM에는 각기 다른 관점들이 존재한다. 어떤 사람들은 ViewModel이 모델과 한께 초기화된 값 타입이여야한다고 하기도 하지만 우리는 조금 다르다. 우리 아키텍처에서의 ViewModel은 UI 상태를 가지고 있고, 다른 서비스를 호출하며, ViewController에 보여줄 가공되지 않은 데이터를 제공하는 역하을 한다.

이렇게 함으로서 ViewController는 굉장히 가볍고 간단한 UIKit 관련 레이어가 되므로 대부분의 앱 로직을 쉽게 테스트할 수 있다.

(Note: ViewController를 테스트 할 수도 있지만 솔찍한 내 의견은 MVVM이 MVC보다 더 나은 아키텍처라고 생각한다.)

의존성 주입(Dependency Injection)
유닛 테스트에서 각 '유닛'은 완전히 분리되어 있기 때문에 앱의 각 컴포넌트를 독립시킬 필요가 있다. 우리가 테스트 하고 있는 각 클래스에 '약한' 의존성을 제공할 방법이 필요하고, 의존성때문에 특정 테스트가 실패하는 것은 아닌지 알아야한다.(This means that we need a way to provide “sterile” dependencies to each class we are testing, to know for certain tests won’t fail because of the dependencies.)

의존성 주입이란 그냥 한 클래스에 외부적인 의존성을 제공한다는 의미의 그럴싸한 표현이다. 클래스가 자기 스스로 의존성이 생길 수는 없다. 다른 클래스를 호출하고자 한다면 그 객체를 초기화때 파라미터로 받아두어야한다. (이것을 constructor injection이라 부른다)

또 다른 한가지 양상은 모든 의존성은 프로토콜 객체로서 선언된다는 것이다. 이러한 방법으로 그 클래스가 필요로하는 오브젝트를 메소드에 담아 우리가 원하는 클래스/구조체의 객체를 초기화할때 쉽게 전달할 수 있다.


이렇게하면 우리의 통제된 의존성을 클래스에 제공할 수 있게 해주고, 나머지 앱 부분으로부터 와넞ㄴ히 독립적이게 만들어준다. 또한 모의 객체 의존성과 함께 깔끔하고 작은 일을 할 수 있게 해준다.

이런식으로 코드를 작성하면 꼭 테스트 뿐만 아니라 코드를 분리시킬 수 있다는 점에서 좋은 방법이다. 그 클래스는 구체적인 구현에 의존하지 않고 그냥 프로토콜로서 들고 있는 것이므로 다른 클래스의 구현을 변경하더라해도 그 클래스는 건드리지 않아도 된다.

외부 프레임워크
만약 클래스가 외부 프레임워크에 의존하고 있으면 어떻게 될까? 우리 클래스가 NSURLRequest나 CoreData에 의존하고 있으면 어떨까? 아마 꼬일 것이다.

우리는 외부 프레임워크를 감싸는 helper를 만들 것이다. 기본적으로 helper는 감싸고 있는 외부 프레임워크의 무엇이든 불러오고, 프레임워크에 함수 호출을 전하며, 외부 프레임워크에 의존없이 우리 코드베이스에서 사용할 수 있는 형태의 결과로 변형하는 역할을 한다. 한가지 일반적인 규칙은, helper는 한 프레임워크당 하나씩 불러온다.

helper 안의 로직은 가능한 작아야한다. ViewController처럼 테스트 하기 쉽지 않기 때문이다. 그리고 그 프레임워크의 같은 기능을 재정의하는것이 아니라 helper를 코딩 기준과 필요한 것에 맞추어야한다.

다른 의존성에도 같은 원리를 적용시켜 모든 클래스는 세부적인 구현을 하는게 아니라 helper에 맞춘 프로토콜의 객체를 받는다.

이렇게하면 커다란 이점이 있다. 예를들어 당신이 CoreData가 너무 복잡해서 Realm으로 바꾸려 할때, 오직 한 클래스만 고치면 된다.

그러면 자신의 의존성을 제공하지 않는 클래스는 무엇이 있을까? 바로 서비스 팩토리이다.

서비스 팩토리(Service Factory)
서비스 팩토리는 앱 전체에 의존성을 만드는 변수를 get-only로 모아놓은 집합체이다. 예를들어 ViewModel은 APIServiceProtocol 타입에 의존성을 가지는데, 그것이 ViewModel로서 컨스트럭트 될 것이다. (apiService: ServiceFactory.apiService)

클래스가 다른 것을 호출하는 장소는 한 곳에 모여있어야 한다. 이것은 마치 통제실 같은 느낌을 주며, 어떤 개발자가 보아도 한번에 이 클래스가 어떻게 돌아가는지 알 수 있어야 한다.

또한 의존성 구현을 맞바꾸는 유일한 장소이기도 하다. 예를 들어보자면 싱글톤에서 일반 객체로 바꾸는데 5초정도 걸린다. 그렇게 하기 너무 크다면 한 클래스를 두 부분으로 쪼개어 시간을 절약할 수 있다.

그리고 여러분의 클래스에 더미나 목(mock) 의존성을 제공하여 실행때 인자레 의존하므로 UI 테스트나 디버깅에 유용하다. UI 테스트시 네트워크로부터 독립적이거나 백엔드에서의 기능은 여전히 잘 동작하고 있을 것이다.

좋다. 이제 우리 앱이 실제 구현과 모의 객체를 맞바꿀 수 있다. 그러나 이것을 어떻게 할 수 있을까?

모의 객체(Mocks)
모의 객체는 여러분이 테스트하고 있는 클래스의 의존성으로서 같은 프로토콜을 따르는 클래스 혹은 구조체이다. 모의 객체는 클래스에 유닛테스트 할 수 있게 해준다.모의 객체는 모통 no-op 메소드나 유닛 테스트에 유용한 작은 기능들을 가지고 있다.

일반적인 역할은 모의 객체가 테스트할 타겟 속으로 들어간다는 점이다. 이렇게 하면 모의 객체에 구현된 코드들이 우리 앱을 더럽히지 않을 수 있다.

당신은 모의 객체 의존성만 생각해선 안된다. 완전한 테스트를 위해 종종 모의 객체 델리게이트 오브젝트를 만들어야한다. 정확한 원칙은 이것이지만 노력하고자하는 델리게이트 메소드를 검증해야 한다는 점을 잊어선 안된다.

Swift에서는 제한된 런타임 접근 때문에 아직 안드로이드용 Mockito나 Objective-C용 OCMock과 같은 좋은 모의객체 프레임워크가 나오지 않았다. 따라서 직접 모의객체를 만들어보자.

지루한 작업일 수 있으나 모의객체 프레임워크를 쓰는 것 보다 더 자유롭게 작업할 수 있을것이다. 대부분 모의객체가 보편적으로 다음과같은 설계를 가진다는 것을 알아냈다:
  1. 당신이 모의로 할 프로토콜을 구현한다.
  2. 각 메소드는 모의객체 안에 methodDidGetCalled 라는 불리언 프로퍼티를 가지고 있어야하고 requestedParameterX 프로퍼티는 옵셔널하게 가지고 있는다. 이 메소드 구현은 보통 마지막에 이 프로퍼티를 설정하는 것이다. 여러분은 테스트한 클래스에 그 메소드가 옳바른 파라미터와 함께 호출 되었는지 나중에 확인해 볼 수 있다.
  3. 만약 메소드가 리턴값이나 완료 핸들러의 뭉치를 가진다면 구조체에 methodXShouldFail이라는 불리언 프로퍼티가 있다. 이 메소드 구현은 불리언을 체크하고 성공하든 못했든 결과를 반환한다. 이렇게하면 테스트한 클래스에 실패가 생겼을 때 다루기 유용해진다.

테스트 작성하기
앱을 옳바르게 준비하고, 무엇을 테스트할지 안다면 이번에는 꽤 같단하게 끝날 것이다. 이번에는 단지 테스트의 동작을 확인한다.

테스트를 도와주는 수많은 라이브러리가 존재한다. 먼저 애플의 XCTest이다. Xcode와 연동하여 사용할 수 있고, 타이핑할 것이 좀 많기는 하나 꽤 좋은 테스트 라이브러리이다. 그리고 써드파티 라이브러리인 QuickNimble이 있다. 우리는 Nimble과 함께 XCTest를 사용한다.

다른 메소드를 호출하는 메소드를 위해, 그 메소드가 호출되었는지, 옳바른 파라미터를 전달했는지 체크한다.

당신의 클래스가 델리게이트에게 알리는지 혹은 옳바른 콜백을 호출하는지 확인한다.

이제 당신이 할 수 있는 모든 것을 테스트 할때까지 두 세번정도 이 일을 하면 된다.

이 두 포스트를 통해 왜 테스트를 해야하는지 아는데 도움을 주고, 유닛테스트를 어떻게 시작하는지에대한 가이드가 되면 좋겠다. 즐거운 테스팅하길 바란다!








WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret

약 4달전, 우리팀(Marco, Arne, and Daniel)은 새 앱의 모델 레이어를 설계하기 시작했다. 우리는 테스트를 개발 과정에서 사용하고 싶었고, 회의를 거쳐 XCTest를 테스트 프레임워크로 정했다.

(테스트도 포함한) 우리의 코드베이스는 190개의 파일과 18,000 라인의 소스로 544KB까지 커져있었다. 우리 테스트에 들어가보면 우리가 테스트할 코드의 2배정도 되는  1,200KB 크기나 된다. 아직 프로젝트가 끝난 상황은 아니지만 거의 마무리 단계에 있다. 이 글을 통해 우리가 무엇을 배웠는지, 일반적인 테스트에 관하여나 XCTest에 관한 주제를 공유하고 싶다.

이 프로젝트는 아직 앱 스토어에 올라가지 않고 진행중이기 때문에 몇몇 클래스 이름이나 메소드 이름은 계속 바뀌어오고 있는 중임을 유의하라.

우리가 XCTest를 고른 이유는 간단하고 Xcode IDE와 잘 결합되기 때문이다. 이 글이 여러분의 XCTest를 고르거나 다른 것을 고를 때 결정을 도와줄 수 있길 바란다.

우리는 이 이슈와 비슷한 주장으로 이어가려 노력했다.

왜 테스트 해야하나
article about bad practices in testing에서 언급했듯, 많은 사람들이 "우리가 코드를 바꿀 때만 테스트 할 가치가 있다"고 생각한다. 이것에 대해 더 명확하게 짚고 싶으면 위 글을 읽어보면 된다. 그러나 사실 첫 버전의 코드를 작성할 때는 코드를 수정하는데 많은 시간이 들 수 밖에 없음을 인지해야한다.—프로젝트가 진행됨에 따라 더 많은 기능들이 추가되며, 그러면 코드 여기저기를 조금씩 수정해야 할 것이다. 따라서 1.1버전이나 2.0버전의 작업이 아니더라도 여전히 수많은 변경할 부분이 있을 것이고, 이때 테스트는 많은 도움을 줄 것이다.

우리는 아직도 최초버전의 프레임워크를 완성하는 과정에 있으며 최근데 10 man months 이상동안 1,000개의 테스트 케이스를 통과시켜 왔다. 우리 프로젝트 아키텍처가 명확한 버전을 가지고 있지만, 여전히 그 방법으로 코드를 수정하고 맞추고 있다. 계속 증가하는 테스트 케이스들은 이렇게 우리를 도와왔다.(원문: The ever-growing set of test cases have helped us do this.)

테스트는 우리 코드의 품질을 안정적으로 만들 수 있게 해주고, 코드를 부수지 않고 리팩토링이나 수정을 할 수 있는 능력을 가지게 해준다. 그리고 모든 코드가 합쳐지지 않아도 매일 코드를 실제 돌려볼 수 있게 해주었다.

XCTest는 어떻게 동작할까
애플은 XCTest 사용하기라는 문서를 제공한다. 테스트는 XCTestCase 클래스의 서브클래스 안에 그룹되어 만들어진다. test로 시작하는 각 메소드들이 실제 테스트이다.

테스트는 간단한 클래스나 메소드이기 때문에, 우리가 원하는 것에 맞춰 @property나 필요한 메소드를 테스트 클래스에 추가할 수 있다.

우리는 코드를 재사용하기 위해 모든 테스트 클래스의 수퍼클래스는 일반적으로 TestCase이다. 이 클래스(TestCase)는 XCTestCase의 서브클래스이다. 모든 테스트 클래스는 TestCase를 수퍼클래스로 한다.

또한 TestCase 안에 다 같이 사용하는 헬퍼 메소드도 하나 넣겠다. 그리고 각 테스트에 필요한 프로퍼티도 넣겠다.(원문: And we even have properties on it that get pre-populated for each test.)

네이밍
test라는 단어로 시작하는 메소드가 하나의 테스트이고, 일반적으로 테스트 메소드는 아래와 같이 생겼다:

우리 모든 테스트들은 "testThatIt"으로 시작한다. 테스트 네이밍에서 자주 쓰는 또 다른 방법은 testHTTPRequest처럼 테스트된 클래스나 메소드 이름을 사용하는 것이다. 그러나 이것은 가볍게 보기만해도 그 테스트의 의미를 바로 알 수 있을 것이다.

"testThatIt" 스타일은 우리가 원하는 결과에 초점이 쏠리고, 대부분의경우 한번에 이해하기 힘들다.

각 제품 코드 클래스의 테스트 클래스가 있고, 어떤 것은 Test가 접미에 붙기도 한다. HTTPRequestHTTPRequestTests클래스가 커지면 이것을 토픽에 따라 카테고리로 쪼개는 작업을 한다.

앞으로 영원히 테스트를 할 필요가 없으면 접두에 DISABLED를 붙인다:

이렇게하면 검색하기도 쉽고, 더이상 메소드 이름이 test로 시작하지도 않음으로 XCTest가 알아서 이 메소드를 생략한다.

Given/When/Then
우리는 모든 테스트를 Given-When-Then으로 나누어 만드는 패턴 구조를 사용한다.

given은 모델 오브젝트들을 만들거나 테스트를 위한 특정 시스템 상태로 만들어 테스트 환경을 셋업하는 영역이다. when은 테스트 하고 싶은 코드를 가지고 있는 영역이다. 대부분 테스트할 메소드 하나를 호출한다. then은 액션의 결과를 확인하는 역역이다. 우리가 기대하던 결과가 나왔는지, 오브젝트가 변경되었는지등을 확인한다. 이 영역은 assertion으로 구성되있다.

아래에 꽤 간단한 테스트가 있다:

이 기본 패턴을 따름으로서 더 짜기쉽고 이해하기 쉽게 해준다. 가독성을 높히기 위해 해당 영역의 상단에 "given", "when", "then"을 주석으로 달아놨다. 이 경우는 테스트된 메소드가 즉시 눈에 띈다.

재사용 가능한 코드
테스트를 여러번 하다보니, 테스트 코드 속에 자꾸 자꾸 재사용되는 코드를 발견했다. 비동기적 처리를 완료할때까지 기다리거나, CoreData 스택을 메모리에 옮기는 그런 코드들을 중복해서 사용하고 있었다. 우리가 최근에 사용하기 시작한 또다른 유용한 패턴은 XCTestCase 클래스에서 직접 프로토콜을 델리게이트하는 것을 구현하는 것이다. 이렇게 하면 엉성하게 델리게이트를 모의객체로 만들지 않고, 꽤 직접적인 방법으로 테스트 할 클래스와 소통할 수 있다.

It turned out that this is not only useful as a collection of utility methods. The test base class can run its own -setUp and -tearDown methods to set up the environment. We use this mostly to initialize Core Data stacks for testing, to reseed our deterministic NSUUID (which is one of those small things that makes debugging a lot easier), and to set up background magic to simplify asynchronous testing. 

Another useful pattern we started using recently is to implement delegate protocols directly in our XCTestCase classes. This way, we don’t have to awkwardly mock the delegate. Instead, we can interact with the tested class in a fairly direct way.

모의객체(Mocking)
우리가 쓰는 모의객체 프레임워크는 OCMock이다. 이 모의객체 주제의 아티클에서 이야기하듯, 모의객체는 메소드 호출에 준비된 결과를 반환하는 오브젝트이다.

우리는 모의객체를 한 오브젝트의 모든 의존성을 위해 사용한다. 이렇게 하면 타깃 클래스를 독립적으로 테스트할 수 있다. 단점이 있다면, 그 클래스에서 뭔가 바뀌게되면 그 클래스에 의존하는 다른 클래스의 유닛 테스트를 자동으로 실패로 만들지 않는다. 그러나 우리는 모든 클래스를 함께 테스트하는 통합 테스트를 하여 이 문제를 해결할 수 있다.

우리는 'over-mock'하지 않도록 주의해야하는데, 이것은 테스트할 하나를 제외한 나머지 모든 오브젝트를 모의객체로 만드는 것이다. 우리가 처음 시작할 때 이런 방식으로 테스트 했었고, 심지어 메소드에 입력하기위해 사용된 간단한 오브젝트까지도 모의객체로 만들었다. 이제는 많은 오브젝트를 모의객체 없이 사용하는 방법으로 테스트 하고 있다.

모든 테스트 클래스를 위한 우리 일반적인 슈퍼클래스의 일부이고, 한 메소드를 추가한다. 
이것은 메소드/테스트 마지막에서 검증하는 모의객체이다. 이것이 모의객체 사용을 더욱 편리하게 만든다. 우리가 만든 모의객체가 그 지점에 옳바르게 있는지 확인할 수 있다:

상태와 상태없음(State and Stateless)
지난 몇년동안 상태없는 코드를 많이 이야기해왔다. 그러나 결국 우리 앱은 상태를 필요로 했다. 상태가 없는 앱은 꽤 요점을 잃어버린다. 반대로 상태를 관리하면 그것이 굉장히 복잡하기 때문에 수많은 버그를 만들어 내기도 한다.

우리는 상태로부터 코드를 떼어내어 작업하기 쉽게 만들었다. 몇몇 클래스는 상태를 가지고 있으나 대부분의 클래스에는 상태가 없다. 또한 코드를 테스트하기도 아주 쉬워졌다.

예를들어 우리가 EventSync라는 클래스가 있는데, 이 클래스의 역할은 로컬의 변화를 서버에 보내는 것이다. 이것은 어떤 오브젝트가 서버에 갱신을 보내야하는지 현재 서버에 보내진 갱신들은 무엇인지 기억하고 있어야한다. 한번에 여러 갱신을 보낼 수 있지만 같은 갱신을 두번 보내서는 안된다.

또한 우리가 주시해야하는 오브젝트들 사이는 상호의존적이다. 만약 A가 B에 연관되있고 B에서 로컬 갱신이 일어나면, A 갱신을 보내기 전에 B 갱신을 먼저 서버에 보낼때까지 기다려 주어야 한다.

우리는 다음 요청을 만드는 -nextRequest 메소드를 가진 UserSyncStrategy를 가지고 있다. 이 요청은 로컬에서의 갱신을 서버로 보낼 것이다. 이 클래스 안에는 상태가 없으나 그 모든 상태는 UpstreamObjectSync 클래스 안에 캡슐화되어 들어 있는데, 이 클래스는 유저가 만든 모든 로컬 갱신에 대한 기록을 서버에 날린다. 이 클래스 바깥에는 상태가 없다.

이 경우 이 클래스가 관리하는 상태가 올바른지 체크한다. UserSyncStrategy의 경우 UserSyncStrategy를 모의객체로 만들어 UserSyncStrategy 내부의 상태에 더이상 신경 쓰지 않아도 된다. 이것이 테스트의 복잡도를 확 낮춰주는데, 수많은 다른 종류의 오브젝트를 동기화하고 있기 때문이다. 그러면 다른 클래스들은 상태가 없으며, UpstreamObjectSync 클래스를 재사용 할 수 있다.

Core Data
우리 코드는 굉장히 Core Data에 의존한다. 우리 테스트가 다른 하나로부터 독립되야하므로 각 테스트 케이스마다 명확한 Core Data 스택을 만들어야하고 그 후에 그것을 다시 원래대로 해야했다. 우리는 이 store를 한 테스트 케이스에만 사용하고 다음 테스트에는 다시 사용하면 안되었다.

우리 모든 코드는 다음 두가지 Managed Object Context 주변에 집중되있다: 하나는 유저 인터페이스가 사용하고 메인 큐에 묶여있는 것이고 다른 하나는 동기화를 위해 사용되며 자신의 개인 큐를 가지고 있다.

우리는 그들이 필요로하는 모든 테스트마다 Managed Object Context 자꾸자꾸 생성하길 원하지 않는다. 그러므로 공유된 TestCase 수퍼클래스의 -setUp 메소드에 두개의 Managed Object Context를 만들어둔다. 이것은 각 개별 테스트에서 가독성을 높혀준다.

Managed Object Context가 필요한 테스트는 간단하게 self.managedObjectContextself.syncManagedObjectContext를 호출하면 된다:

우리는 코드의 일관성을 만들기 위해 NSMainQueueConcurrencyTypeNSPrivateQueueConcurrencyType을 사용하고 있다. 그러나 독립적 문제 때문에 -performBlock: 상단에 우리만의 -performGroupedBlock:을 구현했다. 이것에 대핸 더 많은 자료는 비동기 코드를 테스팅하는 섹션에서 볼 수 있다. 

여러 컨텍스트를 합치기
우리 코드에는 두 컨텍스트를 가지고 있다. 프로덕션에는 -mergeChangesFromContextDidSaveNotification:의 의미로서 한 컨텍스트가 다른 컨텍스트와 합쳐지는 것에 굉장히 의존적이다. 우리는 동시에 각 컨텍스트 별로 독립된 퍼시스턴스 store coordinator를 사용하고 있다. 그러면 두 컨텍스트 모두 최소의 명령으로 한 SQLite store에 접근할 수 있기 때문이다.

그러나 테스트를 위해 약간 바꾸어서 메모리 store를 사용할 것이다.

테스트시 SQLite store를 사용하여 디스크에 두는 것은 디스크 store에서 삭제시 경쟁상태(race condition)를 만들기 때문에 동작하지 않는다. 이것은 테스트간의 독립성을 해칠 것이다. 반면 메모리에 store하면 매우 빠르게 동작하며 테스트하기도 좋다.

우리는 모든 NSManagedObjectContext 객체를 만들기 위해 팩토리 메소드를 사용한다. 기본 테스트 클래스는 이 팩토리 클래스를 약간 고쳐 모든 컨텍스트가 같은 NSPersistentStoreCoordinator를 공유한다. 각 테스트의 마지막에는 다음 테스트가 사용할 새 것이나 새 store가 있는지 확인하기 위해 공유하고 있던 퍼시스턴트 store coordinator를 버린다.

비동기적 코드를 테스트하기
비동기적인 코드는 조금 까다로울 수 있다. 그러나 대부분 테스트 프레임워크는 비동기적 코드를 위한 기본 기능을 지원한다.

NSString에 비동기적인 메시지를 가지고 있다고 해보자:

XCTest에서는 아래와 같이 테스트할 수 있다:
대부분 테스트 프레임워크가 이런식으로 되었다.

그러나 비동기 테스트의 주된 문제는 독립적으로 테스트 하기 힘들다는 것이다. 테스트 습관에 관한 글에서 말했듯, 독립(Isolation)의 첫 글자는 "I" 이다.(원문: Isolation is the “I“ in FIRST, as mentioned by the article about testing practices.)

비동기 코드에서 다음 테스트가 시작하기 전에, 현재 테스트의 모든 스레드와 큐가 완전히 멈추는 것을 확신하기 까다로울 수 있다.

이 문제에 대해 우리가 찾는 최고의 해결책은 dispatch_group_t라는 이름의 그룹을 사용하는 것이다.

혼자 두지 말고 그룹에 넣자
몇 우리 클래스들은 내부적으로 dispatch_queue_t를 사용할 필요가 있다. 몇 우리 클래스들은 NSManagedObjectContext의 private 큐에 블럭들을 넣는다.

모든 비동기 작업이 끝날때까지 -tearDown 메소드에서 기다린다. 이것을 하기위해 우리는 아래 보이는 것처럼 여러 일들을 한다.

테스트 클래스는 이런 프로퍼티를 가진다:

우리는 이것을 일반적인 수퍼클래스에 한번만 선언해 두었다.

다음으로 dispatch_queue나 그 비슷한 것을 사용하는 모든 클래스 안에 이 그룹을 넣었다. 예를들어 dispatch_async()를 호출하는 대신, dispatch_group_async()를 호출하였다.

우리 코드는 CoreData에 의존적이므로 NSManagedObjectContext에 호출하는 메소드도 추가하고
모든 Managed Object Context에 새 dispatchGroup 프로퍼티를 추가했다. 그래서 우리는 독립적으로 -performGroupedBlock:을 사용했다.

이렇게하여 모든 비동기 처리가 끝날때까지 tearDown 메소드에서 기다릴 수 있었다.

메인 루프에서 -tearDown이 호출된다. 메인루프에서 큐에 들어간 어떤 코드가 실행되었는지 확인하기 위해 메인루프를 끈다. 위 코드는 그룹이 비지 않는한 영원히 돌고 있다. 우리의 경우 타임아웃을 넣어 살짝 바꾸었다.

모든 작업이 끝날때까지 기다리기
이렇게 하면 수많은 다른 테스트들도 쉬워진다. 아래와 같이 사용할 WaitForAllGroupsToBeEmpty()를 만들었다:

마지막 라인은 모든 비동기 작업이 완료될때까지 기다리는 코드이다. 즉, 이 테스트는 추가적인 비동기 처리를 큐에 넣은 비동기 블럭들까지도 모두 끝나고 어떠한 것도 거절된 메소드를 호출하지 않는다.

이것을 만든한 메크로로 만들었고:
나중에는 공유된 TestCase 수퍼클래스에 메소드를 정의했다:

커스텀 예외
이 섹션의 초반부에서, 어떻게 이것을 하는지 이야기 했었고

비동기 테스트를 위해나 블럭을 만드는 기본이다.

XCTest는 NSNotification과 key-value observing을 위한 몇가지 약속이 존재하는데, 이 둘다 블럭을 만드는 최상단에서 구현될 것이다.

그러나 종종 여러 곳에서 이 패턴을 사용하고 있다는 것을 발견하였다. 예를들어 Managed Object Context가 비동기적으로 저장될거라 예상할 때, 우리 코드는 이렇게 생길 것이다:

이 코드를 공유된 한 메소드만을 호출하게하여 가볍게 만들었다:

그리고 테스트에서 사용할 때이다:

이렇게하면 가독성이 더 좋아진다. 이 패턴은 사용하면 다른 상황에서도 자신만의 커스텀 메소드를 추가할 수 있다.

The Ol’ Switcheroo — Faking the Transport Layer
앱을 테스트하는데 중요한 줄문 중 하나는, 어떻게 서버와 연동하여 테스트할 것인지 이다. 가장 이상적인 솔루션은 실서버를 로컬에 빨리 복사하고, 가짜 데이터를 제공하여 http를 통해 직접 테스트를 돌려보는 것이다.

사실 우리는 이러한 솔루션을 이미 사용하고 있다. 이 솔루션은 굉장히 실제와 유사한 테스트 환경을 제공한다. 그러나 현실적으로 너무 느리게 환경설정이 된다. 각 테스트마다 서버의 DB를 정리하는 것이 너무 느리다. 우리는 1000여개의 테스트를 가지고 있다. 실서버에 의존하는 30개의 테스트가 있는데, 만약 DB를 정리하고 서버 인스턴스를 깨끗히 만드는데 5초가 걸린다치면 적어도 2분 30초를 테스트를위해 기다려야 한다는 것이다. 그리고 또한 서버 API가 구현되기 전에 서버 API를 테스트할 수 있는 것도 필요했다. 우리는 뭔가 다른 것이 필요했다.

이 대안의 솔루션은 '가짜 서버(fake server)'이다. 우리는 서버와 통신하는 모든 클래스를 TransprotSession이라는 한 클래스와 통신하도록 구조를 짜고, 이 클래스는 NSURLSession과 비슷한 스타일이지만 JSON 변환까지도 처리해준다.

우리는 UI에 제공할 API 테스트들을 가지고, 서버와 통신하는 모든 것들은 TransportSession이라는 가짜 서버로 우회하여 두었다. 이 transport session은 실제 TransportSession과 서버 모두의 행동을 따라한다. 이 가짜 session은 TransportSession의 모든 프로토콜을 구현하여 그것의상태를 설정할 수 있게 해주는 몇 메소드를 추가한다.

OCMock를 사용하여 각 테스트에 커스텀 클래스를 가지는 것은 모의 서버(mocking the server)를 넘어 여러 이점을 가진다. 그중 하나는, 실질적으로 모의 서버를 사용하여 더 복잡한 시나리오를 만들어 테스트해볼 수 있다. 실제 서버에서는 시도해보기 어려운 극한의 상황을 시뮬레이트 해볼 수 있다.

또한 가짜 서버는 그 스스로 테스트를 가지므로 그 결과가 좀 더 정밀하게 정의되어 있다. 만약 요청에 대한 서버의 응답이 항상 바뀌어야 한다면 한 장소에서 오직 그렇게 한다. 이것은 가짜 서버를 사용하는 모든 테스트를 보다 더 튼튼하게 만들며, 우리 코드에서 새 기능이 잘 동작하지 않는 부분을 좀 더 쉽게 찾아낼 수 있다.

FakeTransportSession 구현은 간단하다. HTTPRequest 객체를 요청에 관한 URL, 메소드, 패이로드(payload)에 관련하여 캡슐화 시키면 된다. FakeTransportSession은 내부 메소드에 모든 끝부분을 매핑시키고 응답을 발생시킨다. 이것이 알고있는 오브젝트의 기록을 가지고 있기 위해 메모리에 담은 CoreData 스택까지도 가지고 있는다. 이렇게하여 PUT으로 추가된 이전 오퍼레이션의 리소스를 GET으로 반환할 수 있다.

이 모든것을 하기에 시간이 부족하다고 생각할 수도 있겠지만 사실 가짜 서버는 꽤 간단하다. 실제 서버가 아니며, 많은 부분을 떼어냈다. 가짜 서버는 오직 한 클라이언트에만 기능을 제공하기 때문에 퍼포먼스나 스케일리비티는 전혀 신경쓰지 않는다. 또한 한번에 큰 노력을 들여 모든것을 구현할 필요가 없다. 우리가 개발이나 테스트에 필요한 부분만 만들면 된다.

그러나 우리 상황의 경우, 우리 테스트를 시작할쯤엔 서버 API가 꽤 안정적이고 잘 정의되있었다.

커스텀 Assert 메크로
Xcode 테스트 프레임워크에선, 실제 확인을 위해 XCTAssert 메크로를 사용한다:
애플의 “Writing Test Classes and Methods” 글에 "카테고리로 정의된 Assertion의 모든 목록이 있다.

그러나 우리는 아래와 같은 특정 도메인을 체크하는 Assertion을 자주 사용했다:
이렇게하면 가독성이 너무 떨어지고, 코드의 중복을 피하기 위해 간단한 assert 메크로를 만들었다:

테스트 할때는 아래와같이 간단하게 사용하면 된다:

이 방법으로 테스트의 가독성이 굉장히 좋아졌다.

한단계 더
그러나 우리 모두가 알듯 C의 전처리기 메크로는 굉장히 난잡하다(a beast to dance with).

몇몇은 이것을 피할 수 없으며 그 고통을 줄이고싶은 것에 대한 이야기이다. 어디라인 어디파일에 assertion 실패가 생겼는지 알기 위해 테스트 프레임워크를 정렬하는 경우 메크로가 필요하다.(We need to use macros in this case in order for the test framework to know on which line and in which file the assertion failed.) XCTFail()은 메크로이고 __FILE____LINE__이 설정되는 것에 의존하고 있다.

좀 더 복잡한 assert와 체크를 위해 FailureRecorder라 불리는 간단한 클래스를 만들었다:

우리 코드에는 두 딕셔너리가 서로 일치하는지 확인해야하는 부분이 곳곳에 있는데, XCTAssertEqualObject()가 그것을 체크한다. 이것이 실패했을때 내뱉는 결과가 아주 유용하다.

우리는 이런식으로 하길 원했다:

결과에는

그래서 이렇게 메소드를 만들었다.

FailureRecord가  __FILE__, __LINE__, 테스트 케이스를 잡아내는 방법을 썼다. -recordFailure: 메소드는 그냥 간단하게 문자열을 테스트 케이스로 전달한다:

Xcode, Xcode 서버와 통합
XCTest의 최고 장점은 놀라울 정도로 Xcode IDE와 통합하기 좋다는 것이다. Xcode6과 Xcode6 서버와 함게 작업하면 더욱 빛을 발한다. 이 강력한 결합력은 생산성을 증진시키는데에도 큰 몫을 한다.

초점
테스트 클래스에서 한 테스트나 여러 테스트를 하고 있을 동안, 왼편 라인 넘버 옆에 있는 작은 다이아몬드는 특정 테스트나 테스트 집합을 실행시켜준다.



테스트에 실패하면 빨간색으로 되고:



성공하면 초록색이 된다:




^⌥⌘G 단축키는 마지막 테스트를 다시 돌려볼 수 있게 해주는데, 자주 사용하게 될 것이다. 다이아몬드를 클릭하고, 우리가 테스트를 변경하면, 키보드에 손 델 필요없이 간편하게 다시 테스트를 돌려볼 수 있다. 디버깅 테스트시 아주 유용하다.

네비게이터
(Xcode 왼쪽 창에 있는) 네비게이터는 Test Navigator라는 것인데, 클래스별로 모든 테스트를 묶어 보여준다:



그룹 테스트나 개별 테스트는 이 UI로부터 시작할 수도 있다. 더 유용한 점은 네비게이터 하단의 세번째 아이콘을 활성화시켜 실패한 테스트만 보여주게도 할 수 있다:




이어지는 통합
OSX 서버는 Xcode 서버라 불리기도 한다. 이것은 Xcode를 기반으로 이어지는 통합(continuous integration) 서버이다. 우리는 이렇게 사용해 왔다.

우리의 Xcode 서버는 github의 새 커밋이 들어올때 자동적으로 프로젝트를 체크한다. 우리는 스태틱 어널라이저를 실행하고 iPod touch나 다른 iOS 시뮬레이터에서 모든 테스트를 돌린 뒤 마지막으로 다운받을 수 있는 Xcode 아키브(archive)를 생성하도록 했다.

Xcode6에서는 Xcode 서버의 이 기능들이 복잡한 프로젝트에까지도 꽤 유용하게 쓰인다. 우리는 커스텀 트리거를 가지고 있는데, 이것은 배포 브런치에서 Xcode 서버의 빌드 부분을 실행한다. 이 트리거 스크립트는 생성된 Xcode 아키브를 파일 서버에다 올려둔다. 이렇게 함으로서 버전별 아키브를 관리할 수 있다. UI팀은 파일 서버로부터 미리 컴파일된 특정 버전의  프레임워크를 내려받을 수 있다.


BDD와 XCTest
당신이 만약 BDD(behavior-driven development)에 익숙하다면, 우리의 네이밍 스타일이 이 방식(BDD)에 영감을 받았다는 것을 알 수 있을 것이다. 우리 중 몇명은 Kiwi라는 테스트 라이브러리를 사용해 보았고 자연스럽게 클래스나 메소드의 동작에 집중함을 느꼈을 것이다. 그러면 XCTest가 그 좋은 BDD 라이브러리를 대체할 수 있을까? 대답은 아니오 이다.

XCTest가 간편하다는 것에는 장단점이 분명 존재한다. 당신이 클래스를 생성하고 "test"라는 단어를 접두에 붙인 테스트 메소드를 만들어서 그렇게 해도 된다. 게다가 Xcode와 XCTest는 최고의 통합을 자랑한다. 한 테스트를 실행하기 위해 왼편의 다이아몬드를 누르면 되고, 실패한 테스트들을 ㅅ ㅟㅂ게 걸러볼 수 있으며, 또한 테스트의 리스트 중에 원하는 테스트로 쉽게 이동할 수도 있다.

불행히도 당신에게 이런것들을 새로 배우기에 꽤 부담스러울 수 있는 양이다. 우리는 XCTest와 개발/테스트하면서 어떠한 장애물도 만나지 않았으나 종종 더 편하게 사용해왔다. XCTest 클래스는 일반 클래스처럼 보이지만, BDD 테스트 구조는 nested context가 있다. 이것은 테스트시 nested context를 만드는 것을 잊어버릴 수도 있다. nested context는 개별 테스트를 간단하게 하면 더 많은 특정 시나리오를 만들어내야한다. 물론 XCTest에서도 그렇긴하다. 예를들어 몇 테스트를 위해 커스텀된 초기화 메소드를 호출함우로써 말이다. 이것의 단지 편리함 때문만은 아니다.

BDD 프레임워크의 추가적인 기능이 얼마나 중요한지는 프로젝트의 크기에 따라 알게될 것이다. 우리의 결로은 다음과 같다. XCTest는 작은 사이즈나 중간 사이즈의 프로젝트에 적합하나, 큰 사이즈의 프로젝트에는 KiwiSpecta 같은 BDD 프레임워크를 사용하는 것이 더 낫다.

요약

XCTest가 옳바른 선택일까? 당신의 프로젝트에 따라 판단해야한다. 우리는 KISS의 부분으로 XCTest를 선택했고—다르게 해보고 싶었던 위시리스트를 가진다. XCTest는 우리가 어느정도 절충해야 하지만, 그 역할을 잘 한다. 그러나 다른 프레임워크에서는 다른 것들도 절충해야할 것이다. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret