본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Effective Unit Testing] Chap4. 가독성

by 돼지왕 왕돼지 2019. 3. 13.
반응형

[Effective Unit Testing] Chap4. 가독성


accidental complexity, assertThat, asserttrue, Before, beforeclass, effective unit testing, FIXTURE, hamcrest matcher, hyperassertion, incidental details, inline, junit, magic number, primitive assertion, primitive obsession code smell, setup, single responsibility principle, split logic, Split Personality, test 가독성, [Effective Unit Testing] Chap4. 가독성, 가독성, 과잉보호 테스트, 광역 단언, 기본 타입 강박관념, 기본 타입 단언, 다중인격, 단언문, 단일 책임 원칙, 매직 넘버, 매직넘버 함수 형태, 명료성, 부울 연산자, 부차적 상세정보, 비교문 사용 단언문, 비트 단언, 비트 연산자, 상위 개념을 기본 타입으로 표현, 성능, 셋업 설교, 우발적 복잡성, 인지 부하, 중간 단계 단언문, 쪼개진 논리, 코드 중복, 테스트 냄새, 테스트 리펙토링, 테스트 실패 이유, 테스트 코드 가독성, 테스트 코드 냄새, 픽스처, 햄크레스트 매처, 흩어진 정보


-

테스트를 읽은 프로그래머는 코드가 “해야 할 일”을 이해할 수 있어야 한다.

또한, 테스트를 실행한 후에는 코드가 실제로 "한 일”이  무엇인지 말할 수 있어야 한다.



-

테스트의 핵심인 단언문은 대상 코드의 올바른 동작을 규정한다.



-

결국, 테스트 코드는 읽기 쉬워야 한다.

테스트가 무슨 일을 하는지 파악하기 위해 머리를 쥐어뜯는 일은 없어야 한다.



-

테스트 냄새는 숱하게 많지만, 그중에서도 가장 흔한 냄새는 바로 "기본 타입 단언”이다.





4.1. 기본 타입 단언


-

단언문은 가정이나 의도를 명시해야 한다.

또한 코드의 동작을 서술하는 문장이어야 한다.

기본 타입 단언(Primitive Assertion)이란 단언하려는 이유나 의도가 의미를 알 수 없는 단어나 숫자에 가려진 상황을 말한다.

이 경우 이해하기도 어렵고 단언을 올바로 기술한 것인지도 파악하기 쉽지 않다는 문제가 생긴다.




4.1.1. 예시




4.1.2. 개선 방법


-

assertTrue(out.indexOf(“test.txt:1 1st match”) != -1);


이것이 아래처럼 assertThat 을 사용하면서 햄크레스트 매처(Hamcrest Matcher) 를 조합하여 개선될 수 있다.

assertThat(out.indexOf(“test.txt:1 1st match”), is(not(-1)));

assertTrue(out.contains(“test.txt:1 1st match”));

assertThat(out.contains(“test.txt:1 1st match”), equals(true));


-

테스트 코드에서 어떤 방식으로 의도를 표현할 지 결정할 때, 코드 중복이나 성능보다 가독성과 명료성이 더 중요함을 기억해주다.



-

도구를 잘 쓰면 의도를 표현하는 게 한결 수월해진다.

JUnit 은 자주 쓰이는 햄크레스트 매처를 기본으로 포함하고 있는데, 그 중 containsString() 을 사용하면 또 다시 이렇게 개선할 수 있다.

assertThat(out, containsString(“test.txt:1 1st match”));



4.1.3. 정리


-

테스트에서 != 나 == 등의 비교문을 사용하는 단언문을 발견하면 주저하지 말고 추상화 수준이 적절한지 되짚어보라.

비교 대상이 -1, 0 등의 매직 넘버라면 더 생각할 것도 없다.

단언문을 즉시 이해할 수 없다면 기본 타입 단언에 해당하며 리펙토링 대상이 될 가능성이 높다.



-

기본 타입 단언은 사실 기본 타입 강박관념(primitive obsession code smell)이라는 코드 냄새의 쌍둥이다.

상위 개념을 기본 타입으로 표현하는 냄새로, 전화번호는 String 으로, 휴대전화 부가 서비스 사용 여부는 byte 로, 약정 기간은 Date 로 표현하는 경우를 떠올리면 된다.

테스트를 작성할 때는 이들 타입과 표현하려는 개념의 추상화 수준과 같은지 신경 써야 한다.



-

기본 타입에 집착하는 것 외에, 세세한 내용까지 너무 집착하는 테스트와 마주칠 때도 있다.

사소한 것 하나하나 꼼꼼하게 챙기는 관리자 같은 단위 테스트를 말한다.

이런 집착에 사로잡힌 테스트는 과하게 포괄적인 단언문을 사용하는 경향이 있다.

