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

[Effective Unit Testing] Chap6. 신뢰성

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

[Effective Unit Testing] Chap6. 신뢰성


@ignore, @ignore annotation, @test expected, assumetrue, assumption api, commented-out test, effective unit testing, exception test fail function, happy path test, ignore annotation, junit, lowers expectations, misleading comments, never failing test, platform prejudice, shallow promise, [Effective Unit Testing] Chap6. 신뢰성, 가정 api, 나쁜 주석, 낙관론자, 낮아진 기대치, 단언문으로부터 시작하는 습관, 리펙토링, 오해를 낳는 주석, 왜를 설명하는 주석, 절대 실패하지 않는 테스트, 조건부 테스트, 좋은 주석, 좋은 주석의 조건, 주석 냄새, 주석 제거, 주석으로 변한 테스트, 죽은 코드, 지키지 못할 약속, 플랫폼 편견, 행복한 길 테스트


-

테스트를 작성하는 이유이기도 한 신뢰할 수 있는 코드를 만들기 위해서는 테스트 자체도 믿음직해야 한다.

소프트웨어 개발에는 코드를 수정하고 개선하고 관리하는 일이 빠질 수 없는데, 만약 테스트를 믿지 못한다면 아무 관련 없어 보이는 코드라도 쉽사리 바꾸지 못한다.



-

주석은 이해를 돕기 위한 설명글로 코드에 적어 넣을 수 있다는 요긴함이 있지만,

자칫 한눈파는 순간 엉터리 정보로 둔갑하는가하면, 본문 전체를 주석 처리한 테스트는 실제로는 아무것도 안 하면서 성공했다고 보고되기도 한다.





6.1. 주석으로 변한 테스트


-

주석으로 변한 테스트(Commented-out Test)는 아무런 설명도 없이 읽는 이에게 혼란을 가져다 준다.




6.1.1. 예시




6.1.2. 개선 방법


-

전체 혹은 일부라도 주석으로 변한 테스트를 발견했다면 필시 죽은 코드와 마주하는 중이다.

그 코드에도 한 때 어떤 의미와 목적이 있겠지만, 이제는 사라졌거나 잊혀 버렸다.



-

먼저 그 테스트에 무슨 일이 있었는지 알만한 사람을 찾아 물어보자.

기억하는 사람이 없다면 다음을 따르면 된다.

1.  목적을 이해하려 노력해보고 검증해본다. 성공했다면 주석을 풀고 파악한 목적이 더 잘 표현되게끔 리펙토링한다.

2. 실패했다면 미련 없이 지워버린다.




6.1.3. 정리


-

주석으로 변한 코드는 절대 실행될 수 없는 죽은 코드다.

주석 처리한 이유를 바로 알아내지 못했다면 앞으로도 영원히 알아내지 못할 가능성이 높다.

그렇다면 그 테스트를 깨끗이 제거하는 게 차라리 낮다.

만약 와둬야 할 이유를 알아냈다면 그 의도가 더 명확히 드러나도록 추가 조치를 반드시 취해 놓아야 한다.



-

주석으로 변한 테스트는 주석 처리한 이유를 찾기 위해 프로그래머의 아까운 시간을 좀먹게 하는 고약한 냄새다.





6.2. 오해를 낳는 주석


-

오해를 낳는 주석(Misleading Comments) 은 거짓말을 늘어놓는 제멋대로인 녀석이다.

그 주석을 곧이곧대로 믿고 엉뚱한 곳에서 해매는 프로그래머를 종종 볼 수 있다.




6.2.1. 예시




6.2.2. 개선 방법


-

1. 주석 대신 더 적절한 변수명이나 메서드명을 사용하라.

2. 주석으로 설명하려던 코드 블록을 메서드로 추출하고 알맞은 이름을 지어줘라.



-

코드의 의도를 전달할 책임은 사실 코드 자신에 있다.

주석으로 코드를 설명한다는 생각 자체에 근본적인 문제가 있다는 뜻이다.

주석 없이는 동작을 이해하기 어려운 코드가 있다면 아직 코드를 충분히 리펙토링하지 못했다는 뜻이다.

변수명을 더 잘 지어주거나 코드 블록을 서술적인 이름의 private 메서드로 추출해야 했을지도 모른다.



-

좋은 주석의 조건

결론부터 말하면 “무엇을” 이 아닌 “왜”를 설명하는 주석이 좋은 주석이다.

코드가 무엇을 하는지 설명하는 주석은 무조건 코드 냄새라고 보면 된다.

그런 주석이 필요 없을 만큼 쉽게 읽을 수 있는 코드를 작성해야 한다.




6.2.3. 정리


-

주석에는 좋은 주석과 나쁜 주석이 있다.

