본문 바로가기
독서

자바와 JUnit을 활용한 실용주의 단위테스트 책 리뷰 및 소감

by 도쿄정대리! 2022. 12. 25.

이전 회사에서 단위 테스트 및 결합 테스트에서 많은 문제점이 있었는데요. 그리고 그것이 시발점이 되어 결국 퇴사까지 이어지는 슬픈 사연이 있었습니다. 단위 테스트에 대해서 처음부처 천천히 공부하기 위해 선택한 책 '자바와 JUnit을 활용한 실용주의 단위 테스트'입니다. 책의 내용 요약과 리뷰 및 소감을 확인하실 수 있습니다.

 

목차

     

     

    자바와 Jumit을 활용한 실용주의 단위테스트 라고 적혀있는 책의 표지 부제로 가치있는 단위 테스트를 만들자 라고 적혀 있는 사진
    책 표지

    자바와 JUnit을 활용한 실용주의 단위 테스트 내용 요약

    저자는 각각의 장별로 테스트에 대해서 설명하고 있는데요. 본인에게 필요한 부분만 발췌해서 보는 것을 권장하고 있습니다. 하지만 단위 테스트에 대한 개념이 없는 독자의 경우에는 전체 내용을 다 읽는 것을 권장하고 있는데요. 네 그래서 저는 전체 내용을 다 읽어 보았습니다.

     

    전반적으로 TDD(테스트 주도 개발), 짝 코딩등 애자일 방식에서 강조되는 방식들이 많이 등장하는 것을 알 수 있습니다. 각 장의 내용을 바로 알아보도록 하겠습니다.

     

    1장 첫 번째 JUnit 테스트 만들기

    먼저 2명의 프로그래머를 예시로 들어 단위 테스트의 필요성에 대해서 이야기를 합니다. 한 명은 에러가 검출되었을 때 원인을 찾기 위해서 생각보다 많은 시간을 써야 하는 프로그래머, 그리고 다른 한 명은 작성한 코드에 대해서는 단위 테스트를 항상 실행하는 프로그래머인데요. 프로그램에 문제가 생겼을 때 전자의 인물은 작게는 5분에서 길게는 30분 이상 대응시간이 걸리고 후자는 빠르면 1~2분 이내로 대응을 할 수 있는 프로그래머가 된다는 것입니다.

     

    이후 IDE에서 JUnit을 설정하는 방법에 대해서 설명하고 있습니다. 그리고 테스트 준비, 실행, 단언의 아주 간단한 예시로 JUnit이 어떤 식으로 동작되는지에 대해서 설명하고 있습니다. 간단한 입력 자료를 준비하고 그 자료를 메서드등의 동작으로 실행하고 마지막으로 원하는 기댓값이 검출되었는지 단언을 하는 단계라고 생각하시면 되겠습니다.

     

    2장 JUnit 진짜로 써 보기

    이번장에서는 1장에서 실행한 JUnit 테스트를 좀 더 복잡한 코드에 대입해서 설명을 하고 있습니다. 여러 VO(도메인에서 한 개 또는 그 이상의 속성들을 묶어서 특정 값을 나타내는 객체를 의미) 클래스를 이용해 연산을 하는 메서드를 테스트하는 방법에 대해서 설명하고 있는데요.

     

    어떤 테스트를 작성할 수 있는지에 대해서 결정하는 방식에 대해서 나타내고 있습니다. 각각의 연산별로 어떠한 경로(방식)의 테스트가 가능할지에 대해서 생각하는 것인데요. 우선은 생각나는 데로 가능한 경로를 쭉 적습니다. 그리고 나면 가장 위험한 테스트 경로가 어디인지 대략적으로 보이게 되는데요. 그 경로에 대해서 우선은 테스트를 작성하는 것입니다.

     

    1장과 마찬가지로 테스트를 실행하는 방법은 준비, 실행, 단언의 순서로 진행되게 됩니다. 이때 유지 보수성도 같이 생각하면서 만들면 더욱 좋겠습니다. 예를 들어 20줄의 코드를 위해서 테스트 15개*10줄을 만든다는 것은 과하다는 것이죠.

     

    또한, @Before 메서드를 사용해서 테스트를 초기화하는 방법에 대해서 설명하고 있는데요. 테스트에 사용할 VO 인스턴스를 만들고 그 안의 변수들을 적절한 인스턴스로 초기화하는 것입니다. 이후 검증할 메서드를 실행하고 테스트가 통과 혹은 실패했는지 확인하는 것입니다.

     

    3장 JUnit 단언 깊게 파기

    단위 테스트가 준비, 실행, 단언의 방식으로 진행된다고 하였습니다. 이번에는 단언에 대해서 좀 더 깊게 알아볼 차례입니다. 여러 가지 단언이 무엇을 할 수 있는지에 대해서 설명하고 있습니다.

     

    ・assertTrue : 가장 광범휘하게 사용, 실행한 값이 참인지 확인(boolean 형태)

    ・assertThat : 두 값을 비교해서 참인지 확인

    ・햄크레스트 매처 : 단언 코드에서 더 많은 것을 표현할 수 있는 매처들

      ー부동소수점 비교 : 컴퓨터가 모든 부동소수점 수를 표현할 수 없기에,

            간단한 형태로 사용하는 매처(ex) closeTo()등을 사용해서 결괏값 비교)

     ー단언 설명 : 마치 주석처럼 'assertThat()'에서 첫 번째 인자를 메시지로 사용

     

    그리고 예외 처리를 어떻게 할 것인가에 대해서도 설명하고 있는데요. 어떤 클래스가 예외를 던지는 조건을 이해하면 그 클래스를 사용하는 개발자의 삶이 한결 편안해지죠.

     

    ・애너테이션 사용 : @Test 애너테이션을 사용하여 예외가 발생하면 테스트가 통과하는 방식

    ・try/catch, fail : try 블록에서 예외가 발생하면  catch 블록으로 넘어가 테스트가 종료, 그렇지 않으면 fail문으로 넘어가 테스트 실패

    ・ExpectedException : ExpectedException을 public으로 선언하여 @Rule 애너테이션을 붙여 사용, 이후 예외 발생과 예외 메시지등을 확인

    ・예외 무시 : 이미 검증된 예외에 대해서 throws 사용하여 예외를 다시 던지는 방식

     

     

    4장 테스트 조직

    테스트 코드를 잘 조직하고 구조화할 수 있는 JUnit 기능들에 대해서 설명하고 있습니다. 테스트를 가시적이고 일관성 있게 만드는 방법, 메서드가 아니라 동작을 테스트하여 코드의 유지 보수성을 높이는 방법, 테스트 이름, 애너테이션을 활용해 초기화 및 정리 코드를 만드는 방법, 테스트를 안전하게 무시하는 방법 등입니다. 각각 자세히 살펴보면 아래와 같습니다.

     

    ①AAA로 테스트 일관성 유지

    위에서 테스트를 준비, 실행, 단언으로 조직화하였는데요. 이를 트리플- A(AAA)라고도 부르는데요. 각 동작별로 확인을 하면 아래와 같습니다.

    ・ 준비 : 테스트 코드를 생 행하기 전, 시스템이 적절한 상태에 있는지 확인하는 과정. 객체 생성 및 API 호출 등

    ・ 실행 : 테스트 코드를 실행, 단일 메서드를 호출

    ・ 단언 : 실행한 코드가 기대한 대로 동작하는지 확인

    가끔 네 번째 단계도 필요한데요.

    ・ 사후 : 자원이 할당되었다면 잘 정리되었는지 확인

     

    ②메서드가 아닌 동작으로 테스트

    테스트 작성 시에는 클래스의 동작에 집중해야 하며, 개별 메서드를 테스트한다고 생각하면 안 됩니다. 여기에서 포인트는 클래스의 개별 메서드가 아니라, 확인하고자 하는 동작이 잘 작동하는지 확인하는 것입니다. 예를 들어 은행 입출금 테스트를 한다고 하면, 개별 입출금, 다수의 입출금, 계좌 잔고, 출금 예산 오버 등의 개별 메서드들이 있을 텐데요. 

     

    각각 테스트를 하기 위해서는 입금과 출금 같은 개별 동작이 먼저 진행되어야 하는데요. 이렇게 전체적인 시각에서 테스트를 동작별로 실시하는 것이 중요합니다.

     

    ③테스트와 프로덕션 코드의 관계

    Junit 테스트는 검증 대상인 프로덕션 코드와 같은 프로젝트에 위치할 수 있으나, 프로덕션 코드와는 분리되어야 합니다. 테스트 코드는 프로덕션 시스템 코드에 의존하지만, 그 반대는 해당하지 않아야 합니다. 프로덕션 소프트웨어를 배포할 때도 테스트를 같이 배포할 것인지를 선택하여야 합니다.

     

    테스트를 같이 배포한다면, 대부분 별로 디렉터리로 분리하여 프로덕션 코드와 같은 패키지에 넣는 방법을 사용합니다. 혹은 별도의 디렉터리와 유사한 패키지에 테스트 코드를 유지하는 방법을 사용하기도 합니다. 여기에서 포인트는 테스트를 위해 내부 테이터를 노출하는 것은 좋지 않다는 것입니다.

     

     

    ④단일 목적 테스트

    테스트를 하나의 목적으로 진행해야 한다는 것입니다. 한 동작에 대해서 하나의 테스트를 실시할 수 있도록 긴 테스트가 있다면, 그 테스트를 잘게 쪼개어 여러 개의 테스트로 진행하는 것이 좋다는 것입니다. 이것의 이점은 문제가 있는 동작에 대해서 빠르게 파악할 수 있고, 실패한 테스트를 해독하는데 필요한 시간을 줄이며, 모든 케이스가 올바르게 실행되었음을 확인할 수 있습니다.

     

    ⑤문서로서의 테스트

    단위 테스트가 신뢰할 수 있는 문서의 역할을 해야 합니다. 코드 자체로 쉽게 이 애 하기 어려웠던 것을 테스트로 알려줄 수 있는데요. 이처럼 작은 테스트로 어떠한 테스트를 하는 것인지, 이 메서드의 역할이 무엇인지를 일종의 문서화처럼 활용할 수 있다는 개념입니다.

     

    이를 위해서 테스트의 이름을 설명적으로 지어야 한다고 합니다. 예를 들어 아래와  같습니다.

    좋지 않은 예) makeSingleWithdrawal

    좋은 예) withdrawalReducesBalanceByWithdrawnAmount

     

    무엇을 하고자 하는 건지 메서드명에서 명확하게 알 수 있게 하여야 한다는 것이 요점입니다.

     

    doingSomeOperationGeneratesSomeResult

    (어떤 동작을 하면 어떤 결과가 나온다.),

    someResultOccursUnderSomeCondition

    (어떤 결과는 어떤 조건에서 발생한다.)

    이러한 식으로 설명적인 메서드명을 짓는 것이 좋다고 합니다. 이외에도 지역 변수 이름 개선, 의미 있는 상수 만들기, 햄크레스트 단언 사용, 커다란 테스트를 작게 나누기, 군더더기 테스트들은 도우미 메서드와 @Before 메서드로 이동 등을 실행하는 것이 좋습니다.

     

    ⑥@Before와 @After

    테스트를 실행하다 보면 상당한 테스트 코드가 같은 초기화 부분을 가지고 잇는 것을 발견할 수 있는데요. 이때 @Before 메서드를 활용하면 중복된 코드를 유지 보수하게 되는 악몽을 막을 수 있습니다. 이를 위해서 잘 활용해야 하는 것이 @Before, @After 애노테이션입니다. @After 애노테이션은 테스트가 실행되고 나서 실행되며, 설사 테스트가 실패해더라도 실행되게 됩니다.

     

    이 외에도 BeforeClass, AfterClass의 애너테이션 등이 있습니다. 코드의 작성 순서와는 관계없이 BeforeClass, AfterClass들이 실행되는 순서는 항상 동일하니 주의를 기울이는 것이 좋습니다.

     

    ⑦테스트를 의미 있게 유지

    여기에서는 '항상 녹색으로'라는 문구를 항상 떠올리면 되겠습니다. 테스트를 실행하여 항상 녹색을 유지하는 한 가지 방법은 '필요하다고 생각되는 테스트만 실행'하는 것입니다. 항상 테스트는 빠른 속도로  실행되어야 하고 이를 위해서 DB와 연동하여 테스트하는 것이 아닌, 동작 자체가 잘 작동하는지 개별로 테스트를 하여야 한다는 것입니다. DB데이터와 자바의 메서드 및 동작을 분리하여 테스트하기 위해 목 객체를 활용하여 빠른 속도로 테스트를 진행할 수 있습니다.

     

    또한 테스트를 진행하다 보면 테스트 코드가 적색이 될 수도 있지만, 문제가 없는 경우라면 무시하는 것도 좋은 방법입니다. 이때 적색된 코드를 의식적으로 무시하는 것이 아닌 @Ignore 등의 애노테이션을 사용하면 좀 더 마음이 편해지는 것을 알 수 있습니다.

     

    5장 좋은 테스트의 FIRST 속성

    단위 테스트를 제대로 작성하지 못하면 많은 스트레스와 함께 수면 시간까지 부족해질 수 있습니다. 이러한 일이 없도록 하기 위해서 FIRST 원리를 따를 것을 저자는 제안합니다.

     

    Fast : 빠른

    Isolated : 고립된

    Repeatable : 반복 가능한

    Self-validating : 스스로 검증 가능한

    Timely : 적시의

     

    각각의 내용별로 살펴보면 아래와 같습니다.

     

    Fast  : 빠른

    단위 테스트를 하루에 서너 번 실행하기도 버겁다면 무언가 잘못된 방향으로 흘러가고 있다는 것이라고 저자는 이야기하는데요. 설계를 깨끗하게 하면 빠르게 유지를 할 수 있습니다. 가장 느린 테스트에 대한 의존성을 줄여야 합니다. 테스트 코드가 DB를 호출한다면 전체 테스트 또한 느려집니다. DB를 호출하는 대신 목 객체를 사용하면 테스트를 빠르게 실행할 수 있습니다. 느린 것에 의지하는 코드를 최소화하여 항상 빠르게 테스트를 유지하는 것이 중요합니다.

     

    Isolated : 고립된

    좋은 단위 테스트는 검증하려는 작은 양의 코드에 집중합니다. 그리고 이것이 '단위'라고 말하는 정의와 정확히 일치하는데요. 다른 코드와의 상호작용 및 의존성을 최대한 제거해야 합니다. 테스트에 두 번째 단언을 추가할 때 스스로 질문해 보는 것이 중요한데요. '이 테스트가 단일 동작을 검증하는 것은 돕는가, 아니면  새로운 테스트로 기술할 수 있는가?' 이 질문을 스스로 해보고 후자라면 새로운 단위 테스트를 만드는 것이 옳습니다. 테스트를 고립시켜 시계처럼 동작하게 만드는 것이 중요합니다.

     

    Repeatable : 반복 가능한

    테스트는 실행할 때마다 결과가 같아야 합니다. 그리고 그렇게 하려면 직접 통제할 수 없는 외부 환경에 있는 항목들과 테스트를 격리시켜야 합니다. 예를 들어 현재 시간과 비교를 하는 동작을 테스트 하려고 하면 테스트의 실시 시간에 따라서 테스트 결과는 달라질 텐데요. 이러 때는 코드에 가짜 Clock 객체를 만들어서 실행하는 방법 등으로 최대한 외부와의 의존성 등을 줄여야 합니다.

     

    Self-validating : 스스로 검증 가능한

    단위 테스트는 여러분의 시간을 절약해주기 위해서 존재합니다. 테스트에 필요한 어떤 단계도 자동화를 해야 합니다. 그럼에도 테스트를 하기 위해서 외부 설정이 필요하다면 위의 'Isolated' 단계를 위반한 것입니다. 지속적으로 코드가 통합되고 배포된다는 가정하에 코드를 통합할 때마다 빌드가 자동으로 수행되고 모든 테스트가 실행되도록 테스트를 만들어야 합니다.

     

    Timely : 적시의

    '언제 단위 테스트를 작성할 것인가?'에 대한 대답은 '언제든지'입니다. 사실상 언제라도 단위 테스트를 작성할 수 있습니다. 단위 테스트를 양치를 하는 것과 같이 좋은 습관입니다. 나중에 문제가 생기기 전에 미리 파악해서 해결할 수 있는 아주 좋은 습관이죠. '별 문제없을 코드니 나중에 확인해 보자'라는 생각으로 코드를 코드 저장소에 커밋한 뒤에 그 코드를 다시 테스트해볼 확률은 아주 희박합니다. 아무 문제가 없으면 정말 다행이지만, 생각지도 못 한 에러는 보통 그런 코드들에서 발생하게 됩니다. 평화로운 어느 날 갑작스럽게 식은땀을 흘리지 않기 위해서 단위 테스트를 항상 하는 것에 대해서 당연하게 생각하면 좋습니다.

     

    6장 Right-BICEP : 무엇을 테스트할 것인가?

    메서드 혹은 클래스의 코드를 보고 숨어 있는 모든 버그를 찾아내는 것은 불가능합니다. 경험이 충분히 쌓이면 느낌만으로도 어디에 문제가 있는 찾아 집중적으로 테스트할 수 있습니다. 이번장에서는 무엇을 테스트할지에 대해 쉽게 선별할 수 있게 합니다. 

     

    Right 결과가 올바른가?

    테스트 코드는 무엇보다도 먼저 기대한 결과를 산출하는지 검증할 수 있어야 합니다. 여기에서 중요한 질문은 바로 이것입니다. '코드가 정상적으로 동작한다면, 나는 그것을 알 수 있는가?'인데요. 이런 행복 경로 테스트를 할 수 없다면 그 내용을 완전히 이해하지 못한 것입니다.

     

    B 경계 조건은 맞는가?

    우리가 마주하는 수많은 결함은 주로 모서리 사례(corner case)이므로 테스트로 이것들을 처리해야 합니다. 생각해봐야 하는 경계 조건들은 다음과 같습니다.

     

    ・모호하고 일관성 없는 입력 값, 예를 들어 특수 문자가 포함된 파일 이름

    ・잘못된 양식의 테이터, 예를 들어 최상위 도메인이 빠진 이메일 주소

    ・수치적 오버플로를 일으키는 계산

    ・비거나 빠진 값, 예를 들어 0,0.0, "" 혹은 null

    ・이성적은 기댓값을 훨씬 벗어나는 값, 예를 들어 200세의 나이

    ・중복을 허용해서 안 되는 목록에 중복 값이 있는 경우

    ・정렬이 안된 리스트 혹은 그 반대, 정렬 알고리즘에 이미 정렬된 입력 값을 넣는 경우나 정렬 알고리즘에 역순 테이터를 넣는 경우

    ・시간 순이 맞지 않는 경우, 예를 들어 HTTP서버가 OPTIONS 메서드의 결과를 POST메서드보다 먼저 반환해야 하지만 그 후에 반환하는 경우

     

    많은 경계 조건들을 잘 확인해야 합니다. 이런 경계 조건에서는 CORRECT를 기억하면 많은 도움이 됩니다.

    Conformance(준수) : 값이 기대한 양식을 준수하고 있는가?

    Ordering(순서) : 값의 집합이 적절하게 정렬되거나 정렬되지 않았나?

    Range(범위) : 이성적인 최솟값과 최댓값 안에 있는가?

    Reference(참조) : 코드 자체에서 통제할 수 없는 어떤 외부 참조를 포함하고 있는가?

    Existence(존재) : 값이 존재하는가(non-null), 0이 아니거나, 집합에 존재하는가? 등

    Cardinality(기수) : 정확히 충분한 값들이 있는가?

    Time(절대 혹은 상대적 시간) : 모든 것이 순서대로 일어나는가? 정확한 시간에?

     

    I 역 관계를 검사할 수 있는가?

    때때로 논리적인 역 관계를 적용하여 행동을 검사할 수 있습니다. 마치 곱셈으로 나눗셈을 검증하고 뺄셈으로 덧셈을 검증하는 것과 같이 말입니다. 데이터베이스에 데이터를 넣는 코드가 있다면 테스트에서 직접적인 JDBC 쿼리를 해 보는 것도 하나의 방법입니다.

     

    C 다른 수단을 활용하여 교차 검사를 할 수 있는가?

    테스트에는 수많은 해법이 존재합니다. 우리는 그중에서 성능이 좋거나 느낌이 좋은 테스트를 선택하여 테스트를 실행하는데요. 그럼 선택하지 않은 테스트 방법들을 이용해서 교차 검사로 활용할 수 있습니다.

     

    E 오류 조건을 강제로 일어나게 할 수 있는가?

    행복 경로 테스트가 존재한다는 것은 반대로 불행 경로도 존재한다는 것입니다. 오류가 절대로 발생할 수 없다고 생각할 수도 있는데요. 가장 끔찍한 결함들은 종종 전혀 예상치 못한 곳에서 나옵니다. 고려해야 할 몇 가지 시나리오를 소개합니다.

     

    ・메모리가 가득 찰 때

    ・디스크 공간이 가득 찰 때

    ・시간에 관한 문제들(서버와 클라이언트 간의 시간이 달라서 발생하는 문제들)

    ・네트워크 가용성 및 오류들

    ・시스템 로드

    ・제한된 색상 팔레트

    ・매우 높거나 낮은 비디오 해상도

     

    P 성능 조건은 기준에 부합하는가?

    추측으로 성능 문제에 대응하면 안 됩니다. 단위 테스트를 설계하여 진짜 문제가 어디에 있으며 예상한 변경 사항으로 어떤 차이가 생겼는지 파악해야 합니다.

    최적화를 하기 전에 먼저 기준점으로 현재 경과 시간을 측정하는 성능 테스트를 작성하세요(몇 번 실행하여 평균 시간을 계산하는 것을 추천합니다).  코드를 변경하고 성능 테스트를 다시 실행하여 결과를 비교합니다. 상대적인 개선량을 찾으십시오.

     

    7장 경계 조건 : CORRECT 기억법

    위의 6장에서 소개한 CORRECT를 좀 더 자세히 소개하는 장입니다.

    Conformance(준수) : 값이 기대한 양식을 준수하고 있는가?

    Ordering(순서) : 값의 집합이 적절하게 정렬되거나 정렬되지 않았나?

    Range(범위) : 논리적인 최솟값과 최댓값 안에 있는가?

    Reference(참조) : 코드 자체에서 통제할 수 없는 어떤 외부 참조를 포함하고 있는가?

    Existence(존재) : 값이 존재하는가(non-null), 0이 아니거나, 집합에 존재하는가? 등

    Cardinality(기수) : 정확히 충분한 값들이 있는가?

    Time(절대 혹은 상대적 시간) : 모든 것이 순서대로 일어나는가? 정확한 시간에?

     

    각 CORRECT 조건에 대해 넘겨진 인수, 필드와 지역적으로 관리하는 변수들까지 가능한 모든 발생 원인이 데이터에 미칠 영향을 고려하여야 합니다. 그리고 다음 질문에 완전히 대답할 수 있어야 합니다.

     

    '그 밖에 문제 될 것이 있는가?'

     

    무엇이든지 잘못될 가능성이 있는 것이 떠오르면 테스트 이름을 적어 놓으셔야 합니다. 테스트에 관해 가능한 상상력을 오랫동안 유지해보시면 좋습니다.

     

    Conformance(준수) 

    많은 데이터 요소가 특정 양식을 따라야 하는데요. 예를 들면 이메일 주소, 전화번호, 계좌 번호, 파일 이름 등 양식이 있는 문자열 데이터를 검증할 때는 많은 규칙이 필요합니다. 시스템의 데이터 흐름과 규칙들을 이용해서 각각의 데이터 양식에 맞는 경계 조건들을 테스트하는 것이 중요합니다.

     

    Ordering(순서) 

    데이터 순서 혹은 큰 컬렉션에 있는 데이터 한 조각의 위치는 코드가 쉽게 잘못될 수 있는 CORRECT 조건에 해당합니다. 순서 조건이 맞는지 확인하는 것이 필요합니다.

     

    Range(범위) : 논리적인 최솟값과 최댓값 안에 있는가?

    int 타입으로 사람의 나이를 표현하면 불필요하게 잘못된 숫자가 입력될 가능성이 생깁니다. 이럴 때는 새로운 클래스를 생성하여 최소와 최댓값을 제한하여 생성하는 것도 한 방법입니다. 객체 내부에서 각 나이의 최솟값과 최댓값을 검증하는 메서드를 사용하여 생성하는 것이죠.

     

    Reference(참조) : 코드 자체에서 통제할 수 없는 어떤 외부 참조를 포함하고 있는가?

    메서드를 테스트할 때는 다음을 고려해야 합니다.

     

    ・범위를 넘어서는 것을 참조하고 있지 않은지

    ・외부 의존성은 무엇인지

    ・특정 상태에 있는 객체를 의존하고 있는지 여부

    ・반드시 존재해야 하는 그 외 다른 조건들

     

    어떤 상태에 대해 가정할 때는 그 가정이 맞지 않으면 코드가 합리적으로 잘 동작하는지 검사해야 합니다.

     

    Existence(존재) : 값이 존재하는가(non-null), 0이 아니거나, 집합에 존재하는가? 등

    스스로 '주어진 값이 존재하는가?'라고 질문해보면 많은 잠재적인 결함들을 발견할 수 있습니다. 필드를 유지하는 메서드에 대해서 그 값이 null 혹은 0 혹은 비어 있는 경우에 어떤 일이 일어날지 생각해 보시면 됩니다. 기대하는 파일이 없거나, 네트워크가 다운되었을 때 어떤 일이 일어나는지 확인하는 테스트를 작성하세요.

     

    Cardinality(기수) : 정확히 충분한 값들이 있는가?

    '일직선으로 뻗은 길이가 12미터인 곳에 울타리를 몇 개 세워야 합니다. 각 울타리 영역은 3미터이고, 각 영역의 끝에도 울티라 기둥을 세워야 합니다. 이때 울타리 기둥은 몇 개 필요합니까?' 대다수의 사람들은 4개라고 대답할 것이고 이것은 정답이 아닙니다.(5개입니다.) 이것을 '울타리 기둥 오류'라고 하는데요. 문제에 대해 충분히 생각하지 않아서 발생하는 오류들입니다.

     

    한 끗 차이로 발생하는 수많은 경우 중 하나가 바로 이 '울타리 기둥 오류'입니다. 경계 조건에 집중하여 그 값이 어떤 형태로 존재할 수 있는지 정확히 아는 것이 중요합니다. 기수에서 중요한 경우는 3가지로 나뉘게 됩니다. '0', '1', 다수(1보다 많은 경우)입니다.

     

    Time(절대 혹은 상대적 시간) : 모든 것이 순서대로 일어나는가? 정확한 시간에?

    마지막 경계 조건인 시간입니다. 시간에 관해서 신경 써야 하는 부분은 아래와 같습니다.

    ・상대적 시간(시간 순서)

    ・절대적 시간(측정된 시간)

    ・동시성 문제들

     

    상대적 시간은 타임아웃 문제도 포함할 수 있습니다. 수명이 짧은 자원에 대해 코드가 얼마나 기다릴 수 있는지 결정해야 합니다. 또한, 메서드에서 소요되는 시간이 인내심 없는 호출자에게 너무 길지는 않은지 결정할 필요가 있습니다. 또한 하루가 항상 24시간인지에 대해서도 생각해야 합니다. 미국 대부분의 지역에서는 24시간이 아닙니다. 그 이외에도 멀티스레드이면서 동시적인 프로그램을 설계하고 구현하고 디버깅하는 등의 것들이 있겠습니다.

     

    8장  깔끔한 코드로 리팩터링 하기

    시스템을 개발하면서 코드 설계를 깔끔하게 유지하고  싶을 것이며, 유지 보수를 용이하게 하고 싶을 것입니다. 이를 위해 리팩터링은 반드시 실시되어야 합니다.

     

    ・작은 리팩토링

    리팩토링을 한다는 것은 기존 기능은 그대로 유지하면서 코드의 하부 구조를 건강하게(깔끔하게) 변형하는 것입니다. 그리고 당연하게도 코드를 바꾸었을 때 정상 동작을 확인하는 것은 중요한데요. 네 바로 테스트를 실시해서 확인해봐야 합니다.

     

    먼저 리팩토링에서 첫 번째로 중요한 친구는 '이름 짓기(rename)'입니다. 'test1'같은 변수명은 아무 의미도 없는 변수명임을 알고 있어야 합니다. 그리고 두 번째로 중요한 리팩토링의 친구는 바로 '메서드 추출'입니다. 불필요하게 복잡한 메서드라면 복잡도를 줄여 코드가 무엇을 담당하는지 쉽게 이해할 수 있도록 바꿀 수 있습니다.

     

    IDE에서 제공하는 리팩토링 메뉴를 살펴보는 것은 아주 좋습니다. 코드를 변형하는 데 드는 무수한 시간과 수동으로 변경했을 때 발생하는 실수들을 줄여주는 데에 엄청난 도움을 줍니다.

     

    메서드 동작들을 추출하여 더욱 작은 단위의 메서드들이 많아졌다고 걱정하지 않아도 됩니다. 반복문 몇 개의 메서드가 추가되어 잠재적으로 실행 시간이 몇 배가 되더라도 지저분한 코드를 그대로 두어 가독성과 유지 보수에 많은 비용이 드는 것을 방지할 수 있습니다. 성능에 심각한 문제가 되지 않는다면 어설픈 최적화 노력으로 시간을 낭비하기보다는 코드를 깔끔하게 유지하는 것이 좋습니다.

     

    9장 더 큰 설계 문제

    이번 장에서는 설계 관점에 대해서 이야기하는데요. 여기에서는 단일 책임 원칙에 초점을 맞추는데요. 이는 좀 더 작은 클래스를 만들어 무엇보다 유연성과 테스트 용이성을 높여 줍니다. 많은 책임을 가진 클래스는 코드를 변경할 때 다른 동작들을 깨기 쉽다는 점에서 착안하여 단일 책임 원책을 강조하고 있습니다.

     

    SOLID 클래스의 설계 원칙

    ・단일 책임 원칙(SRP) : 클래스는 변경할 때 한 가지 이유만 있어야 합니다. 클래스는 작고 단일 목적을 추구합니다.

    ・개방 폐쇄 원칙(OCP): 클래스는 확장에 열려 있고 변경에는 닫혀 있어야 합니다. 기존 클래스의 변경을 최소화해야 합니다.

    ・리스코프 치환 원칙(LSP) : 하위 타입은 반드시 상위 타입을 대체할 수 있어야 합니다. 클라이언트 입장에서 오버라이딩한 메서드가 기능성을 깨면 안 됩니다.

    ・인터페이스 분리 원칙(ISP) : 클라이언트는 필요하지 않은 메서드에 의존하면 안 됩니다. 커다란 인터페이스를 다수의 작은 인터페이스로 분할하세요

    ・의존성 역전 원칙(DSP) : 고수준 모듈은 저수준 모듈을 의존해서는 안 됩니다. 둘 다 추상 클래스에 의존해야 합니다. 추상 클래스는 구체 클래스에 의존해서는 안 됩니다. 구체 클래스는 추상 클래스에 의존해야 합니다.

     

    명령 질의 분리

    어떤 값을 반환하고 부작용을 발생시키는 메서드는 명령-질의 분리 원칙을 위반합니다. 이 원칙은 어떤 메서드는 명령을 실행하거나 질의에 대답할 수 있으며, 두 작업을 모두 하면 안 된다는 것입니다.

     

    단위 테스트 유지 보수 비용 줄이기

    ・코드 중복은 가장 큰 설계 문제

    ・단위 테스트가 어려워 보인다면 단순하게 수정

    ・깨진 테스트 고치기 : 코드를 좀 더 직관적이고 단순하게 수정

     

    이렇게 9장의 내용을 알아보았습니다. 설계에 관한 작은 개념들과 작은 코드 리팩토링이 어떻게 커다란 차이를 만들어 내는지에 대해서 이해하셔야 합니다. 다른 프로그래머들의 반발을 살 수 있지만, 그럴만한 가치가 있는 것이 리팩토링입니다. 좀 더 많은 것을 테스트하고 싶지만, 실무에서 코드가 DB등과 상호 작용하기 때문에 항상 단위 테스트를 하는 것은 간단하지 않은데요. 10장에서 목 객체를 도입해 이러한 현실적인 어려움을 극복하는 과정을 설명합니다.

     

    10장 목 객체 사용

    목 객체는 다양한 곳에서 사용될 수 있는데요. 그중 하나가 HTTP 등의 API를 호출하는 곳에서 목 객체를 사용하는 것입니다. 실제 API를 호출하는 대신에 하드 코딩된 JSON 문자열을 반환하게 하는 식이죠. 그리고 그 스텁을 이용해서 테스트를 진행하는데요.

     

    문자열을 반환하는 스텁이라면, 이 문자열이 실제 메서드에 전달되는 URL과 올바르게 상호작용하는지 검증을 하여야 합니다. 이 검증을 통해서 테스트가 제대로 자기 몫을 하기 시작합니다. 그리고 여기에 모키토를 사용하면 스텁을 목으로 확실히 변화시킬 수 있습니다. 모키토는 목 도구로 설계해 놓은 도구입니다. 'when(...). thenReturn(...)' 패턴이 모키토를 사양하는 가장 쉬운 방법으로 알려져 있습니다.

     

    모키토를 사용하면 @Mock 애너테이션으로 목 인스턴스를 생성할 수 있습니다. 

     

    11장 테스트 리팩터링

    사실 테스트 코드는 상당한 투자를 의미합니다. 결함을 최소화하고 리팩토링으로 프로덕션 시스템을 깔끔하게 유지시켜 주지만, 이는 결국 지속적인 비용을 의미하기도 하는데요. 시스템이 변경됨에 따라서 테스트 코드도 다시 확인을 해 봐야 합니다.

     

    이때 불필요한 테스트 코드를 제거하고, 추상화를 도입시켜 테스트를 더욱 단순하게 만들 수 있습니다. 또한 부적절한 정보(매직 리터럴 - 상수로 선언하지 않은 숫자 리터럴)를 제거하며, 쓸데없이 많은 다수의 단언들을 줄일 수 있습니다.

    테스트가 장황하게 늘어나 있다면 준비, 실행, 단언의 과정으로 좀 더 명확하게 테스트를 만들 수 있습니다. 마찬가지로 테스트의 변수와 함수명을 명확하게 테스트를 알 수 있도록 리팩토링 하는 것도 당연하게도 아주 중요합니다. 

     

    마지막으로 읽기 힘들고 지저분한 코드가 널려있던 테스트 코드를 작은 테스트로 나누는 식으로 새로운 테스트를 추가하면 테스트는 더욱 깔끔하고 완벽하게 유지될 수 있습니다.

     

    12장 테스트 주도 개발

    드디어 항상 뜨거운 감자인 TDD를 설명하는 장이 되었습니다. 단위 테스트는 소프트웨어를 어떻게 만들어야 할지에 관한 잘 훈련된 사이클의 핵심적인 부분인데요. 따라서 TDD 채택하면 소프트웨어 설계는 달라지고 더 좋아질 거라고 저자는 이야기하고 있습니다. 

     

    TDD의 주된 이익은 코드가 예상한 대로 동작한다는 자신감을 얻을 수 있다는 것과 구현하는 실질적인 모든 사례에 대해서 단위 테스트를 작성하게 된다는 것입니다. 이는 코드를 지속적으로 발전시킬 수 있는 자유를 줍니다. TDD는 단순하게 시작하는데요. 사이클을 나누면 총 3개의 사이클로 나뉘게 됩니다.

     

    ・실패하는 테스트 코드 작성하기

    ・테스트 통과시키기

    ・이전 두 단계에서 추가되거나 변경된 코드 개선하기

     

    이 동작들을 반복하면서 테스트 주도 개발(TDD)을 진행할 수 있습니다.  테스트-코드-리팩토링을 아주 작은 단계로 계속해서 반복하는 것이 테스트 주도 개발의 핵심이라고 생각하시면 되겠습니다

     

    13장 까다로운 테스트

    모든 것이 단위 테스트를 하기에 쉬운 것은 아닌데요. 특히 스레드와 영속성에 연관된 코드가 그렇습니다. 이를 테스트하기 위해서는 '설계 다시 하기', '목을 사용하여 의존성 끊기'등의 방법이 있습니다.

     

    사실 동시성 처리한 필요한 애플리케이션 코드를 테스트하는 것은 기술적으로 단위 테스트의 영역이 아닙니다. 통합 테스트로 분류하는 것이 낫습니다. 하지만 단위 테스트로 멀티스레드 코드를 테스트할 수 없는 것은 아닙니다. 다음 사항을 따르는 것이 중요한데요.

     

    ・스레드 통제와 애플리케이션 코드사이의 중첩 최소화: 스레드 없이 다량의 애플리케이션 코드를 단위 테스트할 수 있도록 설계를 변경할 것

     

    ・다른 동시성 유틸리티 클래스 사용 : java.util.concurrent 패키지를 활용할 것.(2004년에 나온 자바 5 이후 오랜 시간 충분히 검증받은 클래스) 검증된 BlockingQueue 클래스 등을 사용할 것

     

    다른 까다로운 테스트는 데이터베이스 테스트가 있습니다. 데이터베이스와 연결되어 있는 자료들을 모두 스텁으로 만들어 단위 테스트할 수도 있지만 노력이 많이 들고 테스트도 어려울 것인데요. 이때 데이터베이스와 성공적으로 상호 작용하는 클래스에 대한 테스트를 작성하여 제대로 연결이 되었는지를 따로 테스트하는 것을 추천합니다.

     

    데이터 베이스와 상호 작용하는 통합 테스트를 작성할 때 데이터베이스의 데이터와 그것을 어떻게 가져올지는 매우 중요한 고려 사항입니다. DB에서 기대한 대로 쿼리 결과가 나온다고 증명하려면 먼저 적절한 테이터를 입력하거나 이러한 데이터가 DB에 있다고 가정하여야 하는데요. DB의 데이터가 이미 있다고 가정하여 테스트를 하는 것은 상당히 고통스러운 가정입니다. 테스트 코드와 데이터를 분리시키면 특정 테스트가 왜 통과하거나 실패하는지 그 이유를 이해하기가 더욱 어려워지기 때문입니다. 그럼 어떻게 해야 할까요? 네 테스트 안에서 데이터를 생성하고 관리하셔야 합니다.

     

    먼저 깨끗한 DB로 입력부터 시작하는 것이 좋습니다.(참조 테이블 정도는 놔두어도 좋습니다.) 매 테스트의 전후에 데이터베이스를 비웁니다. 직접적인 데이터베이스와의 상호작용을 테스트하였다면 그 상호작용을 담당하고 있는 Controller를 목으로 처리하여 로직 자체가 올바르게 표현되는지 다시 한번 확인합니다. 목으로 처리한 것은 무엇이고, 그것이 쿼리에  대해 어떻게 반응하고 어떤 부작용을 발생시키는지 충분히 알고 있어야 합니다.

     

    이번장을 요약하면 아래와 같습니다.

    ・애플리케이션 로직은 스레드, 데이터베이스 혹은 문제를 일으킬 수 있는 다른 의존성과 분리할 것

    ・느리거나 휘발적인 코드는 목으로 대체하여 단위 테스트의 의존성을 끊을 것

    ・필요한 경우에는 통합 테스트를 작성하되 단순하고 집중적으로 만들 것

     

    14장 프로젝트에서 테스트

    이제 드디어 마지막 장입니다. 이 장에서는 실제 프로젝트에서 어떻게 끝없는 논쟁과 코드 충돌로 시간을 허비하지 않을지에 대해서 이야기합니다.

     

    먼저 단위 테스트를 항상 빠르게 도입을 해야 한다는 것을 강조합니다. 이를 위해 팀원들과 같은 편이 되어, 왜 단위 테스트를 해야 하는지에 대해서 미리 설명해야 한다고 합니다. 프로젝트 첫날부터 품질을 통제하면서 개발을 해야 되는 이유를 충분히 설명하는 것을 추천하고 있습니다. 그리고 아래의 방법을 사용하기를 추천합니다.

     

    ・단위 테스트 표준 만들기

      -코드를 체크인하기 전에 어떤 테스트를 실행해야 할지 여부

      -테스트 클래스와 메서드의 네이밍 방식

      -햄크레스트 혹은 전통적인 단언 사용 여부

      -AAA사용 여부

      -선호하는 목 도구 선택

      -체크인 테스트를 실행할 때 콘솔 출력 허용 여부

      -단위 테스트에서 느린 테스트를 식별하고 막을 방법

     

    ・리뷰로 표준 준수 높이기

      -사후 리뷰는 적어도 뻔한 표준 위반을 방지하는 관문 역할을 하기에 필요

     

    ・짝 프로그래밍을 이용한 리뷰

      -사후 리뷰보다 훨씬 빠르고 더욱 코드의 완성도가 올라가며 노력의 양은 더 낮음(가성비가 훨씬 좋음)

     

    ・지속적인 통합으로 수렴

      -며칠 혹은 몇 주에 한번 코드를 통합하는 것을 옳지 않다

      -누군가의 컴퓨터에서 동작하는 것이 아닌 전체 코드가 서버에서 동작하는 것을 확인

     

    ・코드 커버리지(단위 테스트가 실행한 코드의 전체 퍼센트)를 얼마나 할 것인가?

      -보통의 팀들은 못 해도 70% 이하의 커버리지는 좋지 않다고 말함

      -TDD(테스트 주도 개발)로 개발하는 팀들은 평균 약 90% 정도의 커버리지를 달성함

      -커버리지를 달성하기 위해 의미가 없는 나쁜 테스트를 작성하는 것은 금물

     

    자바와 JUnit을 활용한 실용주의 단위 테스트 읽은 소감

    단위 테스트를 어떻게 자동화시켜서 실행할 수 있는지 그리고 그것이 왜 좋은지에 대해서 끊임없이 설득하고 상기시켜주는 책입니다. 훌륭한 단위 테스트는 프로그래머의 삶의 질을 올려준다는 이야기에 100% 공감하였습니다. 테스트를 실시할 때 본인이 스스로 테스트를 할 기준을 잘 잡는 것이 중요하다고 생각하는데요. 사실 이 기준이라는 것이 애매합니다. 왜냐하면 각자 생각하는 테스트 기준이 천차만별이기 때문이죠.

     

    예를 들어 11개의 드롭박스 항목을 동시에 선택할 수 있는 페이지 혹은 기능이 있다고 가정을 하면 어떻게 테스트할 건지 테스트 실시 횟수는 몇 회로 할 것인지 사람마다 생각이 다 다를 것입니다. 엔드 유져 테스트(화면상에서 직접 유저의 입장에서 조작하는 방식)로 테스트를 진행한다고 가정하여도 11개의 드롭박스 항목별로 각각 다른 선택을 하는 모든 경우의 수를 전부 테스트하여야 한다고 생각하는 사람과 중요한 부분, 서로 연결되어 있는 부분만 테스트하여야 한다는 사람, 모든 드롭박스를 테스트하는 것은 맞지만, 모든 경우의 수를 테스트할 필요는 없다는 사람 등등으로 정말 사람 바이 사람으로 의견이 나뉘게 되는데요.

     

    이렇게 각자 헛소리를 주장하기 시작할 때 중요한 것은 본인스스로 중심을 잡고 테스트해야 하는 항목을 확실하게 정할 수 있어야 한다는 것입니다. 그리고 그렇게 중심을 잡기 위해서는 테스트를 실시하는 경계, 조건 등에 대해서 확신을 가지고 있어야만 가능한 일이라고 생각하는데요. 저는 그저 그런 개발자라 부족한 부분이 많습니다만, 이 책을 읽고 나서 조금이나마 단위 테스트, 나아가서 테스트 전반에 대해서 조금 더 지식을 쌓았다는 생각이 들었습니다.

     

    테스트에 고통을 받은 적이 있으신 분, 테스트로 인해서 머리를 쥐어뜯어본 적이 있는 개발자, 좀 더 본인의 삶의 질을 높이고 싶은 개발자라면 꼭 한번 읽어 보시는 걸 추천드립니다.

    반응형

    댓글