WWDC 2017 Engineering for Testabillity
Categories: wwdc
배운 내용
- Testable App Code
- Protocols and Parameterization
- Separating logic and effects
- Scalable Testing
Testable App Code
Unit Test의 구조
Characteristics of Testable Code
- Control over Inputs
- Visibility into Outputs
- No hidden state
- 코드를 바꿀만한 Internal state 를 피한다
Protocols and Parameterization
이걸 테스트하기 위한 방법
- UI Test
단점 1: 실행시간이 길다
단점 2:
URL
을 확인할 방법이 없다 - 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로 불러올 방법이 없다.
이 문제점을 해결하기위해 싱글톤을 내부적으로 사용하지않고 주입을 받으면 된다.
그리고 open
메서드는 다음과 같이 생기게 될거다
단위 테스트 코드에서는 subclassing, 대신 protocol 사용하는게 좋다
그래서 protocol
을 코드에 적용시켜보면
이런식으로 생길거다
그러가나서 테스트를 위해 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을 불러오는 코드
테스트하기 위해서 input
과 output
를 먼저 생각해볼 수 있는데
input 1. cache가 얼만큼 커질 수 있는지에 대한 parameter input 2. 현재 cache에 저장되어있는 아이템들
ouput: 없다. 즉, 데이터가 안나온다 - 대신, 파일들이 없어지는 side effect가 발생한다.
그런데 해당 방법은 FileManager
에 의존하고 있다는 특징이 있다.
이걸 테스트에서 다뤄야할텐데, 쉽지않다. 전에 배운 protocol, parameterization을 사용해서 테스트에서는, 삭제된 파일의 리스트를 리턴 받아서 validate하는 형식으로도 할 수 있을텐데 그렇게되면, 실제 코드와의 상호작용이 필요하다. 즉, 테스트코드와 실제코드가 독립된 환경이 아니다.
그렇다면 프로토콜 코드를 살펴보면 다음과 같다
구현부를 보면, side effect는 없어지고, functional 하게 input과 output이 생겼다.
테스트 코드는 다음과 같다
- input에 대한 제어 + output에 대한 시야 + hidden state가 없다
- FileManager 에 의존을 안해도 돼서 빠르다
실제 코드도 이렇게 바뀐다
훨씬 직관적이고 테스트성이 좋아졌다
배운것
- 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
아래로 갈 수록 Distribution은 커지고, Maintenance Cost 는 낮아진다
왜냐하면 UI Test 보다 Unit Test 가 훨신 빨라서 많아도 되고 테스트에서 뭐가 잘못되면 UI Test 보다 Unit Test에서 더 캐치하기 쉬워서 Maintenance Cost 도 더 낮다고한다. 반면에 UI Test 는 어디서 뭐가 틀렸는지 캐치하기 더 어렵다
추가로, Unit Test는 모든 소스코드에 접근이 가능한 반면에 UI Test는 아니다.
Writing Code to help UI tests scale
Abstracting UI element queries
- Store parts of queries in variable
- wrap complex queries in utility methods
- reduces noise and clutter in UI test
이렇게 하는것보다
이렇게하면 추후에 버튼을 추가할때 그냥 배열에만 넣어주면 된다.!
Creating Objects and utility functions
이건 Scalable 하지 않은 코드다
이렇게 바꿔도 되고
enum
을 사용하면 컴파일 되기도 전에 파라미터로 들어오는 인자를 확인할 수 있다
그럼 이렇게 코드가 진화한다
이 부분도 다음과 같이 보완할 수 있다
XCTContent.runActivity
활용하면 테스트 log 를 깔끔하게 할 수 있다
테스트 코드도 실제 코드처럼 신경써줘야 함께 확장이 가능하다!