page-cover

title

JUnit ν”„λ ˆμž„μ›Œν¬μ— μ€€λΉ„λ˜μ–΄ μžˆλŠ” @ArgumentProvider κ΅¬ν˜„μ²΄λ₯Ό 잘 μ‚¬μš©ν•˜λ©΄ 거의 λͺ¨λ“  μ‹œλ‚˜λ¦¬μ˜€λ₯Ό ν•΄κ²°ν•  수 μžˆλ‹€. 이 λ¬Έμ„œμ—μ„œλŠ” 직접 μ‚¬μš©ν•œ μΌ€μ΄μŠ€λ§Œ 닀룬닀. ν•„μš”ν•˜λ‹€λ©΄ javadocs에 λ‹€λ₯Έ μ°Έκ³  νŒŒμΌλ“€λ„ 직접 ν™•μΈν•΄λ³΄μž.

300

  1. @ValueSource: κ°€μž₯ 자주 μ‚¬μš©ν•œλ‹€. ν…ŒμŠ€νŠΈμ— 변인이 많으면 μ„€λͺ…λ ₯이 λ–¨μ–΄μ§„λ‹€κ³  μƒκ°ν•˜κΈ° λ•Œλ¬Έμ—, μ˜λ„μ μœΌλ‘œ 인자λ₯Ό ν•˜λ‚˜λ‘œ μœ μ§€ν•˜λŠ” νŽΈμ΄λ‹€.
  2. @NullAndEmptySource: μ‹œμž‘ μΌ€μ΄μŠ€λ‘œ μ ν•©ν•˜λ‹€. λŒ€μ²΄λ‘œ κΈ°λ³Έκ°’κ³Ό λŒ€μ‘λ˜λŠ” κ²½μš°κ°€ λ§Žλ‹€.
  3. @MethodSource: μΌ€μ΄μŠ€λ₯Ό 직접 μž‘μ„±ν•˜κΈ° μ–΄λ €μš΄ κ²½μš°μ— μ ν•©ν•˜λ‹€. μΌ€μ΄μŠ€μ˜ μˆ˜κ°€ λ§Žκ±°λ‚˜ λ³΅μž‘ν•œ 경우, λ‹¨μˆœ λ‚˜μ—΄μ— λΉ„ν•΄ 가독성이 λ†’λ‹€.

[!example]

κΈ°λ³Έ κ΅¬λΆ„μžμ˜ κ·œμΉ™μ—μ„œ 빈 배열을 λ°˜ν™˜ν•˜λŠ” μΌ€μ΄μŠ€λŠ” null λ˜λŠ” λ¬Έμžμ—΄μ΄ isEmpty인 경우 뿐이닀. κ·ΈλŸ¬λ‚˜ μ»€μŠ€ν…€ κ΅¬λΆ„μž κ·œμΉ™μ—μ„œλŠ” λ¬Έμžμ—΄ μ•ž 뢀뢄에 κ·œμΉ™μ΄ μΆ”κ°€λœλ‹€. λ”°λΌμ„œ, κ·œμΉ™μ€ μ‘΄μž¬ν•˜μ§€λ§Œ, 이후 μˆ«μžκ°€ λΉ„μ–΄μžˆλŠ” μƒνƒœλ₯Ό @ValueSource둜 μΆ”κ°€ν–ˆλ‹€.

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"//.\\n"})
void testParse_empty(String expression) {
    var actual = delimiter.parse(expression);
    assertThat(actual).isEmpty();
}

16μ§„μˆ˜ 0xFλŠ” 4개의 λΉ„νŠΈ(0b1111)둜 ν‘œν˜„λœλ‹€. λ”°λΌμ„œ, charκ°€ κ°€μ§ˆ 수 μžˆλŠ” λͺ¨λ“  λ²”μœ„λ₯Ό νƒμƒ‰ν•˜κΈ° μœ„ν•΄μ„œλŠ” 2 Byte둜 ν‘œν˜„ν•  수 μžˆλŠ”, 0xFFFFκΉŒμ§€μ˜ 문자λ₯Ό ν™•μΈν•˜λ©΄ λœλ‹€.1

[!example]

λ‹€μŒ ν…ŒμŠ€νŠΈλŠ” basic latin λ²”μœ„μ— λŒ€ν•΄μ„œλŠ” @ParameterizedTest, 그리고 μ΄ν›„λ‘œλŠ” 일반 @Testλ₯Ό ν™œμš©ν•˜μ—¬ char λ²”μœ„ 전역에 λŒ€ν•œ parse λ©”μ„œλ“œμ˜ μœ νš¨μ„±μ„ νŒλ³„ν•˜λŠ” μ˜ˆμ œμ΄λ‹€. 2

