Java

테스트 코드 작성하기(JUnit, TDD, BDD, Mockito)

beekei 2024. 5. 8. 17:31
반응형

 테스트 코드는 왜 작성할까?

서비스를 만들 때 개발 후 테스트를 한 번에 하면 되는데 왜 중간중간에 테스트 코드를 작성해야 할까?

요즘은 이런 생각을 하시는 개발자분들은 없으실 거라 생각하지만..

대부분에 작은 회사에선 2~3배에 리소스가 발생하기 때문에 테스트 코드를 작성하지 않는 곳이 많거나 개발자가 귀찮아하고 본인의 일을 줄이기 위해 작성하지 않는 분들도 많이 보았습니다...

 

하지만 테스트 코드는 개발한 코드를 추후에 수정하게 될 때 발생하는 사이드 이펙트를 줄일 수 있고 서비스에 안정성뿐만 아니라 개발자 간의 협업을 원활하게 하기도 합니다.

 

코드를 작성하는 것이 개발자의 능력이 아니라,

가독성 좋고 안정성 있게 코드를 작성하고 효율적으로 관리할 수 있는 방법을 공부하고 이해하며 실제 구현을 하는 것이 실력이라고 생각하고 있습니다.

냉정하게 말하면 코드를 작성해서 기능을 만드는 일은 신입 개발자도 모두 가능한 부분이기 때문에..

 

그래서 저는 평소 실무 프로젝트를 진행하면서 어떻게 하면 백엔드 내부에서 안정성을 높일 수 있을지 고민을 많이 하는 편입니다.

그중에 중요하게 생각하는 것 중 하나가 테스트 코드를 어떻게 작성하고, 의미 있게 작성하느냐입니다.

 

몇 가지 테스트 관련 방법론과 테스트 코드를 어떻게 작성하는지 간단한 예시로 기본적인 개념만 정리해 보았습니다.


※ TDD란?

TDD는 Test Driven Development의 약자로 테스트 주도 개발을 의미합니다.
소프트웨어 개발 방법론 중의 하나로 동작하는 코드를 작성하기 이전에 테스트를 먼저 작성하고, 그 테스트를 통과하는 코드를 작성함으로써 테스트된 동작하는 코드를 얻는 개발 방법입니다.

 

TDD는 아래 이미지처럼 red-green-refactoring cycle을 반복적으로 진행하며 개발합니다.

TDD Red-Green-Refactoring Cycle

 

1. 테스트 코드를 작성한다. (red)

이때 실제 작성한 코드가 없기 때문에 실패하게 됩니다.

2. 테스트를 성공시킨다. (green)

테스트 코드가 성공하도록 실제 코드를 작성합니다.

3. 리팩토링을 수행한다. (refactoring)

중복 코드를 제거하거나 가독성 좋게 코드를 리팩토링 합니다.

 

TDD의 장단점

TDD의 장점으로는 테스트 코드를 먼저 작성하고 코드를 작성하기 때문에 개발자는 코드의 동작을 확실히 이해할 수 있고 정확성을 검증할 수 있습니다.
또한 단위 테스트가 가능한 형태로 코드를 작성하기 때문에 종속성을 낮출 수 있으며 이로 인해 유지보수에도 도움이 되고, 테스트 코드가 개발 프로세스의 일부가 되어 다른 개발자들과의 협업이 용이할 수 있습니다.

 

TDD의 단점으로는 폭포수 개발 방법론과 비교했을 때 개발 리소스 비용이 많이 들어갈 뿐만 아니라 복잡한 프로세스의 테스트 코드를 작성할 때는 설계에 대한 이해가 부족할 경우 테스트를 하는 의미가 없거나 미흡하게 작성될 수 있습니다.

또한 프로세스가 자주 바뀌는 서비스에 경우 테스트 코드도 모두 수정해야 하므로 리소스 비용이 증가합니다.

 

TDD 예제

그렇다면 어떻게 TDD로 구현하는지 간단한 예제를 통해 작성해 보겠습니다.

Java로 개발하고 계신다면 보통 junit을 이용할 것입니다.

1. 테스트 코드를 작성한다. (red)

예제에서는 계산기 클래스에서 두 가지 숫자와 세 가지 숫자를 더하는 로직에 대한 테스트 코드를 작성하였습니다.

이때 Calculator 클래스에 plus 메서드를 생성하고 내부 구현 코드는 작성하지 않은 상태입니다.

public class Calculator {

    public int plus(int number1, int number2) {
        return 0;
    }

    public int plus(int number1, int number2, int number3) {
        return 0;
    }

}

