'아웃풋테스트'에 해당하는 글 1건





당신이 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

,