JUnit는 테스트 프레임워크 입니다. 테스트 주도 개발(TDD) 및 행동 주도 개발(BDD) 접근 방식에 맞춰 개발된 소프트웨어의 작은 부분(유닛)을 테스트하기 위해 사용됩니다.
- 단위 테스트 : JUnit은 개별 클래스와 메서드에 대한 단위 테스트를 작성하고 실행할 수 있습니다.
- 어노테이션 기반 : JUnit은 테스트 메서드를 정의할 때 어노테이션을 사용합니다. 예를들어, @Test 어노테이션이 붙은 메서드는 테스트 메서드로 인식됩니다.
- 어설션 : JUnit 다양한 어설션 메서드를 제공하여 테스트 결과가 기대한 대로인지 확인합니다.
- 테스트 라이프 사이클 : JUnit은 @Before, @After, @BeforeClass, @AfterClass 와 같은 어노테이션을 제공하여 테스트 전처리 및 후처리를 할 수 있게 합니다.
테스트의 필요성
테스트 코드는 여러가지 이유로 필요성을 언급할 수 있습니다. 그중 저자 마이클 패더스의 "Working Effective with Legacy Code" 책에서는 기본적으로 "테스트가 없는 코드" 를 레거시 코드라고 합니다.
- 이해하기 어려움 : 테스트 코드가 없으면 해당 로직의 의도가 명확하지 않기 때문에, 코드를 테스트 하기 어렵습니다. 코드가 무엇을 하는지 어떤 부작용이 있는지 이해하지 못하면 효과적인 테스트 케이스를 작성하기 어렵습니다.
- 변경하기 어려움 : 변경하기 어려운 코드는 보통 복잡하거나 의존성이 높기 때문인데, 이를 변경할 때 예상치 못한 부작용이 발생할 수 있습니다. 이런 상황에서 테스트는 변경 전후의 코드 행동을 검증하는 중요한 역할을 합니다.
- 높은 결합도 : 높은 결합도는 코드의 한 부분을 변경 했을 때 다른 부분에 예상치 못한 영향을 줄 수 있음을 의미합니다. 이러한 시스템에서는 작은 변경에도 많은 부분을 테스트 해야 합니다.
- Regression ( 구글 웹서버 이야기, 구글 엔지니어는 이렇게 일한다. 구글러가 전하는 문화, 프로세스 등 ( 한빛미디어 283p)
- 2005년 구글 웹서버에 생성성이 급격히 떨어지는 문제 발생
- 설상가상 이 시기에는 릴리즈 주기도 길었고 버그도 많아짐
- 아무리 똑똑한 엔지니어를 투입해도 문제가 해결되지 않았고, 항상 불안에 떨며 릴리즈 했음.
- GWS 테크 리더는 자동 테스트를 도입하기로 결정
- 팀원들이 자신감 있게 배포할 수 있게 되었고 그 결과 생산성이 올라감
위와 내용을 고려 해보면, 테스트가 왜 필요한지 어느정도 강조가 되는데요. 테스트는 코드를 이해하고, 안전하게 변경하며, 높은 결합도를 관리하는데 핵심적인 역할을 합니다. 따라서 테스트는 레거시 코드를 유지보수 하고 개선하는데 중요한 수단으로 활용 됩니다.
구글 테스트 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());
}
}
테스트 커버리지 ( 테스트 하려는 해당 패키지 마우스 우클릭 후 커버리지 테스트 전체 실행 )
참고 자료
'테스트' 카테고리의 다른 글
테스트 대역 5가지 (0) | 2024.07.17 |
---|---|
테스트 (0) | 2024.07.16 |
Mockito 를 활용한 단위 테스트 (0) | 2024.01.24 |
번거로운 동작을 스텁(stub)으로 대체 (0) | 2024.01.24 |
번거로운 동작을 스텁(stub)으로 대체 (0) | 2024.01.19 |