티스토리 뷰

 

 

테스트는 일반적으로 다음과 같습니다.

요청할 input(입력) 상태나 값을 준비합니다.

코드가 테스트되도록 호출합니다.

ouput(출력)이 맞는지 확인합니다. (asserting)

 

 

 

테스트 가능한 코드는 클라이언트가 작동하는 모든 입력을 제어할 수 있는 방법을 제공합니다.

클라이언트가 생산되는 모든 출력을 검사할 수 있는 방법을 제공합니다.

나중에 코드의 동작에 영향을 미칠 수 있는 내부적인 상태에 의존하는 것을 방지합니다.

 

 

 

첫 번째 기술

프로토콜과 파라미터화하기

 

open 버튼과 segmented control이 가능한 샘플 예제입니다.

 

 

뷰 컨트롤러에서 버튼을 탭하면 호출되는 event입니다.

iOS가 다른 앱으로 switch하도록 하는 URL을 만드는 것부터 busines logic이 시작됩니다.

UIKit에서 제공하는 shared UI appication인스턴스를 사용합니다.

 

테스트 하는 방법에 2가지가 있는데

하나는 UI test입니다.

앱을 실행하고 화면을 띄워서 버튼을 누르고...

근데 이것은 단점이 잇는데 태스크 실행하는데 시간이 오래걸리고, 특히 여러 유형으로 테스트하도록 확장할 경우 더 그렇스빈다.

더 큰 문제는 UI 테스트로는 URL을 검증할 수 없고 iOS가 앱을 switch하도록 요청할 수 없는 것입니다.

URL이 좀 더 정확하게 검사하고 싶은거죠.

 

그래서 unit 테스트가 더 적절해 보입니다.

유닛테스트를 해보겠습니다.

 

먼저 뷰컨트롤러를 인스턴스화 합니다.

스토리보드를 사용하는 경우 위와 같이 하게됩니다.

그리고 컨트롤 프로퍼티를 덧붙이기 위해 뷰를 로드해야합니다. (loadViewIfNeeded)

그리고 segentedControl로 open 모드를 configure(설정)합니다.

Document를 제공해줍니다.

셋업이 끝났습니다.

 

이제 뭘 더 해야할까요?

여기에 어떤 assertion을 적어야할지 난해하네요.

 

왜 테스트가 어려운지 봅시다.

일단. 뷰컨트롤러에 있으니까 메서드 테스트가 어렵죠.

그리고 입력 상태를 세그먼트 컨트롤러 뷰로 바로 전달하죠. (이러면 뷰가 로드되어야하잖아요.)

제일 큰 문제는 UI application shared instance를 사용하는 것입니다.

 

The return value from this call to canOpenURL, is effectively another input for the method.

(canOpenURL의 리턴값이 사실상 또 다른 메서드의 인풋이 됩니다.??)

그러나 이것은 글로벌 시스템 상태에 의존하기 때문에 이 쿼리의 결과를 컨트롤할 테스트를 만들 방법이 없습니다.

 

URL을 열엇을 때 사이드 이펙트를 관찰할 수 있는 방법도 없습니다.

사실 이것이 호출되고 나서, 테스트 render app이 실제로 백그라운드로 보내지기 때문에 다시 포그라운드로 가져올 방법이 없습니다.

 

이 코드의 테스트 가능성을 향상시키기 위한 방법을 봅시다.

일단, 뷰컨트롤러에서 꺼내야겠습니다.

이 로직과 동작을 캡슐화하는 새로운 DocumentOpener class를 소개합니다.

 

 

open mode와 문서 입력은 테스트가 간접적으로 전달할 수 있는 메서드 인자로 제공해야 합니다.

여전히 shared UI application 인스턴스를 해결해야합니다.

 

 

클래스에 이니셜라이저를 추가하여 특정  application 인스턴스가 전달되도록 하겠습니다.

default값을 제공할 수 있기 때문에 뷰컨트롤러 안에서 세부사항을 걱정할 필요 없습니다.

 

 

다시 돌아와서 application을 변경해줍니다.

 

 

테스트 코드를 다시 작성하려고 하면 여전히 해결이 안됩니다.

우리가 제어할 수 잇는 application 인스턴스를 전달하고 싶습니다.

그래서 UI application을 상속하는 것을 생각할 수 있습니다.

canOpenURL이랑 open 메서드를 오버라이딩해서 컨트롤하려구요.

하지만 UI apllication은 싱글톤 성격을 강하게 강제합니다.

 

And throws an exception to try to make a second instance, even if it's a subclass.

(심지어 subclass에도 두번째 인스턴스를 만들기위해 시도하는 예외가 생길 수 있습니다.??)

 

그니까 sub-classing하지말고 프로토콜을 씁시다.

 

위에서 사용한 application 메서드와 동일한 두가지 메서드로 프로토콜을 만듭니다.

우리는 여전히 UI apllication이 이 프로토콜의 중요한 구현이길 원합니다.

 

그래서 URLOpening을 준수하는 익스텐션을 둡니다.

이미 저 메서드가 있으니까 추가적인 코드를 익스텐션에 작성할 필요가 없습니다. (UIApplication에 구현되어있으니까??)

DocumentOpener가 UI application 을 요청하는 대신에 프로토콜을 사용하도록 업데이트 해봅시다.

 

 

 