수적으로 나쁜 주석이 압도적으로 많다.

테스트 코드에서도 마찬가지다.



-

오해를 낳는 주석의 핵심 문제는 믿을 수 없다는 것이다.

그럼에도 우리는 소스코드를 훑으며 주석만 읽고는 코드가 진짜로 그렇게 동작하는지 확인하지도 않고 덥석 믿어버리는 낙관론자가 된다.



-

오해를 낳는 주석은 처음부터 속이려고 작성되지는 않았을 것이다.

하지만 시간이 흐르고 코드가 변해가면서 서서히 괴리가 생기고 점점 혼란스럽고 제멋대로인 주석으로 변해간다.



-

오해를 낳는 주석에 대처하는 보편적인 방법이 몇 가지 있다.

이 방법의 공통점은 주석 자체를 제거한다는 점이다.

주석을 제거하는 동시에 코드를 리펙토링하여, 주석 없이도 읽을 수 있도록 만든다.

변수명을 잘 짓거나 코드 블록을 적절한 이름의 private 메서드로 추출하는 방법을 활용하면 된다.



-

좋은 주석이란 코드가 그렇게 작성될 수밖에 없던 당위성을 설명하는 주석이다.

이는 프로그래밍 언어의 문법만으로는 표현할 수 없는 영역이다.

그 외의 주석은 모두 지워버리거나 리펙토링해야 할 대상일 뿐이다.



-

empty line 조차 냄새로 치부될 수 있는데, 이는 그만큼 하나의 function 이 하나의 일에 집중하지 못하고 있다는 의미로 해석될 수 있기 때문이다. (그만큼 내용이 많거나 길다.)





6.3. 절대 실패하지 않는 테스트


-

절대 실패하지 않는 테스트(Never Failing Test)는 결코 좋은 게 아니다.

사고가 나도 알려줄 리 만무하며, 오히려 테스트에 통과했으니 문제없을 것이라는 잘못된 인식을 심어줄 수 있으니 차라리 없느니만 못하다.




6.3.1. 예시


-

실패하지 않는 테스트의 단골손님은 바로 예외가 발생하길 기대하는 테스트다.




6.3.2. 개선 방법


-

예외가 발생하길 기대하는 테스트를 작성할 때는 예외가 발생하지 않았을 때 반드시 fail() 을 불러줘야 한다는 사실을 잊어선 안 된다.

이는 JUnit 4 부터 사용가능한 @Test 의 expected 속성으로 대체할 수 있다.

단, 이 방식은 예외 객체에 접근할 기회가 사라져 깊이 있는 단언이 불가능하다.



-

절대 실패하지 않는 테스트를 예방하려면 “실수하지 말자”며 백 번 주의하는 것보다 테스트가 실패하는 모습을 눈으로 직접 확인하는 습관을 들이는 것이 최고다.

실패해야 할 상황을 고의로 만들어서 테스트를 돌려보면 된다.




6.3.3. 정리


-

테스트라면 실패해야 할 상황에서는 반드시 실패해야 한다.

만약 실패하지 않으면 우리에게 그릇된 안도감을 심어주므로 결코 가볍게 여겨서는 안 될 문제다.



-

절대 실패하지 않는 테스트는 예외가 발생했는가를 확인하는 테스트에서 가장 빈번하게 목격된다.

try-catch 블록 때문에 주의가 산만해져서 실수하기 쉽기 때문이다.

예외가 발생하지 않았을 때 fail() 을 호출하는 걸 깜빡할 수도 있고, JUnit 까지 전달되어야 할 예외를 catch 블록에서 실수로 집어삼킬 수도 있다.






6.4. 지키지 못할 약속


-

테스트 코드를 훑다 보면 가끔 실제보다 훨씬 많은 것을 검사한다고 주장하는 테스트를 목격할 때가 있다.

이는 믿지 못할 테스트이기 때문에 문제가 있다.




6.4.1. 예시


-

테스트가 자신이 내세운 것보다 훨씬 적은 것을 검사하거나, 심지어 아무것도 검사하지 않는다는 게 지키지 못할 약속(Shallow promise)의 본질적인 문제다.

이런 일은 크게 세 가지 경우에 생긴다.

    아무 ‘일'도 안 하는 테스트

    무언가 일은 하지만, 정작 ‘검증’은 전혀 하지 않는 테스트

    이름값 못하는 테스트



-

테스트 함수는 남아있지만, 테스트 코드가 없는 경우는 주석으로 변한 테스트보다 더한 해악을 끼친다.



-

assertTrue, assertEquals, assertThat 등 그 어떤 단언문도 가지고 있지 않는 함수들이 있다.

이런 테스트가 실패하는 유일한 경우는 예외가 발생할 때 뿐이다.

