본문 바로가기

TEST CODE

[Junit] Test Code 와 TDD

프로그래밍에는 정답이 없습니다. 더 나은 방법이 있다면 언제든 공유 부탁드립니다.
Next Step 에서 제공하는 TDD-클린코드 교육 내용 기반으로 작성하였습니다.

TEST 코드를 작성해야하는 이유

  • 개발자는 대부분의 시간을 디버깅하는 시간에 투자합니다.
  • 사람이 많아지면 많아질 수록, 기능이 많아지면 많아질 수록 테스트해야되는 코드느 방대해지고, 연관성도 많아지게 됩니다.
  • 작은 하나의 기능의 추가로 인해 전체의 테스트를 다시 해봐야 한다는 것은 엄청난 비용 손해입니다. 하지만, 잘 짜여진 테스트 코드만 있다면 한번만 실행 후 마음 편하게 추가할 수 있습니다.
  • 테스트 코드의 가장 필요한 이유 중 하나는 리펙토링입니다. 리펙토링의 중요한 이유는 다들 알고 있을 것이다. 그 당시에 아무리 좋은 디자인 패턴을 짰다고 하더라도, 시간이 거듭할 수록 코드는 노후화되고 시대에 뒤쳐질 수 밖에 없습니다. 레거시를 보면서 안타깝게 느끼는 점도 그러한 이유입니다.
  • 따라서 리펙토링을 진행해야되는데 만약 테스트코드가 존재하지 않다면, 리펙토링은 매우 힘든 작업이 된다. 리펙토링은 기존에 잘 동작하는 소스를 변경하는 작업이라, 만약 리펙토링의 사유로 기능이 동작하지 않게 된다면 그것은 어떠한 이유로라도 손해인 것입니다. 테스트든, 리펙토링이든 기능이 잘 동작한다는 것이 최우선이기 떄문입니다. 따라서 소스를 리펙토링하는 시간보다 기능 테스트 하는 시간이 더 요구된다면 그것은 손해인 셈인 것입니다. 하지만, 만약 기능을 검증해주는 테스트코드가 있다면 리펙토링을 마음 편히 할 수 있을 것이고 그러한 문화가 유지된다면 코드가 노후화 되지 않고 발전해나가는 형태가 유지될 수 있을 것입니다.

참고사이트

객체지향 생활 체조 원칙은 소트웍스 앤솔러지 책에서 다루고 있는 내용으로 객체지향 프로그래밍을 잘 하기 위한 9가지 원칙을 제시하고 있습니다.
이 책에서 주장하는 9가지 원칙은 다음과 같습니다.

규칙 1 : 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
규칙 2 : else 예약어를 쓰지 않는다.
규칙 3 : 모든 원시값과 문자열을 포장한다.
규칙 4 : 한 줄에 점을 하나만 찍는다.
규칙 5 : 줄여쓰지 않는다(축약 금지).
규칙 6 : 모든 엔티티를 작게 유지한다.
규칙 7 : 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
규칙 8 : 일급 콜렉션을 쓴다.
규칙 9 : 게터/세터/프로퍼티를 쓰지 않는다.

단계별 개발 문화

1단계 : svn, git 등을 활용해 코드 버전 관리

1단계 : svn, git 등을 활용해 코드 버전 관리
2단계 : 이슈 관리 시스템을 통한 기능 및 일정 관리

1단계 : svn, git 등을 활용해 코드 버전 관리
2단계 : 이슈 관리 시스템을 통한 기능 및 일정 관리
3단계 : 지속적 통합 도구를 활용한 피드백 환경

1단계 : svn, git 등을 활용해 코드 버전 관리
2단계 : 이슈 관리 시스템을 통한 기능 및 일정 관리
3단계 : 지속적 통합 도구를 활용한 피드백 환경
4단계 : 코드리뷰 - 온라인 또는 짝 프로그래밍

1단계 : svn, git 등을 활용해 코드 버전 관리
2단계 : 이슈 관리 시스템을 통한 기능 및 일정 관리
3단계 : 지속적 통합 도구를 활용한 피드백 환경
4단계 : 코드리뷰 - 온라인 또는 짝 프로그래밍
5단계 : 지속적 배포

Refactoring

참고사이트

TDD vs Unit Test