너무 넓어서 광역 단언이라 불릴 정도다.





4.2. 광역 단언


-

광역 단언(Hypeassertion)은 검사하려는 동작의 아주 작은 하나까지도 놓치지 않으려는 집착의 산물이다.

결과적으로, 어느 하나만 잘못되어도 바로 실패하게 되고, 본래 의도했던 것도 그 광활한 검증 범위에 묻혀 희석된다.

그래서 광역 단언과 마주치면 검사하려는 것이 무엇인지 정확히 이야기하기 어렵다.




4.2.1. 예시


-

테스트 제목(function name)과 테스트하는 내용은 일치해야 한다.

한 개의 테스트에서는 한 개만 테스트해야 한다.



-

절대 실패할 리 없는 테스트는 별 가치가 없다.

이는 십중팔구 아무것도 검사하지 않을 것이기 때문이다.



-

테스트가 실패하는 이유는 오직 하나뿐이어야 한다.

이는 단일 책임 원칙(Single Responsibility Principle)과 일맥상통한다.



-

실패한 테스트를 보고 우리가 가장 궁금한 건 “왜” 실패했는가다.

그래서 단언문이 너무 광범위하면 실패 원인을 분석하는 것이 쉽지 않다.




4.2.2. 개선 방법


-

지나치게 넓은 광역 단언과 마주쳤을 때 가장 먼저 할 일은 본질과 관련 없는 세부 내용을 찾아 테스트에서 제거하는 것이다.



-

세분화된 주제에 충실한 테스트라야 실패했을 때 문제의 근본 원인을 빠르게 찾을 수 있다.




4.2.3. 정리


-

큰 그림에 관심을 두는 건 좋다.

하지만 자칫 너무 광범위한 단언문을 만드는 실수를 저지를 수 있다.

광역 단언은 대상을 너무 크게 묶어서 모든 걸 비교하려다 보니 너무 쉽게 망가져 버린다.

단언문은 테스트의 주된 관심사와 상관없는 극히 사소한 변경에도 실패할 것이다.



-

광역 단언은 프로그래머가 테스트의 의도와 핵심을 파악하는 데도 방해가 된다.

너무 큰 덩어리를 검증하는 테스트를 찾게 되면 확인하려는 게 정확히 무엇인지 꼭 자문해보라.

그리고 그에 걸맞은 어휘로 단언문을 바꿔보자.





4.3. 비트 단언


4.3.1. 예시


-

비트 연산자는 비트나 바이트 계산을 위해 프로그래밍 언어가 제공하는 훌륭한 기능이다.

하지만 비트나 바이트라는 저수준 개념을 사용함으로써 고수준의 논의가 낯선 비트 연산자 때문에 가려질 수 있다.




4.3.2. 개선 방법


-

해결책은 간단하다.

비트 연산자를 부울 연산자(boolean operator)로 교체해서 기대하는 결과를 명확하게 표현하면 된다.




4.3.3. 정리


-

비트 단언도 여타의 기본 타입 단언과 똑같은 문제를 일으킨다.

즉, 의미 파악을 방해한다.

비트 연산자는 비트 연산자로 남겨두고, 고수준 개념은 그에 합당한 고차원적인 언어로 표현하는 게 옳다.






4.4. 부차적 상세정보


-

테스트 코드에도 부수적인 정보가 넘쳐 흐를 때가 종종 있는데, 이를 부차적 상세정보(Incidental Details) 냄새라 한다.




4.4.1. 예시




4.4.2. 개선 방법


-

1. 핵심이 아닌 설정은 private 메서드나 셋업 메서드로 추출하라.

2. 적절한 인자와 서술형 이름을 사용하라.

3. 한 메서드 안에서는 모두 같은 수준으로 추상화하라.




4.4.3. 정리


-

테스트를 하나 작성할 때마다 한 걸음 물러나 자문해보라.

‘테스트가 하려는 일이 진짜 명백한가?’ 당연하다는 대답이 선뜻 나오지 않는다면 아직 다 끝난 게 아니다.





4.5. 다중 인격


-

가장 손쉬운 테스트 개선 방법의 하나는 코드에서 다중 인격(Split Personality)을 찾는 것이다.

다중 인격이란 여러 개의 영혼(테스트 목적)이 한 몸(테스트 메서드)을 공유하는 걸 말한다.

하나의 테스트는 오직 한 가지만 똑바로 검사해야 한다.




4.5.1. 예시




4.5.2. 개선 방법




4.5.3. 정리


-

읽기 쉬운 코드는 프로그래머에게 자신의 목적을 명확히 전해준다.

다중 인격은 (여러 가지를 다루는 것)은 시야를 뿌옇게 흐리는 테스트 냄새다.

