WWDC 영상이 내려갔지만, 내용이 꽤 좋아보여서 여러 레퍼런스 참고해서 정리하였습니다
Engineering For Testability
- Testable app code
- Testable한 코드가 무엇인지 알고, App 코드를 Testable하게 개선하는 방법
- Scalable test code
- 앱의 크기나 복잡성이 커져도 대응 가능한 확장성 있는 Test 코드를 작성하는 방법
1. Testable App Code
Unit Test의 구조
- AAA 패턴 : (Arrange - Act - Assert)
- input : Arrange
- test : Act
- output : Assert
Testable Code의 특징
- Control over inputs
- Visibility into outputs
- No hidden state
- (코드 동작에 영향을 줄 수 있는) 내부 상태에 의존하지 말아야 한다
Testability Techniques
1. Protocols and Parameterization
Sample View
- segment control (View / Edit) + button (open)
Testable하지 않은 코드
- 버튼 클릭 시 동작하는 메서드
- URL 구성하는 비즈니스 로직
- UIApplication 을 통해 URL에 해당하는 앱 전환
- Unit Test 실행
- (Arrange - Act - Assert) 에서 Assertion을 해야 하는데,
앱을 전환하기 위해 생성된 URL을 검사할 방법이 없다.
XCTAssertTrue(예상 URL == Output URL)
을 해야 하는데, url 자체를 꺼내올 수 없는 상황
코드의 문제점
- 비즈니스 로직(메서드)이 VC에 있다
- 뷰의 입력 상태(
segmentControl.selectedsegmentIndex
)를 직접 가져온다
- 테스트 시 뷰의 속성값을 지정해서 간접적으로 입력을 제공주어야 함
- UIApplication 인스턴스 사용
- canOpenURL의 반환값에 따라 openTapped의 동작 변화
- open을 통해 앱을 열었을 때 side effect에 대한 Unit Test 작성 불가
- 추가적으로, 앱을 다시 foreground로 가져올 방법이 없다.
Testable한 코드 만들기
- 비즈니스 로직을 VC에서 분리
- DocumentOpener 라는 클래스 생성 후 로직 이동
- 테스트 시 VC 생성할 필요 x
document
와 mode
매개변수를 넣어줄 수 있게 해서 뷰의 상태 가져올 필요 x
- UIApplication 주입
- 생성자에서 UIApplication 인스턴스 주입
- DocumentOpenr에서 직접 호출하지 않도록 함
- testDocumentOpenerWhenItCanOpen (NOT completed)
- Test를 위해
app
인스턴스를 만들어서 넘겨주어야 한다.
- 하지만, 타입인
UIApplication
을 맞춰주어야 하는데,
UIApplication
은 싱글톤이기 때문에 인스턴스를 생성하려고 하면 에러가 발생한다.
- 그렇다고
UIApplication
을 상속하려고 해도, 싱글톤 성격을 강제하기 때문에 예외가 발생할 수 있다.
(And throws an exception to try to make a second instance, even if it's a subclass)
- 필요한 메서드를 가진 프로토콜 생성 후 생성자 타입 변경
- 이미
UIApplication
에 구현된 메서드를 선언하면, 추가적으로 구현할 필요가 없다
- 이미 매개변수의 개수와 타입이 완벽히 일치하는 메서드들이 구현되어 있기 때문
메서드 시그니쳐가 동일하다
- 제어가 불가능한
UIApplication
대신 제어가 가능한 Mock 객체 생성
- 프로토콜을 준수하는 Mock 객체 생성
- canOpenURL은 DocumentOpener로부터의 인풋처럼 행동
- open 은 DocumentOpener의 출력처럼 행동
- Test는 이 메서드를 통과하는 URL에 접근하고자 함. -> 프로퍼티에 저장해두고 나중에 Test가 읽을 수 있도록 함
- testDocumentOpenerWhenItCanOpen (Completed)
- Mock 객체를 DocumentOpener의 생성인자로 넘겨준다.
DocumentOpener
의 open
메서드에 input
- 예상 URL과 mock의 URL 비교 후 검증 (assert)
Summary
- Reduce references to shared instances & Accept parameterized input
- UIApplication 싱글톤 인스턴스에 대한 직접 참조를 없애고, 매개변수화된 Input으로 대체
의존성 주입
- Intoduce a protocol
- 클래스에 직접 의존하기보다는 프로토콜을 이용하여 인터페이스 분리
- Create a testing implementation
- 테스트 시 UIApplication 객체를 mock 객체로 대체
2. Separating Logic and Effects
Sample
특징
- Input 1 : 캐시의 저장 용량.
- 저장 용량은 인자로 넣어줄 수 있기 때문에 제어 가능하다
- Input 2 : 캐시에 저장된 아이템 (FileManager)
- 캐시에 저장된 아이템을 불러오기 위해 FileManager 사용
-> 실제 File System에 대한 의존성을 가지고 있다는 문제 발생
side effect : 메서드가 외부의 속성을 변경하는 것. ex). 메서드 호출 시 전역 변수 변경
- 따라서,
cleanCache
메서드를 통해 실제 File System의 아이템이 clean 되는 게 아니라,
어떤 것을 clean할 지 리턴 값으로 주는 것이 더 낫다!
Protocols and Parameterization 도입
- FileManager에 직접 의존하지 않으면서, 아이템을 받으면 뭘 삭제할지 반환하는 구조로 만들어야 한다.
- 또한, 어떤 캐시 정책으로 아이템을 관리할지도 분리한다
-> 직접 지우는 코드와 뭘 지울지 결정하는 코드 분리 가능
- 캐시에 있는 아이템 받아서 뭘 삭제할지 반환하는 프로토콜
- 삭제할 아이템 반환 메서드 구현
- testMaxSizeCleanupPolicy
- input 정의
- 메서드 호출
- Output 받아
- 예상 값과 일치하는지 확인
- Testable Code의 특징 만족
- 입력 제어 가능
- 출력 검사 가능
- 그 외에 어떤 영향을 주는 hidden state x
- cleanCache
- 프로토콜을 채택한 구조체를 paramter로 받고 해당 캐시 정책을 통해 어떤 아이템 제거할지 리턴으로 받는다.
- 즉, cleanCache에는 실제 데이터에 영향을 주는 side effect만 남게 되었다
Summary
- Extract algorithms
- side effect를 고려하여 비즈니스 로직과 알고리즘 분리
- Functional style with value type
- Input과 Output을 보이기 위해 값 타입을 사용하는 functional style
- 알고리즘이 input과 output을 보일 때, functional style을 취한다.
- Thin layer on top to execute effects
cleanCache
메서드는 side effect를 일으키는 소량의 코드만 존재
(side effect : FileManger를 통해 외부 실제 데이터에 영향 주는 것)
레퍼런스
https://gist.github.com/timd/3acdb6aa75efb40c03bb25cb60709c6d
[Unit Test] Testable한 코드를 위한 2가지 스킬과 예제
[WWDC17] Engineering for Testability 정리 (1) Testable App Code
WWDC2017 Engineering for Testability / Testable App Code - 번역