이런 테스트는 절대 실패하지 않고 뭐든 항시 괜찮다고만 말한다고 해서 행복한 길 테스트(happy path test)라는 별칭도 가지고 있다.




6.4.2. 개선 방법


-

주석 처리된 테스트보다는 차라리 텅 빈 테스트가 훨씬 낫다.

적어도 구현되지 않았다거나, 동작하지 않는다거나, 검증된 바 없는 기능이나 앞으로 테스트 코드를 작성해 넣어야 한다는 표시는 확실히 해주기 때문이다.



-

테스트 목록을 빈 메서드 형태로 관리하다 보면 자칫 지키지 못할 약속 문제를 일으킬 수 있다.

그러니 빈 메서드 방식을 선호한다면 JUnit 의 @Ignore annotation 을 달아놓는 것이 좋다.

이는 아직 구현되지 않았고 약속한 기능이 검증되지 않았음을 확실히 알려주는 방법이다.



-

단언문부터 시작하는 습관은 단언문을 빼먹는 실수를 예방하는 데 효과적이다.

이는 지키지 못할 약속의 세 번째 유형인 이름값 못하는 테스트를 예방하는 데도 효과적이다.

테스트 이름과 단언문 둘만 있는 상태에서 둘이 서로 다르다는 걸 알아차리지 못할 리 없기 때문이다.



-

테스트를 완성하기 전까지 테스트의 이름을 아예 비워두거나 TODO 와 같은 임시명을 쓰는 방법도 고려해봄 직하다.

확인하려는 동작을 온전히 파악한 후에는 적당한 이름이 더 잘 떠오를 것이기 때문이다.




6.4.3. 정리


-

약속을 제대로 지키지 못하는 테스트의 유형은 크게 3가지이다.

    아무 ‘일’도 안 하는 테스트

    무언가 일은 하지만, 정작 ‘검증’은 전혀 하지 않는 테스트

    이름값 못하는 테스트



-

문제를 예방하기 위해서는 코드를 주석 형태로 남겨놓기보단 아예 지워버리고, 단언문을 빠뜨리지 않도록 신경 쓰고, 테스트의 이름과 실제로 검사하는 내용이 일치하는지 살펴야 한다.

단언문부터 먼저 작성하고 테스트 코드를 다 채운 후에 테스트의 이름을 결정하는 것도 좋은 방법이다.

또한, 테스트를 작게 유지하는 것만으로도 큰 효과를 볼 수 있다.

하나하나는 작은 규율이지만 잘만 지켜주면 분명 많은 시간을 절약할 수 있을 것이다.





6.5. 낮아진 기대치


-

많은 경우 가장 쉬운 방법을 택하곤 하는데, 그 정도가 지나쳐서 너무 멀리까지 가버릴 때가 있다.

이런 테스트 냄새를 낮아진 기대치(Lowers expectations)라 부른다.

쉬운 길을 찾다 보니 검증 정확도와 정밀도까지 낮춰버리기 때문이다.

지름길을 택하면 작업 진도야 빨라지겠지만, 검증 정확도에 따라 프로그램의 동작이나 작업 결과가 달라지기도 한다.

이런 부정확성은 장기적으로 개발자를 그릇된 안도감에 빠져들게 할 수 있다.




6.5.1. 예시




6.5.2. 개선 방법


-

낮아진 기대치에 대한 확실한 대처법은 기준을 다시 높여서 예상한 바를 정확하게 검사하는 것이다.



-

완전무결한 정확함만이 꼭 미덕은 아니다.

픽셀 퍼펙션은 너무 정밀한 테스트가 가져올 수 있는 잠재적인 단점을 상기해주는 좋은 예다.

그러니 테스트에 가장 적절한 추상화 수준을 항시 고민해야 한다.




6.5.3. 정리


-

테스트는 자신이 명시한 동작에 문제가 생기면 실패해야 한다.

낮아진 기대치라는 테스트 냄새는 실패해야 할 상황에서도 실패하지 않는 지나치게 강건한 테스트를 만들어낸다.



-

낮아진 기대치의 중심에는 예상한 동작을 제대로 기술하지 못하는 너무 모호한 단언문이 자리 잡고 있다.

이처럼 과하게 모호한 단언문은 개발자에게 그릇된 안도감을 퍼트린다.

실제로는 기능 일부가 제대로 작동하지 않는데도, 그 모호한 단언문은 성공하고 있을 수도 있기 때문이다.



-

기준치를 다시 높여 더 명확하고 정교하게 단언하면 낮아진 기대치를 확실하게 해결할 수 있다.

테스트가 세부 내용까지 다룬다는 게 꼭 나쁠 이유는 없지만, 지나치게 구체적인 단언문은 자칫 픽셀 퍼펙션 문제를 일으킬 위험이 있다.





