JUnit5과 AssertJ

초코칩·2024년 4월 13일
0

Test

목록 보기
1/4
post-thumbnail

JUnit5

JUnit5는 자바 언어를 사용하는 소프트웨어 개발자들을 위한 테스트 프레임워크 중 하나이다.

@Test

@Test 애노테이션은 해당 메서드가 테스트 메서드임을 나타낸다. 테스트 메서드는 void 타입을 반환하며, @Test 애노테이션을 사용하여 테스트 메서드임을 나타낸다. @Test 어노테이션을 붙이면 해당 메서드의 반환값이 void 타입인지 확인해주며, 테스트를 실행할 수 있게 한다.

왜 테스트 메서드의 반환값을 void로 강제할까?

return 타입이 void인 경우 JUnit은 해당 테스트 메서드가 정상적으로 종료되었음을 간주하고 예외를 던지면 해당 테스트의 실패했다고 간주하게 된다. 때문에 모든 테스트 결과는 void 또는 exception으로 반환되며 일관성 있고 신뢰성 있는 테스트 환경을 유지할 수 있다.

@DisplayName

테스트의 메서드에 한글을 작성해도 동작은 하지만 크게 3가지 문제가 있다.
1. 도구 및 라이브러리와의 호환성: 일부 개발 도구, 라이브러리 및 프레임워크는 한글을 지원하지 않는다.
2. 언어 혼합: 언어 식별자가 혼합된 코드베이스에서는 일관성을 유지하는 것이 어려울 수 있다. 코드의 일부에서는 영어 명명을 사용하고 다른 부분에서는 한글을 사용하면 전체 코드 가독성이 떨어질 수 있다.
3. 경고 발생: 일부 IDE는 한글을 사용할 때 경고를 발생시킨다.

@DisplayName 애노테이션이 없이 한글 메서드명을 작성한다면, 해당 메서드명에 Non-ASCII characters 경고가 발생한다. 따라서 해당 경고를 제거하기 위해 @DisplayName 애노테이션을 사용하고 메서드명은 영어로 작성하는 것이 좋다.

@Test

void DisplayName_애노테이션을_붙여_경고를_제거한다() {
	// TODO: `@DisplayName` 애노테이션을 활용하여 
    //`Non-ASCII characters` 경고를 제거해주세요.
}

@Test
@DisplayName("@DisplayName 애노테이션 학습 테스트")
void foo() {
}

@Nested

@Nested 애노테이션은 해당 클래스가 중첩 클래스임을 나타낸다. 중첩 클래스는 클래스 내부에 선언된 클래스를 의미한다. 중첩으로 표현하는 이유는 클래스의 의미를 명확하게 하기 위함이다.

@Nested 애노테이션이 없다면 해당 메서드는 중첩 클래스가 아니다. 따라서 해당 메서드는 중첩 클래스 내부에 있는게 아니기 때문에, 테스트 메서드로서의 역할을 수행하지 않는다.
@Nested 애노테이션을 사용하면 해당 메서드는 중첩 클래스 내부에 있는 것으로 인식되며, 테스트 메서드로서의 역할을 수행한다. 이로써 계층적 테스트 가능하게 한다.

아래 블랙잭 예시는 @Nested 애노테이션을 활용하여 계층적으로 각 테스트를 나눈 예시이다.

@DisplayName("결과 판단")
class RefereeTest {
    @DisplayName("딜러가 버스트된 경우")
    @Nested
    class whenDealerBust {
        @BeforeEach
        void setUp() {
			//...
        }

        @DisplayName("플레이어가 버스트된 상황은 딜러는 무승부로 판단한다.")
        @Test
        void drawWhenBustTogether() {
			//...
        }

        @DisplayName("플레이어가 블랙잭인 경우 딜러가 패배한다.")
        @Test
        void loseWhenPlayerBlackjack() {
			//...
        }

        @DisplayName("플레이어가 일반 카드인 경우 딜러가 패배한다.")
        @Test
        void loseWhenPlayerNormal() {
			//...
        }
    }

    @DisplayName("딜러가 블랙잭인 경우")
    @Nested
    class whenDealerBlackjack {
        @BeforeEach
        void setUp() {
			//...
        }

        @DisplayName("플레이어가 버스트되면 딜러가 승리한다.")
        @Test
        void winWhenPlayerBust() {
			//...
        }

        @DisplayName("플레이어가 블랙잭이면 무승부로 판단한다.")
        @Test
        void drawWhenPlayerBlackjack() {
			//...
        }