테스트의 여러 인격(관심사)을 각자의 클래스로 분리하면 뒤섞였던 테스트의 의미가 드러나 이해하기 쉬워진다.



-

테스트 클래스나 메서드를 작게 나누면 대개는 가독성과 유지보수성이 크게 좋아진다.

하지만 너무 흥분해서 정줄 놓고 나누다 보면 다중 인격의 경계를 넘어 쪼개진 논리(split logic)로 변질된다.





4.6. 쪼개진 논리


-

가독성을 높이려면 긴 메서드를 짧은 덩어리로 무조건 나누는 것은 성급한 결론이다.

반드시 의미 있는 조각이어야만 인지능력에 도움이 되는 건 아니지만, 이왕이면 의미가 있을 때 효과도 배가 된다.

무작정 작은 메서드로 추출하는 건 곤란하다.

그보다는 같은 속성을 공유하는 의미 있는 조각이 어디까지인지 신경 쓰면서 나눠주는 세심함이 필요하다.



-

흩어진 정보는 프로그래머의 인지 부하를 가중시키고, 테스트의 의미와 의도를 파악하기 어렵게 한다.




4.6.1. 예시




4.6.2. 개선 방법


-

쪼개진 논리를 해결하는 가장 간단한 방법은 필요한 외부 데이터와 코드를 모두 테스트 안으로 이주시키는 것이다.



-

데이터나 로직을 언제 통합(inline) 해야 할까?

어떤 데이터와 로직은 통합하는 것이 좋고, 어떤 것은 분리된 채로 놔두는 것이 낫다.

무엇을 끌어안고 무엇을 밀쳐둬야 할지 빠르게 판단할 수 있는 팁은 아래와 같다.

    1. 짧다면 통합하라.

    2. 통합하기에 너무 길다면 팩토리 메서드나 테스트 데이터 생성기를 통해 만들어라.

    3. 이마저도 쉽지 않다면 그냥 독립 파일로 남겨둬라.



-

가장 좋은 방법은 필요한 모든 정보를 테스트 안에 두는 것이다.

그러다 보면 테스트가 너무 비대해지기도 하는데, 테스트 클래스에 도우미 메서드를 만들어서 살짝 거리를 두는 정도면 대부분 해결된다.



-

어쩔 수 없이 데이터 파일을 분리해야 할 때에는 다음 조언이 도움이 될 수 있다.

    - 확인하려는 시나리오에 꼭 필요한 최소한의 데이터만 남긴다. 흩어진 정보가 최우선 선결 과제가 아니라 해도 우발적 복잡성(accidental complexity)을 최소화하지 못한 것에 대한 변명의 여지는 없다.

    - 데이터 파일과 이를 사용하는 테스트와 같은 폴더에 둔다. 그래야 데이터 파일을 찾기 쉽고 테스트 코드를 다른 곳으로 옮길 때 빼먹지 않을 수 있다.

    - 어떤 구조를 취하게 되든 팀 공통의 규약을 만들고 따라야 한다. 여러분 동료의 삶도 훨씬 평온하게 만들어 줄 것이다.




4.6.3. 정리


-

때에 따라서는 논리나 데이터를 독립 파일로 두는 것이 합리적이기도 하지만, 일반적으로는 이를 사용하는 테스트 메서드 안에 함께 두는 방법을 찾아보는 게 바람직하다.

여의치 않다면 같은 클래스 안에라도 넣어두자.

쪼개진 논리를 수용하는 건 최후의 보루여야 한다.






4.7. 매직 넘버

-

매직 넘버(magic number)를 피해야 한다는 주장도 거의 모든 이가 동의하는 주제 중 하나이다.

매직 넘버란 소스코드 중 할당문이나 메서드 호출 등에 박혀 있는 숫자로 된 값을 말한다.

매직 넘버가 나쁜 이유는 뜻을 알 수 없기 때문이다.




4.7.1. 예시




4.7.2. 개선 방법


-

매직 넘버를 정적 상수나 지역 변수로 바꿔주는 게 더 일반적인 해법이지만, 함수형태로 표현하는 방법도 있다.

매직 넘버 형태는 

roll(TEN_PINS, TWELWE_TIMES)
    

함수형태로 표현하는 방법은

roll(pins(10), times(12))


함수형태의 장점은 상황에 따라 값을 다르게 할 경우이다.




4.7.3. 정리


-

지역 변수나 서술형 이름의 상수로 대체하는 게 매직 넘버를 없애고 의미 전달력을 키워주는 가장 보편적인 방법이다.

매직 넘버의 뜻을 표현하는 함수를 사용하는 기법도 있다.





4.8. 셋업 설교

-

테스트 시나리오를 준비하기 위한 상당량의 코드를 셋업 메서드로 옮기곤 한다.