@Test
@DisplayName("더하기 테스트")
public void plusTest() {

    Calculator calculator = new Calculator();

    int calculatedResult1 = calculator.plus(1, 2);
    assertThat(calculatedResult1).isEqualTo(3);

    int calculatedResult2 = calculator.plus(1, 2, 3);
    assertThat(calculatedResult2).isEqualTo(6);
    
}

테스트를 실행해 보면 당연하게도 테스트에 실패하게 됩니다.

 

2. 테스트를 성공시킨다. (green)

이제 Calculator 클래스에 plus 메서드를 구현해 테스트에 통과하는 최소한에 로직을 구현합니다.

public class Calculator {

    public int plus(int number1, int number2) {
        return number1 + number2;
    }

    public int plus(int number1, int number2, int number3) {
        return number1 + number2 + number3;
    }

}

 

다시 한번 테스트를 실행해보면 이번에는 테스트에 성공하였습니다.

 

3. 리팩토링을 수행한다. (refactoring)

마지막 사이클인 리팩토링을 수행합니다.

public class Calculator {

    public int plus(int ... numbers) {
        return Arrays.stream(numbers).reduce(0, Integer::sum);
    }

}

테스트 코드는 그대로 둔 채 Calculator 클래스에 두 개의 plus 메서드를 가변인자를 통해 하나의 메서드로 만들어 중복 코드를 제거하였습니다.

다시 한번 테스트를 실행해 보면 이번에도 테스트에 성공하였습니다.

 

TDD는 크게 어려운 부분이 없습니다. 하지만 많은 분들이 TDD를 알고 계시지만 실제 개발을 할 때 적용시키기 어려워합니다.

습관이 되지 않았다면 실제 코드 작성에 먼저 손이 가기 마련이고 습관을 들이는 것은 그렇게 쉽지만은 않을뿐더러, 

테스트 케이스 별로 작성할 코드의 양이 많아졌기 때문에 개발 리소스 비용에도 부담이 커지게 됩니다...

 

하지만! 추후에는 수동 테스트에서의 리소스 비용을 줄일 수 있고 이미 작성된 요구사항이나 기획서가 존재한다면 테스트 케이스를 작성하는데 리소스 비용이 줄어들 것입니다.

이 관점으로 TDD에서 파생된 것이 BDD입니다.


 BDD란?

BDD는 Behavior Driven Development의 약자로 행동 주도 개발을 의미합니다.

사용자 관점 요구사항을 테스트 코드로 작성해 구현하여 고객과 개발자의 관점에서 시스템이 어떻게 작동해야 하는지에 초점을 맞추게 됩니다.

BDD Cycle

얼핏 보면 TDD와 비슷하지만 TDD는 단순히 정해진 로직을 중점으로 테스트 코드를 작성하지만 BDD는 사용자의 행동을 중점으로 테스트 시나리오를 작성합니다.

테스트 시나리오는 Given-When-Then Pattern으로 개발자가 아닌 사람이 봐도 이해할 수 있는 정도의 레벨을 권장합니다.

BDD의 행동 중심 테스트 시나리오 안에 TDD를 포함하고 있는 형태라고 이해하시면 될 것 같습니다.

TDD와 마찬가지로 해당 사이클을 반복적으로 진행하며 개발합니다.

 

Scenario

ex) 계산기를 이용해 1에 1을 더하면 2가 나온다.

1. Given : 주어진 상황이나 환경

ex) 계산기를 이용해 1에

2. When : 사용자에 행위

ex) 1을 더하면

3. Then : 기대한 결과

ex) 2가 나온다.

 

BDD의 장단점

BDD에 장점으로는 기획서에 기반하여 테스트 코드를 작성하기 때문에 코드 작성 이전에 서비스 기획 및 설계에 모순을 발견할 가능성이 높습니다.

또한 개발자는 요구사항을 중점으로 개발하기 때문에 서비스에 대한 이해도가 높아지고, 비개발자와의 소통과 협업에 도움이 될 수 있습니다.


BDD에 단점으로는 테스트 케이스를 만들어 줄 QA의 존재가 필요하며 기획자의 시나리오가 중요해집니다.

만약 기획자의 시나리오가 실제 요구사항과 다를 경우 개발을 처음부터 시작해야 할 수도 있습니다.

또한 BDD에 사용한 메서드에 대한 검증이 필요해지므로 테스트 케이스를 작성하는 리소스 비용은 줄일 수 있지만, 코드를 작성하는 양은 TDD보다 증가할 수 있습니다.

 

BDD 예제

위 TDD 예제에서 작성한 코드로 이어서 BDD 예제를 작성해 보겠습니다.