private static IntStream basicLatin() {
	return IntStream.rangeClosed(0x00, 0x7F) // [0, 127]
			.filter(value -> value != ',' && value != ':')
			.filter(value -> value < '0' || '9' < value);
}

@ParameterizedTest
@MethodSource(value = "basicLatin")
void testParse_function(int code) {
	var expression = String.format("//%c\\n1%c2%c3", code, code, code);
	var actual = delimiter.parse(expression);
	assertThat(actual).isEqualTo(List.of(1, 2, 3));
}

@Test
void testParse_BMPWithoutBasicLatin() {
	var codes = IntStream.rangeClosed(0x80, 0xFFFF);
	codes.forEach(this::testParse_function);
}

ν…ŒμŠ€νŠΈ 쀑 λ¬Έμ œκ°€ λ˜λŠ” 문자의 μœ ν˜•μ„ 크게 μ„Έ κ°€μ§€λ‘œ λΆ„λ₯˜ν•  수 μžˆμ—ˆλ‹€.

  1. μš”κ΅¬μ‚¬ν•­μ—μ„œ λͺ…μ‹œν•œ μ˜ˆμ™Έ μΌ€μ΄μŠ€: κΈ°λ³Έ κ΅¬λΆ„μžλŠ” μ»€μŠ€ν…€ κ΅¬λΆ„μžκ°€ 될 수 μ—†λ‹€λŠ” 도메인 κ·œμΉ™μ— μ˜ν•œ μ˜ˆμ™Έμ΄λ‹€.
  2. 숫자 (0 ~ 9): 10진법을 ν™œμš©ν•œλ‹€λ©΄ λŒ€λ‹€μˆ˜μ˜ μΌ€μ΄μŠ€μ—μ„œ 숫자 λ¬ΈμžλŠ” κ΅¬λΆ„μžλ‘œμ„œμ˜ 역할을 μ œλŒ€λ‘œ μˆ˜ν–‰ν•  수 μ—†λ‹€. λ”°λΌμ„œ, μ΄λŸ¬ν•œ μΌ€μ΄μŠ€λŠ” 자의적으둜 λ°°μ œν•˜μ˜€λ‹€.

[!example]

λ‹€μŒ ν…ŒμŠ€νŠΈλŠ” 숫자 λ˜λŠ” κΈ°λ³Έ κ΅¬λΆ„μžκ°€ μ‚¬μš©λ˜λ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€λŠ” 사싀을 ν‘œν˜„ν•œλ‹€.

private static IntStream charRangeNumbers() {
	return IntStream.rangeClosed('0', '9');
}

@ParameterizedTest
@MethodSource(value = "charRangeNumbers")
@ValueSource(ints = {',', ':'})
void testParse_invalidCustomDelimiterThrows(int code) {
	var expression = String.format("//%c\\n1%c2%c3", code, code, code);
	assertThrows(IllegalArgumentException.class, () -> delimiter.parse(expression));
}
  1. μ •κ·œ ν‘œν˜„μ‹ 문법
    • ν‘œν˜„μ‹ 문법에 μ‚¬μš©λ˜λŠ” λ¬Έμžλ“€μ„ 비ꡐ식에 λ°”λ‘œ μ‚½μž…ν•˜λ©΄ 문법적 였λ₯˜κ°€ λ°œμƒν•  수 μžˆλ‹€. λ”°λΌμ„œ, Pattern.quote둜 λž˜ν•‘ν•  ν•„μš”κ°€ μžˆλ‹€.
    • μΆ”κ°€λ‘œ DOT을 ν™œμš©ν•˜λŠ” 경우, μ˜ˆμ™Έμ μœΌλ‘œ line terminator λ¬Έμžλ“€μ€ 탐지 λŒ€μƒμ—μ„œ μ œμ™Έν•œλ‹€. 이 뢀뢄은 DOTALL ν”Œλž˜κ·Έλ₯Ό μ‚¬μš©ν•˜λ©΄ λ™μž‘μ„ λ³€κ²½ν•  수 μžˆλ‹€. μžμ„Έν•œ λ‚΄μš©μ€ Pattern μ°Έκ³ .

Info: surrogate pair