그러나 우리는 테스트 코드를 제품 코드처럼 정성껏 다루는 건 사치라고 여겨 버려, 지저분한 셋업 메서드를 발견하고도 심각하게 받아들이지 않는다.




4.8.1. 예시


-

셋업 역시 테스트의 일부이기 때문에 셋업이 복잡해지면 자연스럽게 테스트의 복잡도도 함께 커진다는 게 문제다.



-

픽스처(fixture)는 테스트가 실행하는 어떤 것이라 할 수 있는데, 시스템 속성, 테스트 클래스에 정의된 상수, 셋업 메서드가 초기화한 private 멤버 등이 여기 속한다.

따라서 테스트를 온전히 이해하려면 테스트가 사용하는 픽스처도 이해해야 한다.

그리고 셋업의 역할이 테스트를 실행하기 위한 상태와 필요한 객체를 미리 만들어 놓는 것이다 보니 픽스처 정의 대부분을 셋업에서 처리하게 된다.




4.8.2. 개선 방법


-

1. 셋업에서 핵심을 제외한 상세 정보는 private 메서드로 추출한다.

2. 알맞은 서술적 이름을 사용한다.

3. 셋업 내의 추상화 수준을 통일한다.




4.8.3. 정리


-

@Before 나 @BeforeClass 로 대표되는 셋업 메서드도 다른 테스트 코드와 마찬가지로 애정어리게 보살펴야 한다.

셋업은 픽스처의 주요 부분을 구성하고 테스트가 실행될 환경을 조성한다.

이 픽스처를 이해하지 못하면 테스트의 목적 역시 온전히 이해할 수 없다.

따라서 셋업  설교도 테스트 메서드의 부차적 상세정보를 다룰 때와 같은 마음가짐으로 대처해야 한다.

세부 정보를 추출하고, 단계마다 서술적인 이름을 부여하고, 추사오하 수준을 한결같이 유지하여 읽고 파악하기 쉽도록 해야 한다.



-

셋업은 훨씬 더 심각한 문제를 암시하는 경고일 수도 있다.

셋업을 간소화하기 위해 애쓰다가, 마지막에 진짜 문제는 대상 객체가 잘못 설계되어서였음이 밝혀지기도 한다.





4.9. 과잉보호 테스트


4.9.1. 예시




4.9.2. 개선 방법


-

아무런 가치도 없는 무의미한 검사들이 있다.

그런 녀석들은 과감하게 제거해주자.




4.9.3. 정리


-

과잉보호 테스트는 테스트의 성패를 결정짓는 진짜배기 단언문에 도달하기 전까지 불필요한 중간 단계 단언문이 많이 등장하는 것을 말한다.

이런 불필요한 단언문은 아무런 가치도 보태주지 않으므로 제거되어야 마땅하다.





4.10. 요약


-

테스트 코드의 가독성을 떨어뜨리는 테스트 냄새들이 있다.



-

기본 타입 단언은 고수준 개념을 너무 낮은 개념을 이용해서 검사한다.

대상 코드의 추상화 수준과는 거리가 한참 먼 기본 데이터 타입으로 비교하는 것이다.



-

광역 단언은 너무 많은 것을 바란다.

너무 많아서 극히 사소한 내용 하나만 바뀌어도 테스트가 실패해버린다.



-

비트 단언은 요구 조건을 표현하는 단언문에서 비트 연산자를 사용한다는 것이다.

비트 연산자를 자주 접하지 못하는 개발자에게는 상당히 낯설다는 게 문제다.

한마디로 너무 어렵다.



-

부차적 상세정보는 테스트의 본질을 혼잡스럽고 중요치 않은 세부 사항 속에 파묻어 버린다.

다중 인격은 모든 기능을 단번에 검사하겠다는 욕심이 앞서 다수의 독립된 테스트를 억지로 하나로 합쳐놓아 생기는 혼란이다.

쪼개진 논리는 논리를 여러 파일로 흩어버려 추적하려는 개발자의 맥을 끊는다.



-

매직 넘버는 테스트 코드에 어질러져 있는 임의의 숫자이며, 명확한 이름을 지어주기 전까지는 그 의미를 알 방도가 없다.

셋업 설교는 장문의 셋업 메서드인데, 너무 세부적인 것까지 잔소리처럼 끊임없이 늘어놓는다.

과잉보호 테스트는 반드시 통과해야 할 핵심 단언문에 도달하는 과정에서 모든 선행조건 하나하나를 명시적으로 단언하는 데 열정한다.



-

코드는 작성되거나 수정되는 횟수보다 누군가에게 읽히는 횟수가 훨씬 많다.

그리고 코드를 이해할 수 있어야만 유지보수도 생각할 수 있다.




반응형

댓글