        @DisplayName("플레이어가 일반이면 딜러가 승리한다.")
        @Test
        void winWhenPlayerNormal() {
			//...
        }
    }

    @DisplayName("딜러가 블랙잭도 아니고, 버스트되지 않은 일반 경우")
    @Nested
    class whenDealerNormal {
        @BeforeEach
        void setUp() {
			//...
        }

        @DisplayName("플레이어가 버스트되면 딜러가 승리한다.")
        @Test
        void winWhenPlayerBust() {
			//...
        }

        @DisplayName("플레이어가 블랙잭이면 딜러가 패배한다.")
        @Test
        void loseWhenPlayerBlackjack() {
			//...
        }

        @DisplayName("플레이어가 일반이면 딜러 카드가 클 경우 딜러가 승리한다.")
        @Test
        void winWhenPlayerNormalWithSmallerScore() {
			//...
        }

        @DisplayName("플레이어가 일반이면 딜러 카드가 작을 경우 딜러가 패배한다.")
        @Test
        void loseWhenPlayerNormalWithBiggerScore() {
			//...
        }

        @DisplayName("플레이어와 점수가 같을 경우 딜러는 무승부로 판정한다.")
        @Test
        void drawWhenPlayerNormalWithSameScore() {
			//...
        }
    }
}

클래스의 의미를 명확히 했을 뿐만 아니라, 각 클래스마다 @BeforeEach로 다른 테스트 픽스처 를 적용할 수 있다.

@Disabled

@Disabled 애노테이션은 테스트에서 제외하게 만든다. 메서드, 클래스 둘 다 적용 가능하다.

@AssertAll

assertAll 메서드는 여러 검증 코드를 한 번에 실행합니다.
assertAll 메서드는 assertAll(executables) 형태로 오버로딩되어 있습니다.
executables은 검증 코드입니다.

AssertJ

테스트 코드를 작성할 때 더 편리하게 작성할 수 있도록 도와주는 라이브러리다. AssertJ가 JUnit을 대체하는 것은 아니고, JUnit과 함께 사용할 수 있다. 같이 사용하면 보다 더 다양한 메서드를 사용할 수 있고, 테스트 코드를 작성할 때 더 편리하게 작성할 수 있다.

assertThatThrownBy

assertThatThrownBy는 JUnit5의 assertThrows와 같은 역할을 한다.

@Test
@DisplayName("assertThatThrownBy 메서드로 특정 예외가 발생하는지 비교한다")
void assertThatThrownBy_메서드로_특정_예외가_발생하는지_비교한다() {
	// TODO: JUnit5의 assertThrows 메서드를 
    // AssertJ의 assertThatThrownBy 메서드로 변경해보세요.
	assertThrows(IllegalCallerException.class, () -> {
			causeException();
	});
}


@Test
@DisplayName("assertThatThrownBy 메서드로 특정 예외가 발생하는지 비교한다")
void assertThatThrownBy_메서드로_특정_예외가_발생하는지_비교한다() {
	assertThatThrownBy(this::causeException)
			.isInstanceOf(IllegalArgumentException.class);
}

예외 메시지

assertThatThrownBy를 사용하면 예외 타입 뿐만 아니라 예외 메시지까지 비교할 수 있다.

JUnit5의 assertThrows 메서드는 예외 타입만 비교할 수 있다.

@Test
@DisplayName("assertThatThrownBy 메서드로 특정 예외가 발생하는지 비교한다")
void assertThatThrownByTest() {
	assertThatThrownBy(this::causeException)
			.isInstanceOf(IllegalCallerException.class)
            .hasMessage("예외가 발생했습니다.");
}

private void causeException() {
	throw new IllegalCallerException("예외가 발생했습니다.");
}

String의 활용

AssertJ는 assertThat 메서드의 매개변수로 넘기는 타입에 맞게 메서드를 사용할 수 있는 기능을 제공한다. String 타입의 매개변수를 assertThat 메서드의 매개변수로 넘기면 String 타입에 맞는 메서드를 사용할 수 있다.

대표적인 몇가지 메서드를 살펴보자.

contains

contains 메서드로 문자열에 특정 문자열이 포함되어 있는지 비교한다.

@Test
void containsTest() {
	final var actual = "Hello, world!";
	final var expected = "world";
	
    assertThat(actual).contains(expected);
}

startsWith

startsWith는 문자열이 특정 문자열로 시작하는지 비교할 때 사용한다.

@Test
void startsWithTest() {
	final var actual = "Hello, world!";
	final var expected = "Hello";
	
    assertThat(actual).startsWith(expected);
}

