πŸ’« Language/Java

[Java] JUnit5 μ‚¬μš©λ²• μ•Œμ•„λ³΄κΈ°

TIlearn 2024. 1. 9. 16:26

 

 

 

JUnitμ΄λž€?

 

πŸ”² Java 개발자의 93%κ°€ μ‚¬μš©ν•˜λŠ” λ‹¨μœ„ ν…ŒμŠ€νŠΈ ν”„λ ˆμž„μ›Œν¬

 

πŸ”² μŠ€ν”„λ§ λΆ€νŠΈ 2.2버전 이상뢀터 κΈ°λ³Έ μ œκ³΅ν•œλ‹€.

 

 

 

 

 

 

JUnit5λŠ”?

 

 

πŸ”² Platform : ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜κ²Œ ν•΄μ£ΌλŠ” 런처λ₯Ό μ œκ³΅ν•œλ‹€. TestEngine APIλ₯Ό 제곡.

 

πŸ”² Jupiter : JUnit5λ₯Ό μ§€μ›ν•˜λŠ” TestEngine API κ΅¬ν˜„μ²΄

 

πŸ”² Vintage : JUnit4와 3λ₯Ό μ§€μ›ν•˜λŠ” TestEngine κ΅¬ν˜„μ²΄

 

 

 

 

 

 

 

JUnit5λ₯Ό μ‹œμž‘ν•˜λŠ” 방법

 

πŸ”² μŠ€ν”„λ§λΆ€νŠΈ 2.2버전 μ΄μƒλΆ€ν„°λŠ” κΈ°λ³Έ νƒ‘μž¬λœλ‹€.

 

πŸ”² κ·Έ μ™Έμ—λŠ” maven, ν˜Ήμ€ gradle λ°©μ‹μœΌλ‘œ μΆ”κ°€ν•  수 μžˆλ‹€.

 

 

gradle

