본문 바로가기
테스트

JUnit 기본

by 이상한나라의개발자 2024. 1. 3.

JUnit는 테스트 프레임워크 입니다. 테스트 주도 개발(TDD) 및 행동 주도 개발(BDD) 접근 방식에 맞춰 개발된 소프트웨어의 작은 부분(유닛)을 테스트하기 위해 사용됩니다.

  • 단위 테스트 : JUnit은 개별 클래스와 메서드에 대한 단위 테스트를 작성하고 실행할 수 있습니다.
  • 어노테이션 기반 : JUnit은 테스트 메서드를 정의할 때 어노테이션을 사용합니다. 예를들어, @Test 어노테이션이 붙은 메서드는 테스트 메서드로 인식됩니다.
  • 어설션 : JUnit 다양한 어설션 메서드를 제공하여 테스트 결과가 기대한 대로인지 확인합니다. 
  • 테스트 라이프 사이클 : JUnit은 @Before, @After, @BeforeClass, @AfterClass 와 같은 어노테이션을 제공하여 테스트 전처리 및 후처리를 할 수 있게 합니다.

 

테스트의 필요성

테스트 코드는 여러가지 이유로 필요성을 언급할 수 있습니다. 그중  저자 마이클 패더스의 "Working Effective with Legacy Code" 책에서는 기본적으로 "테스트가 없는 코드" 를 레거시 코드라고 합니다.

 

  • 이해하기 어려움 : 테스트 코드가 없으면 해당 로직의 의도가 명확하지 않기 때문에, 코드를 테스트 하기 어렵습니다. 코드가 무엇을 하는지 어떤 부작용이 있는지 이해하지 못하면 효과적인 테스트 케이스를 작성하기 어렵습니다.
  • 변경하기 어려움 : 변경하기 어려운 코드는 보통 복잡하거나 의존성이 높기 때문인데, 이를 변경할 때 예상치 못한 부작용이 발생할 수 있습니다. 이런 상황에서 테스트는 변경 전후의 코드 행동을 검증하는 중요한 역할을 합니다.
  • 높은 결합도 : 높은 결합도는 코드의 한 부분을 변경 했을 때 다른 부분에 예상치 못한 영향을 줄 수 있음을 의미합니다. 이러한 시스템에서는 작은 변경에도 많은 부분을 테스트 해야 합니다.
  • Regression ( 구글 웹서버 이야기, 구글 엔지니어는 이렇게 일한다. 구글러가 전하는 문화, 프로세스 등 ( 한빛미디어 283p)
    • 2005년 구글 웹서버에 생성성이 급격히 떨어지는 문제 발생
    • 설상가상 이 시기에는 릴리즈 주기도 길었고 버그도 많아짐
    • 아무리 똑똑한 엔지니어를 투입해도 문제가 해결되지 않았고, 항상 불안에 떨며 릴리즈 했음.
    • GWS 테크 리더는 자동 테스트를 도입하기로 결정
    • 팀원들이 자신감 있게 배포할 수 있게 되었고 그 결과 생산성이 올라감

위와 내용을 고려 해보면, 테스트가 왜 필요한지 어느정도 강조가 되는데요. 테스트는 코드를 이해하고, 안전하게 변경하며, 높은 결합도를 관리하는데 핵심적인 역할을 합니다. 따라서 테스트는 레거시 코드를 유지보수 하고 개선하는데 중요한 수단으로 활용 됩니다.

 

테스트와 SOLID

 

 

구글 테스트 3분류

 

 

  • 소형 테스트 ( 단위 테스트 ) 매우중요 : 단일서버, 단일 프로세스, 단일 스레드, 디스크 I/O 사용해선 안됨, Blocking call 허용 안됨, 해당 테스트가 전체 테스트의 80%를 차지 해야 한다.
  • 중형 테스트 : 단일서버, 멀티프로세스, 멀티스레드, h2 같은 테스트 db를 사용할 수 있음, 소형 테스트 보다 속도가 느림, 스프링 개발자들이 너무 많이 만드는 테스트이기도 함
  • 대형 테스트 : 멀티서버, End to End

 

간단한 테스트 코드예시

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class ExampleTest {
    
    @Test
    void testAddition() {
        assertEquals(2, 1 + 1, "1 + 1 should equal 2");
    }
}

 

 

계산기 어플리케이션 테스트 ( 소형 테스트 )

 

간단한 계산기를 만들어 테스트를 수행합니다. 기능으로는 +,-,*,/ 이며, 각 모듈을 분리하여 테스트를 진행합니다.

 

아래 코드는 계산기 코드이며, 유틸 클래스 이므로 private 생성자로 만들었습니다.

public class Calculator {

    private Calculator() {
        throw new IllegalStateException("Utility class");
    }

    public static long calculate(long num1, long num2, String operator) {
        long answer = 0;
        switch (operator) {
            case "+":
                answer = num1 + num2;
                break;
            case "-":
                answer = num1 - num2;
                break;
            case "*":
                answer = num1 * num2;
                break;
            case "/":
                answer = num1 / num2;
                break;
            default:
                throw new InvalidOperatorException();
        }
        return answer;
    }
}


class CalculatorTest {

    @DisplayName("덧셈 테스트")
    @Test
    void calculatePlusTest() {

        // given
        long num1 = 2;
        long num2 = 3;
        String operator = "+";

        // when
        long result = Calculator.calculate(num1, num2, operator);

        // then
        assertEquals(5, result);
        Assertions.assertThat(result).isEqualTo(5);
    }

    @DisplayName("곱셈 테스트")
    @Test
    void calculateMultiplyTest() {

        // given
        long num1 = 2;
        long num2 = 3;
        String operator = "*";

        // when
        long result = Calculator.calculate(num1, num2, operator);

        // then
        assertEquals(6, result);
        Assertions.assertThat(result).isEqualTo(6);
    }

    @DisplayName("뺄셈 테스트")
    @Test
    void calculateMinusTest() {

        // given
        long num1 = 2;
        long num2 = 3;
        String operator = "-";

        // when
        long result = Calculator.calculate(num1, num2, operator);

        // then
        assertEquals(-1, result);
        Assertions.assertThat(result).isEqualTo(-1);
    }

    @DisplayName("뺄셈 테스트")
    @Test
    void calculateDivideTest() {

        // given
        long num1 = 6;
        long num2 = 3;
        String operator = "/";

        // when
        long result = Calculator.calculate(num1, num2, operator);

        // then
        assertEquals(2, result);
        Assertions.assertThat(result).isEqualTo(2);
    }

    @DisplayName("잘못된 연산자가 들어올 경우 예외가 발생해야 한다.")
    @Test
    void throwTest() {
        // given
        long num1 = 6;
        long num2 = 3;
        String operator = "x";

        // assertJ
        assertThatThrownBy(
                () -> Calculator.calculate(num1, num2, operator)
        ).isInstanceOf(InvalidOperatorException.class).hasMessage("Invalid Operator");

        // assertions
        assertThrows(InvalidOperatorException.class, () -> Calculator.calculate(num1, num2, operator));
    }
}

 

 

값을 전달 받는 부분 코드 및 테스트

public class CalculationRequest {

    private final long num1;
    private final long num2;
    private final String operator;

    public CalculationRequest(String[] parts) {

        if (parts.length != 3) {
            throw new BadRequestException("Invalid input");
        }
        String operator = parts[2];
        if (operator.length() != 1 || isInvalidOperator(operator)) {
            throw new InvalidOperatorException();
        }

        this.num1 = Long.parseLong(parts[0]);
        this.num2 = Long.parseLong(parts[1]);
        this.operator = parts[2];
    }

    private boolean isInvalidOperator(String operator) {
        return !operator.equals("+") && !operator.equals("-") && !operator.equals("*") && !operator.equals("/");
    }

    public long getNum1() {
        return num1;
    }

    public long getNum2() {
        return num2;
    }

    public String getOperator() {
        return operator;
    }
}

class CalculationRequestTest {

    @DisplayName("유효한 숫자를 파싱할 수 있다.")
    @Test
    void validNumberParsing() {

        // given
        String[] parts = new String[]{"2", "3", "+"};

        // when
        CalculationRequest calculationRequest = new CalculationRequest(parts);

        // then
        assertEquals(2, calculationRequest.getNum1());
        assertEquals(3, calculationRequest.getNum2());
        assertEquals("+", calculationRequest.getOperator());
    }

    @DisplayName("입력 값이 누락 되었으면 에러를 던진다.")
    @Test
    void inputThrow() {

        // given
        String[] parts = new String[]{"2", "3"};

        // when & then
        assertThrows(BadRequestException.class, () -> new CalculationRequest(parts));
        assertThatThrownBy(() -> new CalculationRequest(parts))
                .isInstanceOf(BadRequestException.class)
                .hasMessage("Invalid input");
    }

    @DisplayName("유효하지 않은 연산자기 입력되면 에러를 던진다.")
    @Test
    void operatorThrow() {

        // given
        String[] parts = new String[]{"2", "3", "x"};

        // when & then
        assertThatThrownBy(() -> new CalculationRequest(parts))
                .isInstanceOf(InvalidOperatorException.class);
    }

    @DisplayName("연산자의 길이가 1이 아니면 에러를 던진다.")
    @Test
    void operatorLengthThrow() {

        // given
        String[] parts = new String[]{"2", "3", "+11"};

        // when & then
        assertThatThrownBy(() -> new CalculationRequest(parts))
                .isInstanceOf(InvalidOperatorException.class);
    }
}

 

 

값을 읽는 부분 코드 및 테스트

public class CalculationRequestReader {

    public CalculationRequest read() {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter to numbers and an operator (e.g 1 + 2): ");
        String result = scanner.nextLine();
        String[] parts = result.split(" ");
        return new CalculationRequest(parts);
    }
}

class CalculationRequestReaderTest {

    @DisplayName("System.in 으로 부터 계산식을 입력받는다.")
    @Test
    void read() {

        // given
        CalculationRequestReader calculationRequestReader = new CalculationRequestReader();
        System.setIn(new ByteArrayInputStream("2 3 +".getBytes()));

        // when
        CalculationRequest calculationRequest = calculationRequestReader.read();

        // then
        assertThat(2).isEqualTo(calculationRequest.getNum1());
        assertThat(3).isEqualTo(calculationRequest.getNum2());
        assertThat("+").isEqualTo(calculationRequest.getOperator());
    }
}

 

 

 

테스트 커버리지 ( 테스트 하려는 해당 패키지 마우스 우클릭 후 커버리지 테스트 전체 실행 )

 

 

 

참고 자료

https://velog.io/@hiyeeluca/1.-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1%EA%B3%BC-3%EB%B6%84%EB%A5%98

'테스트' 카테고리의 다른 글

테스트 대역 5가지  (0) 2024.07.17
테스트  (0) 2024.07.16
Mockito 를 활용한 단위 테스트  (0) 2024.01.24
번거로운 동작을 스텁(stub)으로 대체  (0) 2024.01.24
번거로운 동작을 스텁(stub)으로 대체  (0) 2024.01.19