앞서 블로그에 소개한 순서대로, 공부를 진행해보겠다.
1. 계산기 메서드 구현
public class Calculator {
public static int add(int num1, int num2) {
return num1 + num2;
}
public static int subtract(int num1, int num2) {
return num1 - num2;
}
public static int multiple(int num1, int num2){
return num1*num2;
}
public static int divide(int num1, int num2){
return num1/num2;
}
}
일단은 요런식으로 빠르고 간단하게 구현했고, 빨리 Junit5 사용법 익혀서 위 메서드를 테스트 코드에 적용시켜 보겠다. (나중에 MVC 패턴으로 수정 할거임!!)
2. Junit5 학습
먼저 기본적으로 Junit5는,
자바 언어를 사용하는 소프트웨어 개발자들을 위한 테스트 프레임워크 중 하나이다.
(가독성이 떨어질까봐 블록에 넣어봤음)
기본적으로 테스트를 통해 소프트웨어 품질을 향상시킬 수 있고, Junit5는 단위테스트를 하는 데에 주로 쓰인다고 한다.
이제 진짜 start!
학습 테스트 진행 방식
|
위 과정에 따라 진행해보겠다.
/**
* `@Test` 애노테이션이 없다면 해당 메서드는 테스트 메서드가 아닙니다.
* 따라서 해당 메서드는 테스트 메서드가 아니기 때문에, 테스트 메서드로서의 역할을 수행하지 않습니다.
* `@Test` 애노테이션을 사용하면 해당 메서드는 테스트 메서드가 되며, 테스트 메서드로서의 역할을 수행합니다.
*/
@Test
void Test_애노테이션을_붙여_테스트_메서드로_만든다() {
// TODO: `@Test` 애노테이션을 활용하여 테스트가 실행되게 해주세요.
}
* 꼭 @Test 어노테이션을 붙여줘야 한다.
@Test
void return_타입이_void가_아니라면_테스트_메서드가_아니다() {
// TODO: return 타입을 변경하여 해당 테스트가 실행되게 해주세요.
}
* 꼭 void 타입이여야 한다.
원래 int형에 return 0; 이였는데 내가 수정해줌.
@Test
@DisplayName("@DisplayName 학습")
void DisplayName_애노테이션을_붙여_경고를_제거한다() {
// TODO: `@DisplayName` 애노테이션을 활용하여 `Non-ASCII characters` 경고를 제거해주세요.
}
* 메서드명 한글로 해주려면 @DisplayName 필요.
/**
* `@Nested` 애노테이션은 해당 클래스가 중첩 클래스임을 나타냅니다.
* 중첩 클래스는 클래스 내부에 선언된 클래스를 의미합니다.
* 중첩으로 표현하는 이유는 클래스의 의미를 명확하게 하기 위함입니다.
*/
@Nested
@DisplayName("@Nested 애노테이션 학습 테스트")
class NestedAnnotationTest {
/**
* `@Nested` 애노테이션이 없다면 해당 메서드는 중첩 클래스가 아닙니다.
* 따라서 해당 메서드는 중첩 클래스 내부에 있는게 아니기 때문에, 테스트 메서드로서의 역할을 수행하지 않습니다.
* `@Nested` 애노테이션을 사용하면 해당 메서드는 중첩 클래스 내부에 있는 것으로 인식되며, 테스트 메서드로서의 역할을 수행합니다.
*/
@Test
@DisplayName("@Nested 애노테이션을 붙여줘야 중첩 클래스로서의 역할을 수행한다")
void Nested_애노테이션을_붙여줘야_중첩_클래스로서의_역할을_수행한다() {
// TODO: `@Nested` 애노테이션을 활용하여 테스트가 실행되게 해주세요.
}
}
* 이런식으로 해당 메서드를 중첩 클래스 내부에서 사용하려면 @Nested 사용해야 한다.
/**
* `@Disabled` 애노테이션이 없다면 해당 메서드는 테스트 메서드입니다.
* 따라서 해당 메서드는 테스트 메서드로서의 역할을 수행합니다.
* `@Disabled` 애노테이션을 사용하면 해당 메서드는 테스트 메서드가 아니기 때문에, 테스트 메서드로서의 역할을 수행하지 않습니다.
*/
@Test
@DisplayName("@Disabled 애노테이션을 붙여줘야 테스트 메서드로서의 역할을 수행하지 않는다")
@Disabled
void Disabled_애노테이션을_붙여줘야_테스트_메서드로서의_역할을_수행하지_않는다() {
// TODO: `@Disabled` 애노테이션을 활용하여 테스트가 실행되지 않게 해주세요.
throw new RuntimeException("항상 실패한다.");
}
/**
* `@Disabled` 애노테이션은 클래스에도 적용 가능하며, 클래스에 `@Disabled` 애노테이션을 붙이면 해당 클래스의 모든 테스트가 비활성화됩니다.
* 따라서 해당 클래스의 모든 테스트가 비활성화되기 때문에, 테스트 메서드로서의 역할을 수행하지 않습니다.
*/
* @Disabled 사용하면 비활성화가 가능하다.
근데 이건 어느때에 주로 사용될까 궁금하다. 많이 봤는데, 테스트 코드를 제대로 짜본 적이 없어서 어느 타이밍에 사용할 지 감이 안잡힌다. 좀 찾아봐야 할듯.
/**
* `assertEquals` 메서드는 `assertEquals(expected, actual)` 형태로 오버로딩되어 있습니다.
* `expected`는 예상되는 값이며, `actual`은 실제 값입니다.
* <p>
* `assertEquals` 메서드는 두 값이 같다면 테스트를 성공시킵니다.
* 기존에 작성된 검증 코드를 `assertEquals` 메서드를 사용하도록 변경해주세요.
*/
@Test
@DisplayName("assertEquals 메서드로 두 값이 같은지 비교한다")
void assertEquals_메서드로_두_값이_같은지_비교한다() {
final var a = 1;
final var b = 2;
final var actual = a + b;
final var expected = 3;
// TODO: 아래 코드를 assertEquals 메서드로 대체해주세요. 제대로 동작하는지 확인하기 위해 expected와 actual의 값을 바꿔보세요.
assertEquals(actual,expected);
}
* assertEquals (여기에 메세지 추가도 가능함)
/**
* `assertEquals` 메서드는 `expected`와 `actual`이 같은지 비교하기 위해 `equals` 메서드를 사용합니다.
* 따라서 `expected`와 `actual`의 타입이 `equals` 메서드를 정의하고 있다면 `assertEquals` 메서드를 사용할 수 있습니다.
* <p>
* 그 말은 `equals` 메서드를 정의하지 않은 타입은 `assertEquals` 메서드를 사용할 수 없다는 것을 의미합니다.
* 기존 객체 비교 코드에서 `equals`를 재정의하여 테스트를 성공시켜주세요.
*/
@Test
@DisplayName("assertEquals 메서드로 두 객체가 같은지 비교한다")
void assertEquals_메서드로_두_객체가_같은지_비교한다() {
class LocalObject {
private final int value;
public LocalObject(int value) {
this.value = value;
}
@Override
public boolean equals(Object obj) {
if(this==obj){
return true;
}
if(!(obj instanceof LocalObject)){
return false;
}
LocalObject localObject = (LocalObject) obj;
return Objects.equals(this.value,localObject.value);
}
}
final var a = new LocalObject(1);
final var b = new LocalObject(1);
// TODO: LocalObject 클래스의 equals를 재정의하여 아래 코드가 테스트를 성공시키도록 해주세요.
assertEquals(a, b);
}
* assertEquals 쓰려면 꼭 equals 매서드를 재정의 한 타입이여야 한다.
평소에 equals 메서드를 왜 계속 재정의 하나 했더니, 이런 이유도 있다는 것을 오늘 또 처음 알았다. 이 부분에서 30분 동안 헤맴........답은 끝까지 안보고 구글링 통해서 알아냈다.
/**
* `assertNotEquals` 메서드는 두 값이 다르다면 테스트를 성공시킵니다.
* 기존에 작성된 검증 코드를 `assertNotEquals` 메서드를 사용하도록 변경해주세요.
*/
@Test
@DisplayName("assertNotEquals 메서드로 두 값이 다른지 비교한다")
void assertNotEquals_메서드로_두_값이_다른지_비교한다() {
final var a = 1;
final var b = 2;
final var actual = a + b;
final var unexpected = 0;
// TODO: 아래 코드를 assertNotEquals 메서드로 대체해주세요. 제대로 동작하는지 확인하기 위해 expected와 actual의 값을 바꿔보세요.
assertNotEquals(unexpected,actual);
}
* assertNotEquals 메서드.
두 값이 같지 않을 때, 테스트 통과.
@Test
@DisplayName("assertSame 메서드로 두 객체가 같은지 비교한다")
void assertSame_메서드로_두_객체가_같은지_비교한다() {
final var object = new Object();
final var actual = object;
final var expected = object;
// TODO: 아래 코드를 assertSame 메서드로 대체해주세요. 제대로 동작하는지 확인하기 위해 expected와 actual의 값을 바꿔보세요.
assertSame(expected,actual);
}
* assertSame 메서드: 두 객체의 비교 메서드
/**
* `assertThrows` 메서드는 `assertThrows(expectedType, executable)` 형태로 오버로딩되어 있습니다.
* `assertThrows` 메서드는 특정 예외가 발생한다면 테스트를 성공시킵니다.
*/
@Test
@DisplayName("assertThrows 메서드로 특정 예외가 발생하는지 비교한다")
void assertThrows_메서드로_특정_예외가_발생하는지_비교한다() {
// TODO: try-catch문을 사용하지 않고 assertThrows 메서드를 사용하여 테스트가 성공하도록 해주세요.
try {
causeException();
} catch (Exception e) {
return;
}
throw new RuntimeException("예외가 발생하지 않았습니다.");
}
* 대망의 try-catch 문이 나왔다. 나같은 초짜에게 있어서 이 부분 테스트 어떤식으로 해야할지 감이 1도 잡히지 않았슴......
기본적으로 try-catch 문은 try 문에서 발생한 예외를 catch 문에서 잡아서 오류 코드 실행시키는 것.
try{ 예외가 발생할 수 있는 코드 } catch(발생할 수 있는 예외 타입){ 예외 처리 코드 } finally{ 예외 관계 없이 실행되는 코드 } |
내가 이해한 try-catch 문은 요런 느낌임. 이제 이 코드를 어떻게 assertThrows()를 사용해서 테스트 해볼까....
@Test
@DisplayName("assertThrows 메서드로 특정 예외가 발생하는지 비교한다")
void assertThrows_메서드로_특정_예외가_발생하는지_비교한다() {
// TODO: try-catch문을 사용하지 않고 assertThrows 메서드를 사용하여 테스트가 성공하도록 해주세요.
assertThrows(RuntimeException.class, this::causeException);
}
난 요런식으로 작성했다. 근데 예외 처리 문장은 언제 봐도 어색한 것 같다.. 많이 접해보질 않아서.. 그래도 이번 기회에 예외처리에 대해 어떤 느낌으로 테스트 코드 짜는지 알 수 있어서 진짜 진짜 좋음.......혼자 공부할 땐 너무너무 힘들었다ㅠ
그리고 이 try-catch test 부분은 많이 중요한 것 같아서 문제 부분까지 코드를 같이 올렸다.
@Test
@DisplayName("assertThrows 메서드로 특정 예외가 발생하는지 비교한다")
void assertThrows_메서드로_특정_예외가_발생하는지_비교한다() {
assertThrows(Exception.class, this::causeException);
}
요건 모범답안임. 난 Runtime에 대해서만 예외 발생시키고 답안은 모든 예외처리 한듯.
/**
* `assertThrows` 메서드는 `assertThrows(expectedType, executable)` 형태로 오버로딩되어 있습니다.
* `expectedType`은 예상되는 예외 타입이며, `executable`은 예외가 발생할 코드입니다.
*/
@Test
@DisplayName("assertThrows 메서드로 특정 예외가 발생하는지 비교하며, 특정 예외가 발생하지 않는다면 테스트가 실패한다")
void assertThrows_메서드로_특정_예외가_발생하는지_비교하며_특정_예외가_발생하지_않는다면_테스트가_실패한다() {
// TODO: `causeException` 메서드에서 발생하는 예외 타입을 확인 후 `expectedType`를 변경하여 테스트가 성공하도록 해주세요.
assertThrows(IllegalCallerException.class, this::causeException);
}
private void causeException() {
throw new IllegalCallerException("예외가 발생했습니다.");
}
expectedType 을 Caller 타입으로 바꿔줬다. 이건 쉬웠다 ㅋ(밑에 예외 종류 써져있음 ㅋㅋ..)
/**
* `assertDoesNotThrow` 메서드는 `assertDoesNotThrow(executable)` 형태로 오버로딩되어 있습니다.
* `assertDoesNotThrow` 메서드는 특정 예외가 발생하지 않는다면 테스트를 성공시킵니다.
* <p>
* `assertDoesNotThrow` 메서드를 사용하지 않아도 테스트는 성공하지만, `assertDoesNotThrow` 메서드를 사용하지 않는다면 테스트의 의도를 명확하게 표현할 수 없습니다.
* 따라서 `assertDoesNotThrow` 메서드를 사용하여 테스트의 의도를 명확하게 표현하는 것이 좋습니다.
*/
@Test
@DisplayName("assertDoesNotThrow 메서드로 특정 예외가 발생하지 않는 것을 명시한다")
void assertDoesNotThrow_메서드로_특정_예외가_발생하지_않는_것을_명시한다() {
// TODO: `assertDoesNotThrow` 메서드를 사용하여 테스트의 의도를 명확하게 표현해주세요.
assertDoesNotThrow(() -> {
final var number = Integer.valueOf(0x80000000);
});
}
assertDoesNotThrow는 처음 봐서 신기했다. 그래서 구글링 통해서 사용법 익히고 적용해봄. 당연한 코드를 테스트 할 때 사용하면 테스트의 의도를 더욱 확실하게 전달해 줄 수 있을 것 같다.
/**
* `assertAll` 메서드는 `assertAll(executables)` 형태로 오버로딩되어 있습니다.
* `assertAll` 메서드는 `executables`에 포함된 검증 코드를 모두 실행합니다.
* `executables`에 포함된 검증 코드 중 하나라도 실패한다면 테스트는 실패합니다.
* <p>
* `assertAll`를 사용하는 이유는 여러 검증 코드가 존재할 때 문제가 발생한다면 어떤 검증 코드에서 문제가 발생했는지 알기 어렵기 때문입니다.
*/
@Test
@DisplayName("assertAll 메서드로 여러 검증 코드를 한 번에 실행한다")
void assertAll_메서드로_여러_검증_코드를_한_번에_실행한다() {
// TODO: `assertAll`을 사용하지 않고 모든 테스트를 통과시키는 것과 `assertAll`를 사용하고 모든 테스트를 통과시키는 것에 차이를 비교해보세요.
assertAll(
() -> assertEquals(3, 1 + 2),
() -> assertEquals(5, 3 + 2),
() -> assertEquals(21, 7 * 3),
() -> assertEquals(32, 3 * 7 ^ 5),
() -> assertEquals(4, 7 * 3 / 5 + 33 / 21),
() -> assertEquals(22, 33 * 3 / 5 + 7 / 2),
() -> assertEquals(22, 33 * 3 / 5 + 7 / 2 + 1));
}
assertAll 도 처음 써보는데 한 번에 검증할 때, 편리할듯.
터미널 결과에 어디 부분이 오류인지 다 뜸..!!!! 너무 길어서 첨부하지는 못했다 ㅠ
/**
* `@ValueSource` 애노테이션은 `@ParameterizedTest` 애노테이션과 함께 사용되며 정의된 값을 하나의 인자로 받아들이는 역할을 합니다.
* `@ValueSource` 내부에 선언된 `ints` 속성은 정수 형태의 인자를 입력할 수 있도록 만들어줍니다.
*/
@ParameterizedTest
@ValueSource(ints = 1)
@DisplayName("ValueSource 애노테이션을 붙여 정수 매개변수를 한 번 입력받는다")
void ValueSource_애노테이션을_붙여_정수_매개변수를_한_번_입력받는다(int value) {
}
@ParameterizedTest, @ValueSource 진짜 진짜 이 기능을 원했다............ 맨날 매개변수 어떻게 넘겨줄 지 고민했는데....초록스터디 진짜 짱이네
/**
* `@ValueSource` 애노테이션의 속성들은 다음과 같이 배열 형태로 입력이 가능합니다.
* `@ValueSource` 애노테이션의 속성을 배열 형태로 입력해줌으로서, 각 배열의 값마다 각각의 테스트를 수행하도록 구현할 수 있습니다.
*/
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4})
@DisplayName("ValueSource 애노테이션을 붙여 정수 매개변수를 여러 번 입력받는다")
void ValueSource_애노테이션을_붙여_정수_매개변수를_여러_번_입력받는다(int value) {
// TODO: `@ValueSource`를 사용하지 않고 여러 정수의 범위를 테스트하는 것과, `@ValueSource`를 사용해서 테스트하는 것의 차이를 비교해보세요.
assertTrue(value > 0 && value < 10);
}
이런 식으로 사용할 수 있음 !! 매개변수 여러개 넘겨주기 가능(배열 형식으로)
@ParameterizedTest
@ValueSource(strings = {"a", "b", "c"})
@DisplayName("ValueSource 애노테이션을 붙여 문자열 매개변수를 여러 번 입력받는다")
void ValueSource_애노테이션을_붙여_문자열_매개변수를_여러_번_입력받는다(String value) {
// TODO: `@ValueSource`를 사용하지 않고 여러 문자열의 Length를 테스트하는 것과, `@ValueSource`를 사용해서 테스트하는 것의 차이를 비교해보세요.
assertEquals(value.length(), 1);
}
문자열도 가능함
@ParameterizedTest
@ValueSource(strings = {"1", "2", "3", "4", "5"})
@DisplayName("ValueSource 애노테이션을 활용하여 1부터 5까지의 문자열 값을 Integer로 parseInt하는 로직이 예외를 발생시키지 않는 지 검증한다")
void ValueSource_애노테이션을_활용하여_1부터_5까지의_문자열_값을_Integer로_변경하는_로직이_예외를_발생시키지_않는_지_검증한다(String value) {
// TODO: `@ValueSource`를 사용하지 않고 1~5 까지의 문자열 값을 `Integer`로 `parseInt`하는 로직을 테스트하는 것과, `@ValueSource`를 사용해서 테스트하는 것의 차이를 비교해보세요.
assertDoesNotThrow(() -> Integer.parseInt(value));
}
/**
* `@MethodSource` 애노테이션은 메서드 이름을 인자로 받아들이며, 인자에서 호출된 메서드는 테스트 메서드에 전달될 인자를 반환합니다.
*/
@ParameterizedTest
@MethodSource("methodSourceTestArguments")
@DisplayName("MethodSource 애노테이션을 붙여 Object 매개변수를 한 번 입력받는다")
void MethodSource_애노테이션을_붙여_Object_매개변수를_한_번_입력받는다(Object value) {
}
@MethodSource 이 어노테이션은 메서드 이름을 받아서 사용할 수 있게 해줌. 완전 신기하네 메서드명 받아서 해주는건
@ParameterizedTest
@MethodSource("methodSourcesTestArguments")
@DisplayName("MethodSource 애노테이션을 붙여 Object 매개변수를 여러 번 입력받는다")
void MethodSource_애노테이션을_붙여_Object_매개변수를_여러_번_입력받는다(Object object) {
assertInstanceOf(Object.class, object);
}
/**
* `Arguments`를 `Stream` 내부에 여러 개 선언해줌으로서, 각 `Arguments`마다 각각의 테스트를 수행하도록 구현할 수 있습니다.
*/
private static Stream<Arguments> methodSourcesTestArguments() {
return Stream.of(
Arguments.arguments(new Object()),
Arguments.arguments(new Object()),
Arguments.arguments(new Object()),
Arguments.arguments(new Object())
);
}
이런 식으로 사용할 수 있음.
/**
* `@MethodSource` 애노테이션을 사용하면 `Iterable` 또한 테스트의 인자로 입력받을 수 있으며, 이 외에 어떠한 객체라도 입력받을 수 있습니다.
* `Arguments`의 `arguments` 메서드는 매개변수로 `Object... arguments`를 입력받고, 내부 구현을 통해 `Arguments` 객체를 생성하기 때문에 어떠한 타입이든 테스트의 인자로 사용할 수 있는 것입니다.
*/
@ParameterizedTest
@MethodSource("methodSourceIterableTestArguments")
@DisplayName("MethodSource 애노테이션을 붙여 Iterable 매개변수를 입력받는다")
void MethodSource_애노테이션을_붙여_Iterable_매개변수를_입력받는다(List<Integer> values) {
assertEquals(values.size(), 3);
}
private static Stream<Arguments> methodSourceIterableTestArguments() {
return Stream.of(
Arguments.arguments(
List.of(1, 4, 5),
List.of(1, 2, 3),
List.of(1, 3, 4)
)
);
}
그냥 메서드 명으로 메서드가 실행되는게 아직도 신기하다.. 자바 왤케 기능이 많냐 ㅠㅠ
/**
* `@MethodSource` 애노테이션과 `Arguments` 객체를 사용하면 서로 다른 자료형의 값 또한 테스트의 인자로 함께 입력받을 수 있습니다.
*/
@ParameterizedTest
@MethodSource("methodSourcesStringAndIntegerTestArguments")
@DisplayName("MethodSource 애노테이션을 붙여 정수와 문자열 매개변수를 입력받는다")
void MethodSource_애노테이션을_붙여_정수와_문자열_매개변수를_입력받는다(String v1, int v2) {
// TODO: `MethodSource`를 사용하지 않고 문자열과 정수 변수들을 직접 선언하여 테스트하는 방식과, `MethodSource`를 통해 입력받아 테스트하는 방식의 차이를 비교해보세요.
assertEquals(Integer.parseInt(v1), v2);
}
private static Stream<Arguments> methodSourcesStringAndIntegerTestArguments() {
return Stream.of(
Arguments.arguments("1", 1),
Arguments.arguments("2", 2),
Arguments.arguments("3", 3),
Arguments.arguments("4", 4)
);
}
이런 방법도 있음
여기까지가 Junit5 학습이였고, 위에 있는 코드들은 모두 내가 문제->정답으로 작성한 코드들이다.
Junit5 단위테스트 학습을 하면서 느낀 점은, 내가 처음 spring boot 시작했을 때 다들 테스트 코드가 중요하다고 하는데 어떤 식으로 작성해야 할 줄 몰랐는데 이 답답함에 대한 부분을 많이 해소한 것 같다. 물론 테스트 코드 짤 때, 여러 가지 경우의 수는 내가 생각해야 하지만 작성법, 어찌보면 사용법을 알았으니까 제대로된 방향성을 잡고 공부할 수 있을 것 같다. 열심히 해야쥥
'자바 > 초록스터디' 카테고리의 다른 글
(6)초록스터디 로또 1,2 단계 구현 (0) | 2024.05.23 |
---|---|
(5)초록스터디 : 계산기 구현 PR 후기 (0) | 2024.05.22 |
(4)초록스터디 step1 : 문자열 계산기 구현과 JUnit5 테스트 (0) | 2024.05.09 |
(3)초록스터디 step1 - 초간단 계산기 구현: 계산기 MVC 패턴 구현, JUnit5 테스트 적용 (0) | 2024.05.06 |
(1)초록스터디 시작! step1 - 초간단 계산기 구현 (0) | 2024.05.04 |