test {
    useJUnitPlatform()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

 

 

 

maven

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

 

 

 

 

 

 

 

 

 

Annotations

 

 

πŸ”Ά @Test

 

ν…ŒμŠ€νŠΈ λ©”μ„œλ“œμž„μ„ λ‚˜νƒ€λ‚΄λŠ” μ–΄λ…Έν…Œμ΄μ…˜μ΄λ‹€. JUnit4μ™€λŠ” λ‹€λ₯΄κ²Œ μ–΄λ– ν•œ 속성도 κΈ°μž¬ν•˜μ§€ μ•ŠλŠ”λ‹€.

 

@Test
void create() {
...
}

 

 

 

 

πŸ”Ά 생λͺ…μ£ΌκΈ°(LifeCycle) μ–΄λ…Έν…Œμ΄μ…˜

 

 

 

 

πŸ”² @BeforeAll : ν•΄λ‹Ή ν΄λž˜μŠ€μ— μœ„μΉ˜ν•œ λͺ¨λ“  ν…ŒμŠ€νŠΈ λ©”μ„œλ“œ μ‹€ν–‰ μ „, ν•œ 번만 μ‹€ν–‰λ˜λŠ” λ©”μ„œλ“œ

 

πŸ”² @AfterAll : ν•΄λ‹Ή ν΄λž˜μŠ€μ— μœ„μΉ˜ν•œ λͺ¨λ“  ν…ŒμŠ€νŠΈ λ©”μ„œλ“œ μ‹€ν–‰ ν›„, ν•œ 번만 μ‹€ν–‰λœλ‹€.

 

πŸ”² @BeforeEach : ν•΄λ‹Ή ν΄λž˜μŠ€μ— μœ„μΉ˜ν•œ λͺ¨λ“  ν…ŒμŠ€νŠΈ λ©”μ„œλ“œ μ‹€ν–‰ 전에 μ‹€ν–‰λ˜λŠ” λ©”μ„œλ“œ, BeforeAllκ³Ό 달리 λͺ¨λ“  λ©”μ„œλ“œ λ§ˆλ‹€ μ‹€ν–‰λœλ‹€.

 

πŸ”² @AfterEach : ν•΄λ‹Ή ν΄λž˜μŠ€μ— μœ„μΉ˜ν•œ λͺ¨λ“  ν…ŒμŠ€νŠΈ λ©”μ„œλ“œ μ‹€ν–‰ 후에 μ‹€ν–‰λ˜λŠ” λ©”μ„œλ“œ, AfterAllκ³Ό 달리 λͺ¨λ“  λ©”μ„œλ“œλ§ˆλ‹€ μ‹€ν–‰λœλ‹€.

 

 

 

 

 

 

 

πŸ”Ά @DisplayName

 

μ–΄λ–€ ν…ŒμŠ€νŠΈμΈμ§€ μ‰½κ²Œ ν‘œν˜„ν•  수 μžˆλ„λ‘ ν•΄μ£ΌλŠ” μ–΄λ…Έν…Œμ΄μ…˜μ΄λ‹€. κ³΅λ°±μ΄λ‚˜, Emoji, 특수문μž₯ λ“± λͺ¨λ‘ μ§€μ›ν•œλ‹€.

 

 

 

@DisplayName("ν…ŒμŠ€νŠΈμž…λ‹ˆλ‹€")
void test() {
...
}

 

 

 

 

 

 

πŸ”Ά RepeatedTest

 

 

νŠΉμ • ν…ŒμŠ€νŠΈλ₯Ό λ°˜λ³΅μ‹œν‚€κ³ μž ν•  λ•Œ μ‚¬μš©ν•œλ‹€. 반볡 νšŸμˆ˜λŠ” _value_ 속성을 톡해 μ„€μ •ν•˜κ³ , _name_속성을 톡해 κ·Έ 이름도 지어쀄 수 μžˆλ‹€.

 

@RepeatedTest(value = 10, name = "")
@DisplayName("반볡")
void repeatedTest() {
...
}

 

 

 

 

πŸ”Ά @ParameterizedTest

 

ν…ŒμŠ€νŠΈμ— μ—¬λŸ¬ λ‹€λ₯Έ λ§€κ°œλ³€μˆ˜λ₯Ό λŒ€μž…ν•΄κ°€λ©° 반볡 μ‹€ν–‰ν•  λ•Œ μ‚¬μš©ν•œλ‹€. 자주 μ‚¬μš©λ˜λŠ” μ–΄λ…Έν…Œμ΄μ…˜μ΄λΌ κΈ°μ–΅ν•΄λ‘˜ λ§Œν•˜λ‹€.

 

 

πŸ”² CsvSource

@ParameterizedTest
@CsvSource(value = {"ACE,ACE:12", "ACE,ACE,ACE:13"}, delimiter = ":")
@DisplayName("")
void useParameterizedTest(final String input, final int expected) {
}

 

 

key: valueλŠλ‚ŒμœΌλ‘œ inputκ³Ό expect에 :λ₯Ό κΈ°μ€€μœΌλ‘œ split된 값듀이 λ“€μ–΄μ˜¨λ‹€.

 

 

 

πŸ”² ValueSource

 

@ParameterizedTest
@ValueSource(strings = {"", "  "})
void isBlankTest(String input) {
    assertTrue(Strings.isBlank(input));
}

 

test λ©”μ„œλ“œ μ‹€ν–‰ μ‹œμ— input κ°’μœΌλ‘œ ν•˜λ‚˜μ˜ μΈμˆ˜κ°€ μ „λ‹¬λœλ‹€. μ΄λ•Œ, λ¦¬ν„°λŸ΄ κ°’μ˜ 배열을 ν…ŒμŠ€νŠΈ λ©”μ„œλ“œλ‘œ μ „λ‹¬ν•œλ‹€.

 

 

 

 

πŸ”² Null and Empty Values

 

_@NullSource_λ₯Ό μ‚¬μš©ν•˜λ©΄ 인수둜 null값을 μ „λ‹¬ν•˜κ³ , _@EmptySource_λ₯Ό μ‚¬μš©ν•˜λ©΄ 인수둜 빈 값을 μ „λ‹¬ν•œλ‹€. λ§Œμ•½ _@NullAndEmptySource_λ₯Ό μ‚¬μš©ν•˜λ©΄ nullκ°’κ³Ό 빈 κ°’ λͺ¨λ‘λ₯Ό 전달할 수 μžˆλ‹€.

 

 

 

νŠΉμ΄ν•œ 것은 _@EmptySource_와 _@NullAndEmptySource_λŠ” λ¬Έμžμ—΄, μ»¬λ ‰μ…˜, λ°°μ—΄μ—μ„œλ„ λ™μž‘ν•œλ‹€.

 

 

@ParameterizedTest
@NullAndEmptySource
void isBlankTest(String input) {
    asertTrue(Strings.isBlank(input));
}

 

 

 

πŸ”² Enum

 

 

_@EnumSource_λ₯Ό 톡해 μ—΄κ±°ν˜• κ°’μ˜ 배열을 ν…ŒμŠ€νŠΈ λ©”μ„œλ“œλ‘œ 전달할 수 μžˆλ‹€. μ΄λ•Œ, ν•˜λ‚˜μ˜ μΈμˆ˜λ§Œμ„ μ „λ‹¬ν•œλ‹€.

 

 

 

μ†μ„±μœΌλ‘œλŠ” _name_이 μžˆλŠ”λ°, μ •κ·œ ν‘œν˜„μ‹μ„ 전달할 수 있으며, _mode_λΌλŠ” μ†μ„±μœΌλ‘œ μ œμ™Έν•  enum을 μ§€μ •ν•  μˆ˜λ„ μžˆλ‹€.(EXCLUDE)

 

 

 

@ParameterizedTest
@EnumSource(
  value = Month.class,
  names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER", "FEBRUARY"},
  mode = EnumSource.Mode.EXCLUDE)
void exceptFourMonthsTest(Month month) {
  final boolean isALeapYear = false;
  assertEquals(31, month.length(isALeapYear));
}

 

 

 

 

 

πŸ”Ά @Nested

 

ν…ŒμŠ€νŠΈ 클래슀 μ•ˆμ—μ„œ λ‚΄λΆ€ 클래슀λ₯Ό μ •μ˜ν•˜μ—¬ ν…ŒμŠ€νŠΈλ₯Ό 계측화할 λ•Œ μ‚¬μš©ν•œλ‹€.

 

 

 

 

 

 

Assertions

 

ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€μ˜ μˆ˜ν–‰ κ²°κ³Όλ₯Ό νŒλ³„ν•˜λŠ” λ©”μ„œλ“œμ΄λ‹€. λ˜ν•œ λͺ¨λ“  JUnit Jupiter AssertionsλŠ” static λ©”μ„œλ“œμ΄λ‹€.

 

 

 

import static org.assertj.core.api.Assertions.*;

 

 

Object Assertion

 

public class Dog { 
    private String name; 
    private Float weight;
    
}

Dog fido = new Dog("Fido", 5.25);

Dog fidosClone = new Dog("Fido", 5.25);

 

μ£Όμ–΄μ§„ μ½”λ“œμ— λŒ€ν•΄μ„œ μ•„λž˜μ™€ 같이 ν…ŒμŠ€νŠΈν•  수 μžˆλ‹€.

 

 

assertThat(fido).isEqualTo(fidosClone);

// λ‚΄μš© 자체λ₯Ό λΉ„κ΅ν•˜κΈ° μœ„ν•΄μ„œλŠ” μ•„λž˜ μ‚¬μš©
assertThat(fido).isEqualToComparingFieldByFieldRecursively(fidosClone);

 

 

 

 

 

Boolean Assertion

 

assertThat("".isEmpty()).isTrue();

 

 

 

 

 

 

Iterable/Array Assertion

 

 

πŸ”Ά Iterable/Arrayκ°€ νŠΉμ • μ›μ†Œλ₯Ό μ§€λ‹ˆλŠ” μ§€ ν…ŒμŠ€νŠΈ

List<String> list = Arrays.asList("1", "2", "3");

assertThat(list).contains("1");

 

 

πŸ”ΆλΉˆ 값인 μ§€ 확인

assertThat(list).isNotEmpty();

 

 

 

πŸ”Ά 첫 번째 μ›μ†Œκ°’ 확인

assertThat(list).startsWith("1");

 

 

 

πŸ”Ά 체이닝을 ν†΅ν•œ μ‰¬μš΄ ν…ŒμŠ€νŠΈ

assertThat(list)
  .isNotEmpty()
  .contains("1")
  .doesNotContainNull()
  .containsSequence("2", "3");

 

 

 

 

 

 

Character Assertion

 

 

assertThat(someCharacter)
  .isNotEqualTo('a')
  .inUnicode()
  .isGreaterThanOrEqualTo('b')
  .isLowerCase();

 

 

 

 

 

 

Map Assertion

 

key/valueλ‚˜ entryλ₯Ό μ§€λ‹Œ Map μžλ£Œκ΅¬μ‘°μ— λŒ€ν•΄μ„œλ„ ν…ŒμŠ€νŠΈκ°€ κ°€λŠ₯ν•˜λ‹€.

 

assertThat(map)
  .isNotEmpty()
  .containsKey(2)
  .doesNotContainKeys(10)
  .contains(entry(2, "a"));

 

 

πŸ”Ά assertAll(executables...)

 

맀개 λ³€μˆ˜λ‘œ λ°›λŠ” λͺ¨λ“  ν…ŒμŠ€νŠΈμ½”λ“œλ₯Ό ν•œ λ²ˆμ— μ‹€ν–‰ν•œλ‹€. κ·Έλž˜μ„œ 였λ₯˜κ°€ λ‚˜λ„ λκΉŒμ§€ μ‹€ν–‰ν•œ 후에 ν•œ λ²ˆμ— λͺ¨μ•„μ„œ 좜λ ₯ν•œλ‹€.

 

 

assertAll(
    () -> assertNotNull(study),
    () -> assertEquals(Status.STARTED, study.getStatus(), "처음 μƒνƒœκ°’ DRAFT"),
    () -> assertTrue(study.getLimit() > 0, "μ΅œλŒ€ 인원은 0보닀 μ»€μ•Όν•œλ‹€.")
);

 

 

 

 

 

 

 

 

μ΄μ™Έμ˜ Assertion

 

 

πŸ”Ά assertThrows(extectedType, excutable)

 

μ˜ˆμ™Έ λ°œμƒμ„ ν™•μΈν•˜λŠ” ν…ŒμŠ€νŠΈμ΄λ‹€. 둜직이 μ‹€ν–‰λ˜λŠ” 도쀑 μ˜ˆμ™Έλ₯Ό λ°˜ν™˜λ°›μ•„μ„œ, μ˜ˆμ™Έμ˜ μƒνƒœκΉŒμ§€ μ•Œ 수 μžˆλ‹€.

 

@Test
void exceptionThrow() {
    Exception e = assertThrows(Exception.class, () -> new Test(-10));
    assertDoesNotThrow(() -> System.out.println("Do Something"));
}

 

 

 

체이닝을 ν†΅ν•΄μ„œ μ•„λž˜μ™€ 같이 클래슀의 μ’…λ₯˜λ‚˜ 메세지도 μ•Œμ•„λ‚Ό 수 μžˆλ‹€.

// when, then
assertThatThrownBy(() -> TryCountValidator.validate(input))
        .isInstanceOf(NullPointerException.class)
        .hasMessage("μž…λ ₯값이 곡백이면 μ•ˆλ©λ‹ˆλ‹€.");

 

 

πŸ”Ά assertTimeout(duration, excutable)

 

νŠΉμ • μ‹œκ°„ μ•ˆμ— 싀행이 μ™„λ£Œλ˜λŠ”μ§€ ν™•μΈν•œλ‹€. _Duration_은 μ›ν•˜λŠ” μ‹œκ°„μ΄λ©°, _Executable_은 ν…ŒμŠ€νŠΈν•  λ‘œμ§μ΄λ‹€.

 

@Test
@DisplayName("νƒ€μž„μ•„μ›ƒ μ€€μˆ˜")
void timeoutNotExceeded() {
    assertTimeout(ofMinutes(2), () -> Thread.sleep(10));
}

 

 

 

 

 

πŸ”Ά Assumption

 

μ „μ œλ¬Έμ΄ true라면 μ‹€ν–‰ν•˜κ³ , false라면 μ’…λ£Œν•œλ‹€. CI와 같이 νŠΉμ • ν™˜κ²½μ—μ„œλ§Œ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•˜μ—¬μ•Ό ν•  λ•Œ μ‚¬μš©ν•  수 μžˆλ‹€.

 

πŸ”² assumeTrue : false일 λ•Œ, ν…ŒμŠ€νŠΈ 전체가 μ‹€ν–‰λ˜μ§€ μ•ŠλŠ”λ‹€.

 

πŸ”² assumingThat : νŒŒλΌλ―Έν„°λ‘œ μ „λ‹¬λœ μ½”λ“œλΈ”λŸ­λ§Œ μ‹€ν–‰λ˜μ§€ μ•ŠλŠ”λ‹€.

void some_test() {
    assumeTrue("DEV".equals(System.getenv("ENV")), () -> "개발 ν™˜κ²½μ΄ μ•„λ‹™λ‹ˆλ‹€.");
    assertEquals("A", "A")' // 좜λ ₯ x
}

 

 

 

 

 

 

 

 

References

https://gmlwjd9405.github.io/2019/11/27/junit5-guide-parameterized-test.html

https://www.baeldung.com/introduction-to-assertj

https://www.youtube.com/watch?v=EwI3E9Natcw