티스토리 뷰

728x90
반응형

지난 글까진 만들어진 애플리케이션에 트랜잭션을 적용해 보았다.

 

2022.09.05 - [개발/Spring] - [Spring]트랜잭션(Transaction)

2022.09.06 - [개발/Spring] - [Spring]트랜잭션(Transaction) - @Transactional

 

[Spring]트랜잭션(Transaction) - @Transactional

지난 글에서 트랜잭션의 정의와 규칙, 구체적인 WorkFlow에 대해 알아보았다. 2022.09.05 - [개발/Spring] - [Spring]트랜잭션(Transaction) [Spring]트랜잭션(Transaction) 트랜잭션이란 더 이상 쪼갤 수 없는 일..

gnidinger.tistory.com

이번 글부터는 에러 발생을 최소화하기 위한 애플리케이션 테스트에 대해 알아보자.

 

테스트란 한 마디로 말하면 일정 기준을 정한 뒤 특정 대상이 그것을 충족시키는지 아닌지를 검증하는 과정이다.

 

이전까지의 글에서 테스트란 애플리케이션을 실행시키고 Postman을 통해 요청을 보낸 후 로그를 확인하는 식이었다.

 

쉽게 말해 애플리케이션 전체에 대한 테스트를 진행했다는 뜻인데,

 

매번 이런 식으로 테스트를 진행하면 일단 진행 자체에 시간이 많이 들고 정확한 에러를 찾는데도 힘이 든다.

 

그렇다면 계층별, 혹은 클래스나 개별 메서드 별로 테스트하는 방법은 뭐가 있을까?

 

많은 방법 중에 이번 글에선 가장 작은 단위의 기능을 테스트 하는 방법을 먼저 살펴본다.

 

Unit Test

 

여러 기준이 있겠지만, 소프트웨어 테스트는 시간 순서에 따라 대략 아래와 같이 구분할 수 있다.

 

그래프의 위로 갈수록 드는 시간과 돈, 다루는 컴포넌트의 개수가 늘어나는 것을 확인할 수 있다.

 

반면 테스트 속도와 고립성, 횟수는 아래로 내려올수록 증가하는데,

 

우리는 가장 아래에 위치한 단위 테스트(Unit Test)에 대해 알아본다.

 

위 삼각형에 표시된 통합 테스트(Functional Test)를 시간 순이 아닌 계층 순으로 나타내면 아래와 같은 그림이 된다.

 

 

Functional Test

 

기능 테스트는 주로 사용자의 입장에서 가장 큰 단위의 기능을 다룬다.

 

개발자가 직접 진행하기도 하지만 보통은 테스트 전분 부서나 외부 QA(Quality Assurance) 업체가 맡는다.

 

클라이언트부터 DB, 외부 서비스까지 광범위하게 연관되어 있기 때문에 단위 테스트라고는 부르기 힘들다.

 

Integration Test

 

통합 테스트는 주로 개발자 혹은 개발팀이 코드를 실행시켜 다룬다.

 

예를 들면 클라이언트 측 도구 없이 Controller API를 호출해 DB에 접속 및 동작을 잘하는지 테스트하는 것이다.

 

클라이언트와는 연관되어 있지 않지만 DB가 연결되어 있어서 독립적인 테스트를 할 수 없기 때문에

 

이 경우에도 단위 테스트라고 부르기는 힘들다.

 

Slice Test

 

슬라이스 테스트는 애플리케이션을 계층으로 쪼개서 진행하는 테스트를 말한다.

 

위 그림에선 API, Service, Data Access 계층이 각각 슬라이스 테스트의 대상이 된다.

 

충분히 작은 단위라고 볼 수도 있지만 여전히 HTTP 요청이나 DB 연결이 필요하기 때문에

 

단위 테스트라고 볼 수는 없다.

 

Unit Test

 

서비스 계층은 애플리케이션의 핵심인 비즈니스 로직을 구현하는 계층이다.

 

일반적으로 이 비즈니스 로직에서 사용하는 클래스가 독립적인 테스트에 적합하기 때문에 단위 테스트의 대상으로 불린다.

 

그중에서도 클래스의 기능을 담당하는 파트, 즉 메서드 단위로 테스트를 진행하는 것이 보통이다.

 

DB와의 연결이 있는 경우에도 멱등성이 보장된다면 단위 테스트라 부르기도 한다.

 

Advantages of Unit Test

 

  • IDE 실행 및 Postman을 통한 HTTP 요청 생략 가능 → 결과를 빠르게 확인할 수 있음
  • 큰 단위의 테스트보다 비교적 빠르게 문제의 원인을 파악할 수 있음
  • 리팩터링이 쉬워짐

 

F.I.R.S.T Principles

 

단위 테스트를 위한 테스트 케이스를 작성하기 위해선 몇 가지 원칙이 있다.

 

이를 묶어서 FIRST Principles라 부르며, 대략적인 내용은 아래와 같다.

 

