제목: 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

,


유닛 테스트에서 가장 힘든 시점은 시작 시점이다. 그 이유는 모든 아키텍처가 유닛 테스트 되지 않기 때문이다. 만약 유닛 테스트를 하고자 한다면(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

,




당신이 TDD를 사용하든 하지않든, 코드를 검증하기 위한 테스트를 하면 많은 편리함을 느낄 것이다. 새 기능을 추가하거나 리팩토링을 할 때에 코드베이스의 일부분을 수정할 필요 없이 그것을 가능하게 해준다.

COBE에서 이전까지는 겉모습만 유닛테스트인 것을 만들다가 최근에 실제 유닛 테스트를 만들기 시작했다. 그리고 우리의 경험을 두 글을 통해 적어보기로 했다. 첫번째 글은 유닛 테스트를 왜 하고, 어떤 유닛 테스트를 할지에 대해 다룰것이고, 두번째 글은 좀 더 실질적으로 들어가서 우리 코드베이스를 어떻게 테스트하기 쉽게 짤 수 있는지, 실제 테스트는 어떻게 작성하는지에대해 이야기 해볼 것이다.

왜 테스트할까?
두려움은 성공의 적이다.
여러분의 코드베이스 속에는 한마리의 거대한 괴수처럼 생긴 클래스를 가지고 있을 것이라고 조심스럽게 예상해 본다. 그 클래스는 리팩토링이 필요하지만 모두가 그 코드를 건드리기 무서워 할 것이다.

유닛 테스트는 이런 클래스를 리팩토링하기 쉽게 해준다. 심지어 새 기능이 추가되어도 기존 코드베이스를 고치든 그렇지 않든 상관없이 원래 있던 테스트는 잘 돌아간다.

클래스의 동작을 확인하기 위해 테스트를 한번 만들면, 그 안에 있는 코드를 고쳐볼 수 있고, 그리고 코드를 분해하지 않아도 즉각적이고 꽤 정확하게 알 수 있다. 그러면 두려움은 사라지고 그 클래스를 당신이 원하는대로 리팩토링 할 수 있게 될 것이다.

두려움은 독창력을 압도하고 결과적으로 품질을 압도한다. 우리가 한번 작성한 코드는 항상 두려움이 없어야한다.


능동적 vs 수동적 기록
코드를 기록하는 데에는 두가지 타입의 문서화가 있다고 생각한다: 능동적 기록과 수동적 기록

먼저 수동적 기록은 주석이나 다른 외부 문서들을 말한다. 이때 다른 외부 문서간 당신을 포함한 팀원들이 당신의 코드를 위해 작성한 것이다. 수동적 기록의 단점은 다른이가 최신버전이 어떤지 모른다는 점이고, 누군가 강제로 읽게 할 수 없다.(호의를 배풀어가며 누군가에게 읽도록 강요해 볼 순 있다.)

능동적 기록은 개발을 진행하는 동안 가시적인 방법으로 문서를 보여주는 것이다. 이렇게하면 여러분의 메소드에 옳바르게 접근하는지 컨트롤할 수 있고 또 디프리케이트(deprecated)된 메소드라고 표시도 할 수 있는 등 다양하게 가능하다.

앱이 어떻게 동작 하는지 알려주고 만약 동작하지 않으면 그것 또한 모두에게 알려주기 때문에 나는 테스트가 능동적 기록에 속한다고 생각한다. 이러한 특징은 특히 많은 사람들과 복잡한 코드베이스에서 작업할때 유용하게 쓰인다.

테스트 작성하기
코더처럼 작성하지 말고 사용자처럼 작성하기
테스트는 구현한 것을 테스트하려 하는 것이 아니라 당신이 필요한 동작을 테스트해야한다. 나는 이 개념을 Orta Therox의 e북인 iOS testing에서 처음 접했다. 여기서 말하길, 메소드(혹은 클래스)들을 블랙박스라 생각하고 접근해야 한다고 한다. 당신이 아는 것이라고는 메소드나 클래스에 넣는 인풋과 결과로 나오는 아웃풋 뿐이다.

즉, 각 메소드와 클래스의 끝나는 지점이 어떨지 생각해보고 그것만 테스트한다. 절대 그 구현 사이에 것들을 테스트 하지 마라.

이렇게 하면 자유롭게 리팩토링 할 수 있다. 메소드 이름을 변경하지 않은채, 테스트하고 있던 클래스/구조체의 모든 구현이 바뀌어도 당신의 테스트는 돌아갈 것이다.

실질적으로 당신은 끝나는 결과를 테스트한다. 각 클래스를 작은 전자기적 요소로 비유하여 생각해보자. 한쪽끝은 -, 다른쪽 끝은 +를 띌것이다. 그리고 이제 관찰을 해보는데, 테스트는 이 두 끝점을 관찰하는 것이지 이 두 사이를 관찰하는 것이 아니다.

언제 테스트를 하고 어떤것을 테스트 할까
이상적으로는 모든 것을 테스트하고 모든 시점에서 테스트하는 것이다. 그러나 현실세계에선 그것이 불가능하다. 여러분들의 앱 레이어들은 생각해보자면 테스트는 이 레이어를 항상 커버 하도록 해야한다. 몇 레이어는 너무 고수준이고, 몇은 또 너무 저수준일 수도 있지만 말이다.

나는 외부 프레임워크를 유닛 테스트 할 수 없다고 생각한다. 대부분의 앱이 외부 프레임워크와 함께 작업할 수 밖에 없을 것이다. 이 말은 여러분의 앱이 데이터베이스부터 UIKit까지 외부 프레임워크를 항상 끼고 개발한다.

여기서 문제는 외부 프레임워크에 다이렉트로 호출하는 클래스들은 유닛테스트하기 힘들다는 점이다. 이것이 외부 프레임워크에 바로 호출하는 클래스를 가능한 가볍게 만들어야하는 이유이기도하며, 나중에 Part2에서 테스트가능한 아키텍처에 관해 좀 더 살펴볼 것이다.

외부 프레임워크 사이에 있는 것들은 모두 유닛테스트 해야한다. 그러나 때론 시간이 그렇게 넉넉하지 않을 수 있다. 이럴때 나는 아래와 같은 기준으로 테스트를 한다.

1) 더 복잡한 클래스일수록, 더 많이 테스트한다.
크고 복잡한 클래스는 버그가 넓게 퍼져 있을 가능성이 많으며, 그런 것들을 쪼개어서 관리하기 쉽게 만들어 주어야 한다. 그래서 이것을 우선순위로 테스트한다. 그 무서움을 빨리 제거하기 위함이다.