TDD란 프로그래밍 의사결정과 피드백 사이의 간극을 의식하고 이를 제어하는 기술이다.
켄트벡, Test Driven Development by Example 중
TDD의 아이러니 중 하나는 테스트 기술이 아니라는 점이다. TDD는 분석 기술이며, 설계 기술이기도 하다.
켄트벡, Test Driven Development by Example 중

  • TDD = TFD(Test First Development) + 리팩토링
  • 실패하는 테스트를 구현한다.
  • 테스트가 성공하도록 프로덕션 코드를 구현한다.
  • 프로덕션 코드와 테스트 코드를 리팩토링한다.
  • 테스트 주도 개발과 지속적인 리팩터링
  • 테스트 주도 개발과 지속적인 리팩터링은 프로그래밍을 다음과 같은 대화로 바꿔, 기존 코드를 효율적으로 발전시킬수 있도록 한다

질문. 테스트를 작성함으로써 시스템에 질문한다
대답. 테스트를 통과하는 코드를 작성해 질문에 대답한다
정제. 아이디어를 통합하고, 불필요한 것은 제거하고, 모호한 것은 명확히 해서 대답을 정제한다.
반복. 다음 질문을 물어 대화를 계속한다.
TDD와 지속적인 리팩터링에 대해 켄트 벡이 내건 슬로건 빨강, 초록, 리팩터링 이다.

빨강. 코드가 해야 할 일을 예상하고 이것을 나타내는 테스트를 작성한다. 테스트를 통과하는 코드를 아직 작성하지 않았기 때문에 테스트는 실패할것이다.
초록. 테스트를 통과하도록 임시방편으로라도 프로그램을 작성한다. 이 단계에서는 코드 중복, 단순함, 명확한 설계 같은 것을 고민할 필요가 없다. 그런 설계는 나중에 모든 테스트를 통과한 후 더 좋은 설계를 맘 편하게 테스트할 수 있는 단계가 되면 그 때에 가서 생각할 일 이다.
리팩터링. 테스트를 통과한 코드의 설계를 개선한다.

  • TDD 원칙

원칙 1 - 실패하는 단위 테스트를 작성할 때까지 프로덕션 코드(production code)를 작성하지 않는다.
원칙 2 - 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
원칙 3 - 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

테스트 코드의 장단점

장점

  • 테스트 코드가 없는 경우
    • 사전에 검증이 끝난 부분도 재 테스트를 해야하는 사항 (전체 테스트코드가 돌아가는 모습을 보여줘야 함)
    • 간단하게 예외처리를 해야하는 부분이나 단순한 로직을 새로 추가할 때 테스트 코드만 실행 시켜서 테스트해 볼 수 있음.
    • 테스트코드가 없는 경우에는 다시 빌드해서 추가한 기능이 실행될 때 까지 로직을 흘려야함

단점

  • 테스트 코드 작성에 대한 비용이 불가피함
  • 가장 쉽게 할 수 있는 테스트의 형태가 unit test 형태인데, unit test 를 전부 할 수도 없을 뿐더러 전부 한다해서 오류가 발생하지 않는다는 보장이 없음
  • Intergration, E2E 테스트의 형태는 로직의 전체 흐름을 점검할 수 있는 테스트이지만 비용이 많이 요구됨

참고사이트