먼저 프로퍼티를 바꾸고 URLOpening 프로토콜 타입으로 파라미터를 바꿔줍니다.

뷰컨트롤러에서 이것을 사용할 떄 여전히 편리하게 shared인스턴스를 디폴트 인자로 가지고 있습니다.

 

 

URLOpener 프로퍼티 네임으로 바꿔줍니다.

테스트로 돌아가겠습니다.

 

UIApplication이 테스트에 필요한 컨트롤을 제공하지 않기 때문에 프로토콜의 Mock을 구현해서 사용합니다.

여기서 두 가지 메서드 구현을 추가합니다. ( canOpenURL과 open)

canOpenURL 메서드는 DocumentOpener로부터의 인풋처럼 행동합니다.

따라서 테스트는 이 인풋을 컨트롤해야합니다.

우리는 구현이 테스트가 설정할 수 있는 속성 값을 반환하도록 함으로써 그것을 얻을 수 있습니다.

 

open 메서드는 documentOpener의 출력처럼 행동합니다.

테스트는 이 메서드를 통과하는 어떤 URL에든 접근하고 싶어합니다.

테스트가 나중에 읽을 수 있도록 이 URL을 프로퍼티에 저장하여 해낼 수 있습니다.

 

 

이제 다시 테스트 코드를 작성해봅시다.

 

mockURLOpener 인스턴스를 생성하고 canOpen 프로퍼티로 입력 configure합니다.

그리고 documentOpener를 만듭니다.

그리고 그 mockURLOpener를 인자로 넘겨줍니다.

도큐먼트랑 오픈 모드 값을 전달하고 open메서드를 호출할 수 있습니다.

 

openedURL 프로퍼티가 예상한 URL로 세팅된 것을 assert할 수 있습니다.

이 하나의 assertion에 open메서드와 URL이 정확한 data를 보내는 것을 동시에 테스트합니다.

 

 

싱글톤 인스턴스 참조 제거

파라미터화된 인풋 (Dependency Injection 의존성주입)

프로토콜사용

테스트 구현

 

 

 

서버에서 이전에 다운로드한 자산을 빨리 검색하게 해주는 캐시 클래스입니다.

 

삭제 메서드를 보겠습니다.

생성된 순서대로 정렬하고 사이즈가 max보다 커지면 아이템을 삭제합니다.

 

테스트에대해 생각해봅시다.

인풋이뭐고 아웃풋이 뭘까요?

 

인풋의 하나는 maxSize라는 간단한 integer 파라미터고 컨트롤가능합니다.

다른 하나는 캐시에 저장된 아이템입니다.

file manager로 되어있네요.

file system에서 인풋이 나온다는 건데 이것은 이 의존성을 테스트가 다뤄야한다는 거네요.

 

클린 캐시 메서드는 리턴값이 없습니다.

따라서 아웃풋이 data가 될 수 없겠네요.

대신 디스크에서 파일이 삭제되는 어떤 사이드 이펙트가 됩니다.

이 파일시스템에 대한 의존성 때문에 이 메서드에대한 테스트가 파일시스템의 파일매니저를 다룰 수 있어야해요.

 

아까 위에서 했던 프로토콜과 파라미터화 스킬을 사용할 수 있습니다.

 

그렇게 하더라도, 우리는 여전히 코드와 간접적으로 상호작용하게 됩니다.

그 코드는 우리가 테스트하려는 코드인데 파일 매니저에 의해 좌우됩니다.

 

 

더 직접적으로 상호작용할 수 있는CleanupPolicy를 분리합니다.

 

 

아이템 삭제라는 메서드 하나를 가진 CleanupPolicy를 프로토콜로 정의합니다.

 

인풋으로 캐시아이템 set을 받고 아웃풋으로 다른 아이템 set을 방출합니다.

 

 

 

maxSize 프로퍼티를 만들고 프로토콜에서 요구하는 아이템 삭제 메서드를 만듭니다.

 

이 코드를 보면 전 버전에서 사이드 이펙트가 있던 경향이 사라진걸 볼 수 있습니다.

데이터를 인풋으로 사용하고 일부 데이터를 출력으로 반환하는 알고리즘만 남았습니다.

 

 

Data in; data out.

 

이렇게 로직을 분리해서 명확한 테스트를 작성할 수 있게 되었습니다.

인풋 컨트롤도 쉽고 아웃풋을 보기도 좋고 숨겨진 논쟁할만한 state도 없습니다.

 

그리고 파일시스템에 대한 의존성도 없어서 빠릅니다.

 

코드로 돌아가서 보면 지우는 로직이 policy로 되어있습니다.

그리고 루프에서 effect를 처리 합니다.

 

 

여기서 비지니스 로직과 알고리즘을 사이드 이펙트를 발생하는 코드와 분리하는 법을 알아봤습니다.

이렇게 할 때, 알고리즘이 인풋과 아웃풋을 describe할 때 functional 스타일을 취하게 됩니다.

컴퓨터 데이터를 기반으로한 사이트 이펙트를 수행하는 코드를 약간만 남겨놓았습니다.

이 작은 코드가 종종 시스템의 나머지 부분과 상호작용이 제대로 작동하는지 추적하기에 사용하기에 좋습니다.

 

 

댓글
공지사항