2) 이상한 버그를 발견하면 테스트한다.
이렇게 해놓으면 누군가 당신의 코드를 싹 뜯어 고칠 필요 없이 디버깅하는데 도움을 줄 것이다.

3) 클래스를 리팩토링할 때 누군가에게 당신이 고려하고 있는 것을 주석으로 남기기 보다는 테스트를 만든다.
처음 딱 보았을때 주석보다는 Xcode의 빨간 다이아몬드 표시(Xcode에서 테스트에 실패했다는 표시)가 더 눈에 들어온다. 또한 미래에 소스를 만지게될 개발자가 무언가 고칠때 단지 테스트를 돌려 보면서 고칠 수 있으므로 매우 유용할 것이다.

4) 개발자와 더 적게 마주치는 클래스는 더 많이 테스트해야한다.
UI 테스트는 시간을 절역하기에 유용하지만서도 사실 UI 버그는 꽤 쉽게 해결된다. 왜냐하면 개발자가 그 버그는 반복적으로 눈에 들어오기 때문이다. 그러나 다른 부분에서 유저는 겪었으나 당신이나 테스터는 겪지 못한 아주 작은 버그를 가지고 있을 수 있다.

5) 무언가를 리팩토링 하기 전에 테스트를 작성한다.
일단 믿어보아라. 이 방법은 삶을 순탄하게 해주고 시간을 절약하게 해줄 것이다.

기본적인 규칙은 가장 작게 테스트하고싶은 클래스를 테스트하는 것이다. 가장 유용한 테스트는 종종 가장 작성하기 어려운 테스트이다.

유닛 테스트 종류들
수학적 테스트
순수 함수를 생각해볼때 나는 종종 수학적인 함수를 떠올린다. 즉 A집합으로부터 각 요소를 가져다가 B집합에 있는 요소에 정확히 할당한다.

이것이 파라미터와 함께 호출하거나 예상된 결과를 비교함으로서 테스트 할 수 있는 함수이다. 이런 함수가 유닛 테스트를 만들기 가장 이상적인 함수이다. 안타깝게도 이런 함수는 작은 유틸리티 클래스에만 조금 구현되어 있다.

델리게이션 테스트
이 테스트는 한 클래스가 액션이나 어떤 정보를 다른 클래스에 보내는 것을 검증한다. 예를들어 로그인 버튼을 눌렀을 때 옳바른 파라미터와 함게 네트워크 클래스에 있는 메소드를 호출하는지 확인하고 싶거나, 혹은 어떤 설정 값이 바뀔때 나의 UserSettingManagerOrWhatever가 호출되는지 확인하고 싶을 때 이 테스트를 작성한다는 것을 발견했다.

이 테스트는 당신의 코드를 물려받은 프로그래머에게 굉장히 유용할 것이다. 또한 당신이 각 레이어마다 테스트를 만들어 놓으면 당신의 모든 클래스가 그들이 할 수 있는한 다 호출할테고, 버그는 구현속에 놓여있으며, 코드가 서로 엉겨붙어 있지 않게 해준다.

아웃풋 테스트
때론 함수들이 수학적인 함수가 아닐때도 많다. 때론 인풋 아웃풋이 한가지 이상 종류일 수도 있다. 예를 들어보자. 네트워크를 다루는 대부분의 메소드는 인터넷이 끊기거나, 서버가 터지거나 할때 다른 형태의 아웃풋을 내놓는다.

