WWDC 2017 Engineering for Testabillity

Date:     Updated:

Categories:

Tags:

배운 내용

  • Testable App Code
    • Protocols and Parameterization
    • Separating logic and effects
  • Scalable Testing

Testable App Code

Unit Test의 구조

Screen Shot 2022-01-29 at 10 14 27 PM

Characteristics of Testable Code

  • Control over Inputs
  • Visibility into Outputs
  • No hidden state
    • 코드를 바꿀만한 Internal state 를 피한다

Protocols and Parameterization

Screen Shot 2022-01-29 at 8 42 05 PM

이걸 테스트하기 위한 방법

  1. UI Test 단점 1: 실행시간이 길다 단점 2: URL 을 확인할 방법이 없다
  2. Unit Test ✅
func testOpensDocumentURLWhenButtonIsTapped() {
    let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Preview") as! PreviewViewController
    controller.loadViewIfNeeded()
    controller.segmentedControl.selectedSegmentIndex = 1
    controller.document = Document(identifier: "TheID")
    
    controller.openTapped(controller.button)
    ...
}

문제점:

  • 일단 ViewController 안에 있어서 테스트하기가 힘들다.
  • 싱글톤의 메서드를 사용하고 있어서 (UIApplication.shared) canOpenURL() 의 결과를 테스트 코드로 제어할 방법이 없다. 왜냐하면 Global system state에 의존하고 있기 때문에 (이 부분 잘 모르겠다)
    • 이걸 호출하면 테스트 앱이 백그라운드로 가고나서 다시 foreground로 불러올 방법이 없다.

이 문제점을 해결하기위해 싱글톤을 내부적으로 사용하지않고 주입을 받으면 된다.

Screen Shot 2022-01-29 at 9 30 43 PM

그리고 open 메서드는 다음과 같이 생기게 될거다

Screen Shot 2022-01-29 at 9 31 11 PM

단위 테스트 코드에서는 subclassing, 대신 protocol 사용하는게 좋다

Screen Shot 2022-01-29 at 9 33 10 PM

그래서 protocol 을 코드에 적용시켜보면 Screen Shot 2022-01-29 at 9 35 27 PM

Screen Shot 2022-01-29 at 9 31 11 PM

이런식으로 생길거다

그러가나서 테스트를 위해 Mock 를 사용하는게 좋다. 왜냐하면 UIApplication 을 테스트에서 제어할 수가 없다. 그래서 프로토콜에대한 Mock 을 다음과 같이 만들 수 있다.

class MockURLOpener: URLOpening {
    var canOpen = false // input 역할..테스트에서 설정할 수 있게
    var openedURL: URL? // 메서드로 받은 url 저장
    
    func canOpenURL(_ url: URL) -> Bool { // 
        return canOpen
    }
    
    func open( _url: URL, options: [String: Any], completionHandler: ((Bool) -> Void)?) { //
        openedURL = url
    }
}

테스트 코드는

func testDocumentOpenerWhenItCanOpen() {
    let urlOpener = MockURLOpener() 
    urlOpener.canOpen = true 
    let documentOpener = DocumentOpener(urlOpener: urlOpener)
    
    documentOpener.open(Document(identifier: "TheID"), mode: .edit)
    
    XCTAssertEqual(urlOpener.openedURL, URL(string: "alsdkjflkasdjflkjasdlkfj"))
}

배운것

  • Reduce References to shared instances
  • Accept parameterized input
    • 의존성 주입
    • 대체성을 위해
  • Introduce a protocol
  • created a test implementation
    • input 에 대한 제어 제공
    • output에 대한 시야 확보

Seperating Logic and Effects

이것도 testability를 향상 시킬 수 있는 기법 예시

on-disk cache class cache된 asset을 불러오는 코드

Screen Shot 2022-01-29 at 10 05 47 PM

Screen Shot 2022-01-29 at 10 07 53 PM

테스트하기 위해서 inputoutput 를 먼저 생각해볼 수 있는데

Screen Shot 2022-01-29 at 10 15 24 PM

input 1. cache가 얼만큼 커질 수 있는지에 대한 parameter input 2. 현재 cache에 저장되어있는 아이템들