@Test
@DisplayName("계산기를 이용해 1에 1을 더하면 2가 나온다.")
public void plusTest() {
    // given
    Calculator calculator = new Calculator();
    int number1 = 1;

    // when
    int calculatedResult = calculator.plus(number1, 1);

    // then
    assertThat(calculatedResult).isEqualTo(2);
}

계산기를 이용해 1에 1을 더하면 2가 나온다.라는 시나리오를 Given-When-Then Pattern으로 작성하였습니다.

예제를 보시면 시나리오는 통과하였지만 시나리오에서 사용한 Calculator 클래스에 plus라는 메서드도 검증을 거쳐야 합니다.

 

여기서 확인할 수 있는 부분은 프로젝트를 BDD와 TDD 중 하나를 선택하여 개발하는 것이 아닌,

코드를 작성한 관점에 따라 코드마다 다른 방법론을 채택하여 개발해야 한다는 것 입니다.

 

저는 보통 DDD Architecture 계층 중 Presentation, Application 계층에선 BDD 방법론을 채택하여 개발하고 있고 Domain과 Infra 계층에서는 TDD 방법론으로 개발을 진행하고 있습니다.

 Mockito를 활용하여 테스트 관심도 분리하기

만약 위 예제에서 Calculator 클래스에 plus라는 메서드가 개발자의 실수로 덧셈이 아닌 뺄셈이 되었다 생각해 봅시다.

public class Calculator {

    public int plus(int ... numbers) {
        // 개발자의 실수로 뺄셈으로 바뀜;;
        return Arrays.stream(numbers).reduce(0, (a, b) -> a - b);
    }

}

@Test
@DisplayName("계산기를 이용해 1에 1을 더하면 2가 나온다.")
public void plusTest() {
    // given
    Calculator calculator = new Calculator();
    int number1 = 1;

    // when
    int calculatedResult = calculator.plus(number1, 1);

    // then
    assertThat(calculatedResult).isEqualTo(2); // 테스트 실패!!
}

 

테스트 시나리오는 변경되지 않았지만 한 가지 실수 때문에 해당 메서드를 사용하는 모든 테스트 코드가 실패하게 됩니다. 

이처럼 간단한 로직은 단숨에 확인이 가능하지만, 복잡한 로직에서는 열심히 어디서부터 잘못된 것인지 찾아야 하는 쓸대없는 리소스도 발생할 수 있습니다.

 

 

이러한 문제를 예방하기 위해 Mockito를 활용합니다.

@Test
@DisplayName("계산기를 이용해 1에 1을 더하면 2가 나온다.")
public void plusTest() {
    // given
    Calculator calculator = Mockito.mock(Calculator.class); // 계산기 클래스 Mocking
    Mockito.when(calculator.plus(1, 1)).thenReturn(2); // 더하기 메서드 Mocking

    // when
    int calculatedResult = calculator.plus(1, 1);

    // then
    assertThat(calculatedResult).isEqualTo(2);
}

 

여기서 주의할 점!

계산기 클래스를 Mocking 하게 되면 테스트를 하는 의미가 없는 것이 아니냐!? 하는 의문점이 있으실 겁니다.

하지만 해당 테스트 코드는 "계산기를 이용해 1에 1을 더하면 2가 나온다."라는 시나리오를 테스트하는 것에 목적을 둡니다.

plus 메서드에 덧셈에 대한 테스트는 따로 단위 테스트로 진행하여(실패하겠죠?) 예제 시나리오는 테스트에 성공해야 할 것 입니다.

이처럼 테스트 로직을 분리하여 테스트 코드에 대한 종속성을 낮추어 테스트의 목적을 확실히 할 수 있습니다.

 

저는 보통 DDD Architecture 계층 중 Application 계층이나 Domain 계층에 테스트를 진행할 때 직접 DB를 조회하지 않고 Mocking을 이용해 테스트 데이터를 만들어서 사용하고 있습니다.
그렇기 때문에 실제 DB에 데이터가 존재하지 않아도 해당 테스트 코드에서는 문제가 되지 않습니다.

DB를 조회하는 테스트도 H2 Database를 활용하여 직접 데이터를 등록하지 않고 테스트를 하고 있습니다.

 마치며

사실 인터넷에 워낙 잘 정리해 주신 글들이 많기 때문에 정리는 하지 않으려 했지만
희미한 기억 속에 잊히지 않고 있던 테스트 방법론에 대해 다시 한번 생각하고 테스트 코드 작성에 익숙해지기 위해 블로그 글을 정리해 보았습니다.

추후에 예제가 아닌 제가 실제 테스트 코드를 작성하는 방식을 따로 정리하여 공유하도록 하겠습니다!!

반응형