테스트 케이스(Test Case) - 하나의 단위를 테스트하기 위해 작성한 테스트 코드

 

  • Fast - 테스트 케이스는 빨라야 한다.
  • Independent/Isolation - 테스트 케이스는 실행 순서에 독립적이어야 한다.
  • Repeatable - 테스트 케이스는 어떤 환경에서도 반복해서 작동, 같은 결과를 내보내야 한다.
  • Self-Validating - 테스트 케이스는 자체적으로 검증 결과를 판단할 수 있어야 한다.
  • Timely - 테스트 케이스는 기능 구현 직전에 작성해야 한다. 혹은 기능의 업데이트에 맞춰 즉시 개선해야 한다.

 

Unit Tests without JUnit

 

계속해서 JUnit을 사용하지 않고 단위 테스트에 대한 테스트 케이스를 작성해 보자.

 

위 그림에서도 표시했듯이 단위 테스트를 가장 쉽게 적용할 수 있는 클래스는 헬퍼(Helper)와 유틸(Utility)이다.

public class StampCalculator {
    public static int calculateStampCount(int nowCount, int earned) {
        return nowCount + earned;
    }
}

헬퍼 패키지에 생성한 위 코드는 보유한 스탬프와 회원이 획득한 스탬프를 합산해주는 헬퍼 클래스이다.

 

단위 테스트 예제를 목적으로 만들어 단 하나의 메서드만 가지고 있는 위 클래스에 대해 검증 코드를 작성해보자.

public class StampCalculatorTestWithoutJUnit {
    public static void main(String[] args) {
        calculateStampCountTest();
    }

    private static void calculateStampCountTest() {
        // given
        int nowCount = 5;
        int earned = 3;

        // when
        int actual = StampCalculator.calculateStampCount(nowCount, earned);

        int expected = 7;

        // then
        System.out.println(expected == actual);
    }
}

테스트 대상이 되는 StampCalculator.calculateStampCount() 메서드에 주어진 값은 5와 3이고,

 

기대하는 값(expected)에는 7을 지정했다.

 

코드를 실행해보면 틀린 값인 7을 기대했기 때문에 false를 출력하는 것을 확인할 수 있다.

 

주석으로 쓰인 Given-When-Then은 BDD(Behavior Driven Development)에서 사용하는 용어로, 설명은 아래와 같다.

 

  • Given - 테스트를 위한 준비과정 및 전제조건. 전달되는 테스트 데이터를 포함한다.
  • When - 테스트할 대상과 동작 지정
  • Then - 테스트의 결과를 검증하는 영역. 예상 값과 결과를 비교하는 검증(Assertion) 코드가 포함된다.
  • 즉 A 상태에서 출발한(Given) 테스트 대상은, 어떤 상태 변화를 가했을 때(When) 기대하는 상태로 완료되어야 한다(Then).

 

Assertion

 

Assertion은 애플리케이션의 특정 포인트에 대한 boolean 값이다.

 

테스트 케이스의 결과를 참/거짓으로 나타낸다고 생각하면 편하다.

 

대표적인 프레임워크로 JUnit이 있으며, 다양한 라이브러리를 지원한다.

import com.codestates.order.entity.Order;

public class StampCalculator {
    public static int calculateStampCount(int nowCount, int earned) {
        return nowCount + earned;
    }
    
    public static int calculateEarnedStampCount(Order order) { // 추가
        return order.getOrderCoffees().stream()
                .map(orderCoffee -> orderCoffee.getQuantity())
                .mapToInt(quantity -> quantity)
                .sum();
    }
}

테스트 대상 클래스에 주문 정보에서 스탬프 개수를 얻어내는 메서드를 추가했다.

 

추가 테스트를 작성해 보자.

...

public class StampCalculatorTestWithoutJUnit {
    public static void main(String[] args) {
        calculateStampCountTest();
    }
    ...

    private static void calculateEarnedStampCountTest() { // 추가
        // given
        Order order = new Order();
        OrderCoffee orderCoffee1 = new OrderCoffee();
        orderCoffee1.setQuantity(3);

        OrderCoffee orderCoffee2 = new OrderCoffee();
        orderCoffee2.setQuantity(5);

        order.setOrderCoffees(List.of(orderCoffee1, orderCoffee2));

        int expected = orderCoffee1.getQuantity() + orderCoffee2.getQuantity();

        // when
        int actual = StampCalculator.calculateEarnedStampCount(order);

        // then
        System.out.println(expected == actual);
    }
}

추가한 메서드에 대한 테스트 케이스이다.

 

  • given - Order와 OrderCoffee 객체를 만들어 테스트용 데이터를 생성
  • when - 테스트 대상 메서드에 given에서 생성한 테스트 데이터 전달
  • then - 기대와 테스트 값이 같은지 검증(Assertion)

코드를 실행하면 다음과 같은 결과가 콘솔에 출력된다.

false
true

이와 같이 하나의 테스트 클래스에서 여러 개의 테스트 케이스를 실행할 수 있다.

 

또한 두 개의 테스트의 순서를 바꿔 사용하더라도 각각의 결과는 바뀌지 않는데,

 

이는 앞서 살펴본 FIRST Principles를 잘 따르는 결과라고 할 수 있다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함