endsWith

endsWith를 활용하여 문자열이 특정 문자열로 끝나는지 비교할 수 있다.

@Test
void endsWithTest() {
	final var actual = "Hello, world!";
	final var expected = "world!";
	
    assertThat(actual).endsWith(expected);
}

matches

matches를 활용하여 문자열이 정규 표현식과 일치하는지 비교할 수 있다.

@Test
void matchesTest() {
	final var actual = "Hello, world!";
	final var expected = "Hello, [a-z]+!";

	assertThat(actual).matches(expected);
}

Collection의 활용

Collection 타입의 매개변수를 assertThat 메서드의 매개변수로 넘기면 Collection 타입에 맞는 메서드를 사용할 수 있다.

대표적인 몇가지 메서드를 살펴보자.

hasSize

hasSize는 Collection의 크기를 비교한다.

@Test
void hasSizeTest() {
	final var actual = List.of(1, 2, 3);
	final var expected = 3;

	assertThat(actual).hasSize(expected);
}

contains

contains는 Collection에 특정 객체가 포함되어 있는지 비교한다.

@Test
void containsTest() {
	final var actual = List.of(1, 2, 3);
	final var expected = 1;

	assertThat(actual).contains(expected);
}

containsExactlyElementsOf

Collection에 특정 객체들이 포함되어 있는지 비교한다.

@Test
void containsExactlyElementsOfTest() {
	final var actual = List.of(1, 2, 3);
	final var expected = List.of(1, 2, 3);

	assertThat(actual).containsExactlyElementsOf(expected);
}

특정 객체들이 포함되어 있는지 비교하는 방법은 굉장히 많습니다. 또한 비슷한 기능도 굉장히 많다.

  • contains
  • containsAll
  • containsExactly
  • containsExactlyElementsOf
  • containsAnyElementsOf
  • containsExactlyInAnyOrder
  • containsExactlyInAnyOrderElementsOf

extracting

extracting 메서드는 Collection에 포함된 객체들 중 특정 필드를 추출할 때 사용한다. extracting 메서드는 추출한 필드를 기반으로 메서드를 사용할 수 있다.

추출한 필드를 기반으로 메서드를 사용하면 테스트 코드를 작성할 때 더 편리하게 작성할 수 있다.

extracting 메서드로 Collection에 포함된 객체들 중 특정 필드를 추출한다.

@Test
void extractingTest() {
	class User {
		private final String username;
		private final String password;

		User(final String username, final String password) {
			this.username = username;
            this.password = password;
		}

		public String getUsername() {
			return username;
		}
	}

	final var actual = List.of(
				new User("user1", "password1"),
				new User("user2", "password2"),
				new User("user3", "password3")
            );
            
	final var expected = List.of("user1", "user2", "user3");

	assertThat(actual).extracting("username").containsExactlyElementsOf(expected);
}

주의

extracting을 사용할 경우 getter가 없어도 필드값을 추출할 수 있다. 그 이유는 extracting 메서드가 Reflection을 사용하기 때문이다. Reflection은 객체의 필드나 메서드에 접근할 수 있도록 해주는 기능이지만, Reflection을 사용할 때는 주의해야 한다.

Fluent API

Fluent API는 메서드를 연속해서 사용할 수 있도록 메서드를 연결하는 방식이다. 메서드를 연속해서 사용하면 테스트 코드를 작성할 때 더 편리하게 작성할 수 있다. 또한 AssertJ는 메서드를 사용할 때 메서드 이름을 보고 어떤 기능을 하는지 쉽게 알 수 있도록 메서드 이름을 지었기 때문에 메서드 이름을 보고 어떤 기능을 하는지 쉽게 알 수 있다.

@Test
void chainingTest() {
	final var actual = new Object();
	final var expected = actual;

 	assertThat(actual).isNotNull()
				.isInstanceOf(Object.class)
                .isSameAs(expected);
    }

Ref

https://velog.io/@hope1213/junit%EC%9D%98-%EC%98%88%EC%A0%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%A9%94%EC%84%9C%EB%93%9C%EB%8A%94-%EC%99%9C-public%EB%A7%8C-%ED%97%88%EC%9A%A9%ED%96%88%EC%9D%84%EA%B9%8C-%EC%99%9C-void%EB%A7%8C-%EB%B0%98%ED%99%98%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C

https://github.com/cho-log/java-learning-test

profile
초코칩처럼 달콤한 코드를 짜자

0개의 댓글