객체지향적 개발이란

  • SOLID 란? - SOLID 관련 링크

    SRP (단일책임의 원칙: Single Responsibility Principle)

    • 작성된 클래스는 하나의 기능만 가지며 클래스가 제공하는 모든 서비스는 그 하나의 책임(변화의 축: axis of change)을 수행하는 데 집중되어 있어야 한다

    OCP (개방폐쇄의 원칙: Open Close Principle)

    • 소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려있고, 변경에는 닫혀있어야 한다.

    LSP (리스코브 치환의 원칙: The Liskov Substitution Principle)

    • 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다. 즉, 서브 타입은 언제나 기반 타입과 호환될 수 있어야 한다.

    ISP (인터페이스 분리의 원칙: Interface Segregation Principle)

    • 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.

    DIP (의존성역전의 원칙: Dependency Inversion Principle)

    • 구조적 디자인에서 발생하던 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의 역전 원칙이다.
  • ENUM을 사용하는 이유 - OCP 원칙

    • 확장에는 열려있고, 변화에는 닫혀 있어야합니다.
    • 어떤 기능을 추가할 때 중심이 되는 로직은 변경이 되지 않아야 하며, ENUM 같은 인터페이스나 상속같은 중심 객체는 변경 없이 외부의 객체의 변경으로만 확장이 가능해야 합니다.
  • 테스트 코드를 작성하는 것은 사실상 로직에 비하면 크게 어렵지 않습니다. 약간의 학습만 있다면 쉽게 구현해 낼 수 있습니다.

    • 하지만 테스크 코드를 구현하기 위한 환경을 만드는 것은 어렵습니다. 이러한 구조의 형태는 테스트 코드를 작성하기 매우 어려워진다. 따라서 테스트코드를 구현하기 위한 환경을 만드는 것이 중요합니다. (생활 체조의 원칙 8가지)
    • OCP를 지킨다는 것은 SOLID 원칙의 중요한 여러가지를 지키고 있다는 것이 됩니다. 이러한 원칙은 사실상 지키기 매우 어렵습니다. 하지만 이러한 ENUM을 사용한 예시만 지킬 수 있다면 테스트코드도 매우 작성하기 쉬워지고 (ENUM의 값만 확인해주면됨) 확장성(다형성 등등) 지키기 매우 쉬워집니다.

Enum

장점

  • 문자열과 비교해, IDE의 적극적인 지원을 받을 수 있습니다.
  • 자동완성, 오타검증, 텍스트 리팩토링 등이 쉬워집니다.
  • 허용 가능한 값들을 제한할 수 있습니다.
  • 리팩토링시 변경 범위가 최소화 됩니다.
  • 내용의 추가가 필요하더라도, Enum 코드외에 수정할 필요가 없습니다.
  • Java 에서 Enum 은 인터페이스를 기반으로 구성되어 있기 때문에 모든 클래스의 기능을 사용할 수 있습니다.(메서드 생성 등)

ENUM 사용 예시

  • 새로운 Ranking이 추가되어도 profic을 가져오는 핵심로직은 변경되지 않고 Ranking을 추가해주면 됩니다. 즉, 확장에는 열려있고 변화에는 닫혀있게 됩니다.

Enum 테스트 코드

참고사이트

assertJ

  • 메소드 체이닝을 통해 좀 더 깔끔하고 읽기 쉬운 assert 코드 구현 가능
  • 기존의 Junit 에서 제공하는 검증 코드는 ',' 형태로 비교 값과 구분을 하기 때문에 식별하기가 어려움이 존재함
  • 반면에, assertJ 는 메소드 체이닝을 통해 좀 더 깔끔하고 읽기 쉬운 assert 코드 구현 가능

예시 1

import static org.assertj.core.api.Assertions.*;


assertThat(frodo.getName()).isEqualTo("Frodo");
assertThat(frodo).isNotEqualTo(sauron);


assertThat(frodo.getName()).startsWith("Fro")
.endsWith("do")
.isEqualToIgnoringCase("frodo");


assertThat(fellowshipOfTheRing).hasSize(9)
.contains(frodo, sam)
.doesNotContain(sauron);

예시 2

assertThat("Hello, world! Nice to meet you.") // 주어진 "Hello, world! Nice to meet you."라는 문자열은
.isNotEmpty() // 비어있지 않고
.contains("Nice") // "Nice"를 포함하고
.contains("world") // "world"도 포함하고
.doesNotContain("ZZZ") // "ZZZ"는 포함하지 않으며
.startsWith("Hell") // "Hell"로 시작하고
.endsWith("u.") // "u."로 끝나며
.isEqualTo("Hello, world! Nice to meet you.");

assertThat(3.14d) // 주어진 3.14라는 숫자는
.isPositive() // 양수이고
.isGreaterThan(3) // 3보다 크며
.isLessThan(4) // 4보다 작습니다
.isEqualTo(3, offset(1d)) // 오프셋 1 기준으로 3과 같고
.isEqualTo(3.1, offset(0.1d)) // 오프셋 0.1 기준으로 3.1과 같으며
.isEqualTo(3.14); // 오프셋 없이는 3.14와 같습니다

예시 3


import static org.assertj.core.api.Assertions.*;