6.6. 플랫폼 편견


-

플랫폼 편견(Platform Prejudice)이란 필요한 모든 플랫폼을 동등하게 다루지 못하는 테스트 냄새이다.




6.6.1. 예시




6.6.2.개선 방법


-

assumeTrue() 라는 가정(assumption) API 를 활용하여 의도한 플랫폼이 아닌 경우에 테스트를 중단하게 할 수 있다.

가정과 다르면 해당 테스트는 더 진행되지 않는다.



-

텍스트 파일의 줄바꿈 문자로 윈도우즈에서는 캐리지 리턴(\r)과 줄바꿈(\n)이라는 두 문자를 사용하는데, 유닉스 계열에서는 줄 바꿈(\n)만 사용한다.




6.6.3. 정리


-

리눅스, 맥, 윈도우즈 등 복수의 플랫폼을 지원해야 할 경우가 많아져서 플랫폼 편견 냄새의 발생 빈도도 서서히 증가하는 추세다.

테스트가 하부 플랫폼에 따라 다른 경로를 타고 다른 단언문을 호출하고 있다면 이 문제에 직면한 것이다.



-

플랫폼마다 다르게 동작하는 테스트는 지원 플랫폼의 수대로 나눠서 그 사실을 만천하에 공개해야 한다.

그러면 최소한 플랫폼 종속 코드가 있다는 사실 정도는 모두가 인지할 수 있다.



-

테스트에서 플랫폼 편견을 발견하면 주저하지 말고 테스트를 간소화하여 테스트 각각이 자신에게 필요한 플랫폼을 직접 생성할 방법을 찾아보는 것이 좋다.



-

플랫폼 편견의 근본 원인은 최종적으로 자그마한 플랫폼 식별 로직으로 귀결된다.

그 로직을 찾아 정말로 특정 플랫폼에서만 수행해야 할 기능을 찾아 격리하고, 그 기능을 검증하는 테스트도 따로 만들어두면 이 문제를 쉽게 관리할 수 있다.



-

조건부 테스트는 태스트 내부에 숨어 있는 어떤 조건에 따라 다르게 실행되는 것을 말하며, 플랫폼 편견은 그 중 특수한 형태에 해당한다.





6.7. 조건부 테스트


-

조건부 테스트란 테스트 안에 숨겨진 조건 때문에 테스트의 이름이 의미하는 것과 다르게 동작하는 걸 말한다.




6.7.1. 예시




6.7.2. 개선 방법


-

테스트에서 조건문을 찾아내면 모든 갈래가 확실한 실패 조건을 가졌는지 확인하자.




6.7.3. 정리

-

테스트 안의 조건문은 자칫 개발자를 오해하게 할 수 있는 나쁜 신호다.

실패해야 할 상황인데 성공할 수도 있고, 심지어 제대로 검증하고 성공했다 해도, 디버거로 확인해보기 전까지는 정말로 기대한 경로를 다라 실행된 것인지 확실할 수 없다.



-

기본적으로 테스트 메서드의 모든 분기에는 자신만의 실패 조건이 있어야 한다.

하지만 엄밀히 말하면 테스트하려는 시나리오와 동작이 분기별로 서로 다르다는 뜻이니 각각을 독립된 테스트로 갈라놓는 것이 옳다.



-

if 블록 등의 조건 분기는 코드가 실제로 어떻게 작동할지가 다소 불확실하다는 방증이다.

만약 모든 게 명명백백하다면 애초에 조건문이 나타날 이유도 없기 때문이다.

불확실한 부분이 있다면 조건절보다는 가정 내용을 명시적으로 확인하는 단언문을 대신 사용하는 걸 추천한다.





6.8. 요약


-

테스트를 신뢰할 수 없고 불안정하게 만드는 냄새 중 절반은 어디가 고장이 나도 알려주지 않는 냄새다.

즉, 실패해야 할 때 실패하지 않는 테스트다.

나머지 절반은 또 다른 방식으로 오해를 낳는다.



-

주석으로 변한 테스트와 오해를 일으키는 주석 등 주석을 발못 사용해서 발생하는 문제가 있다.



-

절대 실패하지 않는 테스트와 지키지 못할 약속이 일으키는 문제도 있다.



-

낮아진 기대치의 문제는 오작동을 제대로 알아차리지 못한다.



-

플랫폼 편견과 그 밖의 조건부 테스트도 오동작을 제대로 알아차리기 힘들다.



-

이런 테스트 냄새들은 모두 아무 문제도 없다는 오해를 낳는다.

실행되지조차 않으면서 성공했다고 주장하거나 검사하지도 않을 거면서 검사할 거라 주장하는 식이다.




반응형

댓글