완료 핸들러를 가지는 대부분의 메소드들이 정확하게 이것을 처리했는지 보기위해, 성공적인 길을 갔는지 그렇지 않은지 반드시 테스트해야한다.

유닛 테스트의 구조
정의에의하면 유닛 테스트는 독립적이다. 유닛 테스트는 당신이 테스트 하고 있는 클래스 내부를 수정했을 때만 실패가 나타날 수 있으며, 그 테스트 외부의 클래스는 테스트 결과에 영향을 주어선 안된다. 이런 점이 테스트 중에 문제가 어디있는지 알게 해줌으로서 유닛 테스트의 장점이라 할 수 있다.(비록 무엇이 문제인지는 모를지라도 말이다.)

간단한 수학적 테스트의 경우, 각 순수 함수들이 당신의 코드로부터 완전히 독립적이기 때문에 문제가 없다.(옮긴이: 좀 더 크고 복잡한 수학적 함수라면 말이 다를 것이다)

간단한 수학적 함수간단한 수학적 함수


그러나 함수로부터 사이드 이펙트가 있다면 쉽지 않을 것이다. 만약 사이드 이펙트가 그 클래스 안에서 끝난다면 꽤 간단한 문제일 수도 있다—다시 처음부터 클래스를 설정하고 테스트를 돌려, 바뀐 클래스가 예상한대로 돌아가는지 확인한다.

클래스 내부에서 사이드 이팩트가 있는 메소드클래스 내부에서 사이드 이팩트가 있는 메소드


메소드가 다른 클래스를 호출한다면, 우리는 모의 객체(mock)를 만들어서 테스트할 수 있다. 모의 객체는 당신의 클래스 의존성을 위해 대리 역할을 하며, 테스트에서 만들어놓고 관찰(observe)할 수 있다. 당신은 테스트하는 클래스에 모의 객체를 제공하고 그 객체의 옳바른 메소드가 호출되는지 확인하면 된다.

다른 클래스를 호출하는 메소드다른 클래스를 호출하는 메소드



고려사항
상황별로 잠적으로 생기는 테스트의 수
여러분의 클래스에 불리언(boolean)타입의 프로퍼티가 있다고 생각하자. 자연스럽게 그 프로퍼티의 상태가 true인지 false인지 결과를 테스트 하려 할 것이다. 여기서 프로퍼티가 하나 더 추가되면 이 프로퍼티들이 조합되어 4가지의 결과 나올 수 있게 된다. 또 하나 더 추가되면 8가지나 된다!

당신이 테스트를 작성하든 하지 않든 프로그래밍을 할 때 상태(state)는 우리의 적이다. 확인할 것을 기억해두고 반드시 상태에 의존하는 것만 그렇게 해야한다.

코드 커버리지가 거짓일 수 있다.
우리는 가능한 많은 테스트를 한 코드를 가지고 있을지라고 다른 고려사항이 있다. 때론 테스트 메소드를 한 줄 더 적는것 보단, 새로운 것을 배워 적용하는게 더 나을 수도 있다는 점이다. 아래 설명을 보자.

Xcode는 테스트 할때 불려진 코드 매 라인마다 'coveraged'라는 표시를 등록한다. 어떤 라인은 테스트 되지 않고 지나쳤을 수도 있고, 어떤 라인은 여러번 호출되었을 수 있다. 테스트 할때에는 가능한 이런 모든 상황을 고려해야한다. 데이터가 없을때, 데이터가 많을때, 예외의 데이터일때 어떻게 동작하는지 메소드의 모든 경로를 테스트 해야한다.

커버리지는 당신 코드의 어디가 커버되지 않았는지만 말해주지, 당신은 어느 부분인지 말해주는 그것을 믿어선 안된다. (Coverage can only tell you which parts of your code aren’t covered. You cant trust it to tell you which parts are.)

한정된 상황에서만 테스트해서는 안된다.
어떤 경우 미래의 개발자가 당신이 의도하지 않은 방향으로 클래스를 사용할 수 있다. 이런 경우도 확실히 테스트 해주어야하고 클래스가 옳바른 결과를 내는지 확인해보아야한다.

만약 다중 델리게이트 메소드를 가지고 있다면 당신의 클래스가 옳바른 것을 호출하는지 확인해야한다. 혹은 너무 많은 시간이 걸려 호출되었는지도 확인한다. 또한 로그인 콜에서 빈 패스워드를 입력하게 되었는지와 같은, 당신의 클래스가 잘못된 데이터를 전달하진 않았는지도 확인해야한다. 때론 일어나지 않을 것 같은 것을 검증하는것이 유용할 때도 있다.



여기까지 유팃테스트의 일반적인 개괄이었고, 좀 더 추상화된 기본과 함께 우리가 어떻게 할지에 대한 이야기를 해보았다. Part2에서는 어떻게 유닛 테스트가 가능한 방법으로 우리 코드를 설계할지에대해 더 이야기 해보겠다.



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

,

약 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

,