assertThat(frodo.getName()).isEqualTo("Frodo");
assertThat(frodo).isNotEqualTo(sauron);


assertThat(frodo.getName()).startsWith("Fro")
.endsWith("do")
.isEqualToIgnoringCase("frodo");


assertThat(fellowshipOfTheRing).hasSize(9)
.contains(frodo, sam)
.doesNotContain(sauron);


assertThat(frodo.getAge()).as("check %s's age", frodo.getName()).isEqualTo(33);


assertThatThrownBy(() -> { throw new Exception("boom!"); }).hasMessage("boom!");

Throwable thrown = catchThrowable(() -> { throw new Exception("boom!"); });
assertThat(thrown).hasMessageContaining("boom");


assertThat(fellowshipOfTheRing).extracting("name")
.contains("Boromir", "Gandalf", "Frodo", "Legolas")

assertThat(fellowshipOfTheRing).extracting(TolkienCharacter::getName)
.doesNotContain("Sauron", "Elrond");


assertThat(fellowshipOfTheRing).extracting("name", "age", "race.name")
.contains(tuple("Boromir", 37, "Man"),
tuple("Sam", 38, "Hobbit"),
tuple("Legolas", 1000, "Elf"));


assertThat(fellowshipOfTheRing).filteredOn("race", HOBBIT)
.containsOnly(sam, frodo, pippin, merry);

assertThat(fellowshipOfTheRing).filteredOn(character -> character.getName().contains("o"))
.containsOnly(aragorn, frodo, legolas, boromir);


assertThat(fellowshipOfTheRing).filteredOn(character -> character.getName().contains("o"))
.containsOnly(aragorn, frodo, legolas, boromir)
.extracting(character -> character.getRace().getName())
.contains("Hobbit", "Elf", "Man");

참고사이트

MOCKITO

In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways. A programmer typically creates a mock object to test the behavior of some other object, in much the same way that a car designer uses a crash test dummy to simulate the dynamic behavior of a human in vehicle impacts.

장점

  • 테스트 대상 코드 격리
  • 테스트 속도 개선
  • 예측 불가능한 실행 요소 제거
  • 특수한 상황 테스트 가능
  • 감춰진 정보를 확인 가능

단점

  • Mock을 많이 사용해서 테스트코드를 작성하다보면 무시되어지는 로직이 많아지기 때문에 추후에 정상 동작 시 예상치 못한 문제가 발생할 수 있음
  • Database의 의존성을 제외한 것에 Mock을 사용해야하는 상황이란 것은 의존성 주입이 많이 주입된 상황이라는 뜻
  • 즉, 설계나 작성된 코드에 대해 다시 생각해 볼 필요가 있음

참고사이트


테스트 코드 작성 예시

  1. MVC 형태의 로직에서 Service 로직을 테스트하는 경우

    • Spring 에서 MVC 로직을 구현하고 있다면, 가장 쉽게 접할 수 있는 테스트코드 형태입니다.
    • CRUD 에 대한 로직을 간단하게 테스트하는 형태입니다.
  2. Service Layer 테스트코드를 추가할 경우 데이터 저장을 위한 Features를 구성하는 형태

    • 테스트코드를 작성하기 위해서는 테스트로직을 실행시키기 위한 데이터가 필요합니다.
    • 하지만, 이런 데이터들이 많아지게 되면 서비스 로직의 가독성이 현저히 줄어드는 문제가 발생합니다.
    • 이런 문제를 해결하기 위해 Features 형태의 package 를 추가하여 관리하게 되면 Service Layer 와 Data Layer 를 분리할 수 있습니다.

  1. 날짜 변환하는 서비스에 따른 로직 테스트 (경계값 테스트)

    • 테스코드의 경우 날짜에 대한 값을 테스트해야하는 경우가 빈번이 발생합니다.
    • 하지만 날짜의 경우 예외의 케이스(2월이나 30 - 31일)가 존재하기 때문에 주의깊은 테스트가 필요합니다.
  2. 예외처리에 대한 테스트코드

    • 테스트코드 중 중요하다고 생각하는 부분이 예외처리에 대한 테스트코드입니다.
    • 예외는 예측하지 못하는 부분도 있기 때문에 발생하는 순간마다 예외에 대한 테스트코드 추가가 필요합니다.