ouput: 없다. 즉, 데이터가 안나온다 - 대신, 파일들이 없어지는 side effect가 발생한다.

그런데 해당 방법은 FileManager 에 의존하고 있다는 특징이 있다.

이걸 테스트에서 다뤄야할텐데, 쉽지않다. 전에 배운 protocol, parameterization을 사용해서 테스트에서는, 삭제된 파일의 리스트를 리턴 받아서 validate하는 형식으로도 할 수 있을텐데 그렇게되면, 실제 코드와의 상호작용이 필요하다. 즉, 테스트코드와 실제코드가 독립된 환경이 아니다.

그렇다면 프로토콜 코드를 살펴보면 다음과 같다

Screen Shot 2022-01-29 at 10 18 23 PM

Screen Shot 2022-01-29 at 10 20 22 PM

구현부를 보면, side effect는 없어지고, functional 하게 input과 output이 생겼다.

테스트 코드는 다음과 같다

Screen Shot 2022-01-29 at 10 49 05 PM

  • input에 대한 제어 + output에 대한 시야 + hidden state가 없다
  • FileManager 에 의존을 안해도 돼서 빠르다

실제 코드도 이렇게 바뀐다 Screen Shot 2022-01-29 at 10 54 32 PM

훨씬 직관적이고 테스트성이 좋아졌다

배운것

  • extract algorithms
    • business logic & algorithms를 각각 다른 타입으로 분리
  • functional style with value types
  • thin layer on top to execute effects
    • 남아있는 코드

Scalable Testing

  • faster
  • readable
  • modularized

주제

  • Balance between UI and Unit Tests
  • Code to help UI tests scale
  • Quality of test code

Balance between UI and Unit Tests

Screen Shot 2022-01-29 at 10 59 42 PM

아래로 갈 수록 Distribution은 커지고, Maintenance Cost 는 낮아진다

왜냐하면 UI Test 보다 Unit Test 가 훨신 빨라서 많아도 되고 테스트에서 뭐가 잘못되면 UI Test 보다 Unit Test에서 더 캐치하기 쉬워서 Maintenance Cost 도 더 낮다고한다. 반면에 UI Test 는 어디서 뭐가 틀렸는지 캐치하기 더 어렵다

Screen Shot 2022-01-29 at 11 03 50 PM

추가로, Unit Test는 모든 소스코드에 접근이 가능한 반면에 UI Test는 아니다.

Writing Code to help UI tests scale

Screen Shot 2022-01-29 at 11 05 58 PM

Abstracting UI element queries

  • Store parts of queries in variable
  • wrap complex queries in utility methods
  • reduces noise and clutter in UI test

Screen Shot 2022-01-29 at 11 16 31 PM

이렇게 하는것보다

Screen Shot 2022-01-29 at 11 17 08 PM

이렇게하면 추후에 버튼을 추가할때 그냥 배열에만 넣어주면 된다.!

Creating Objects and utility functions

Screen Shot 2022-01-29 at 11 21 36 PM

이건 Scalable 하지 않은 코드다

Screen Shot 2022-01-29 at 11 23 48 PM 이렇게 바꿔도 되고

Screen Shot 2022-01-29 at 11 24 31 PM

enum 을 사용하면 컴파일 되기도 전에 파라미터로 들어오는 인자를 확인할 수 있다

Screen Shot 2022-01-29 at 11 26 04 PM

그럼 이렇게 코드가 진화한다

Screen Shot 2022-01-29 at 11 26 41 PM

이 부분도 다음과 같이 보완할 수 있다

Screen Shot 2022-01-29 at 11 28 42 PM

Screen Shot 2022-01-29 at 11 29 10 PM

Screen Shot 2022-01-29 at 11 30 11 PM

Screen Shot 2022-01-29 at 11 31 42 PM

XCTContent.runActivity 활용하면 테스트 log 를 깔끔하게 할 수 있다

Screen Shot 2022-01-29 at 11 35 23 PM

테스트 코드도 실제 코드처럼 신경써줘야 함께 확장이 가능하다!

wwdc 카테고리 내 다른 글 보러가기