[!info] Java의 charλŠ” UTF-16 기반의 2 Byte μžλ£Œν˜•μ΄λ‹€. 이 μ•ˆμ— 세상 λͺ¨λ“  문자λ₯Ό 담을 수 μ—†μŒμ„ κΉ¨λ‹«κ³ , surrogate pairλΌλŠ” κ°œλ…μ„ ν™œμš©ν•΄μ„œ ν‘œν˜„ λ²”μœ„λ₯Ό ν™•μž₯ν–ˆλ‹€. codePointAt은 UTF-16이 κΈ°λ³Έμ΄μ§€λ§Œ, String의 μƒμ„±μžλŠ” UTF-8이 기본이닀. 인코딩 νƒ€μž…μ— λ”°λ₯Έ ν™œμš© λ°©μ‹μ˜ 차이가 λ“œλŸ¬λ‚˜λ„λ‘ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•΄λ³΄μ•˜λ‹€.

private static IntStream emoji() {
	Supplier<Integer> utf8Code = () -> {
		byte[] utf8Bytes = new byte[]{(byte) 0xF0, (byte) 0x9F, (byte) 0xA4, (byte) 0x94};
		// String constructor use default charset UTF-8
		return new String(utf8Bytes).codePointAt(0);
	};

	return IntStream.of("πŸ€”".codePointAt(0), // internally treated as UTF-16
			"\uD83E\uDD14".codePointAt(0), // UTF-16
			0x0001_F914, // UTF-32
			utf8Code.get() // UTF-8
	);
}

/**
 * BMP(Basic Multilingual Plane) μ™ΈλΆ€ 문자의 surrogate pair λ§€μΉ­ μΌ€μ΄μŠ€
 */
@ParameterizedTest
@MethodSource("emoji")
void testParse_withSurrogatePair(int code) {
	var expression = String.format("//%c\\n1%c2%c3", code, code, code);
	assertEquals("//πŸ€”\\n1πŸ€”2πŸ€”3", expression);
	var actual = delimiter.parse(expression);
	assertThat(actual).isEqualTo(List.of(1, 2, 3));
}

Footnotes

  1. λ¬Όλ‘  0xFFFFκΉŒμ§€ ν™•μΈν•˜λŠ” 것도 ν•˜λ‚˜μ˜ λ¬Έμžμ— λŒ€ν•œ κ²€μ¦μœΌλ‘œ μ™„μ „ν•˜μ§€λŠ” μ•Šλ‹€. λ¬Έμžλ“€μ€ 인코딩 방식에 따라 λ™μΌν•œ λ¬Έμžλ„ λ‹€λ₯Έ λ°”μ΄νŠΈ μ½”λ“œλ‘œ μž…λ ₯될 수 있기 λ•Œλ¬Έμ΄λ‹€. link ↩

  2. 일반 Test와 νŒŒλΌλ―Έν„° ν…ŒμŠ€νŠΈλ₯Ό ν˜Όμš©ν•œ 것은 μ˜λ„μ μ΄λ‹€. μ΄μœ λŠ” ν…ŒμŠ€νŠΈ κ²°κ³Όλ₯Ό ν™•μΈν•˜λŠ” 데 μ§€λ‚˜μΉ˜κ²Œ μ‹œκ°„μ΄ 많이 μ†Œμš”λ˜κΈ° λ•Œλ¬Έ. ν˜„μž¬ 개발 ν™˜κ²½μ—μ„œ 0xFFFFκΉŒμ§€ μ•½ 6만 5천개의 ν…ŒμŠ€νŠΈλ₯Ό μ²˜λ¦¬ν•˜λŠ”λ° μ•½ 23μ΄ˆκ°€ μ†Œμš”λœλ‹€. ν…ŒμŠ€νŠΈ 싀행에 ν•„μš”ν•œ μ€€λΉ„ μž‘μ—…λ“€μ΄ 짧더라도, ν…ŒμŠ€νŠΈ κ°œμˆ˜κ°€ μ΄λ ‡κ²Œ 많으면 였래 걸릴 수 밖에 μ—†λ‹€. λ”°λΌμ„œ, basic latin plane μ΄ν›„λ‘œλŠ” μ˜€λ²„ν—€λ“œκ°€ 생기지 μ•Šλ„λ‘ λ°˜λ³΅λ¬Έμ„ λŒλ Έλ”λ‹ˆ 600~800ms 정도 λ‚˜μ˜¨λ‹€. λͺ»ν•΄λ„ 30λ°° 이상 μ°¨μ΄λ‚˜λŠ” μ…ˆ. ↩