AssertJ를 사용한 단위 테스팅 팁

단위 테스트는 개발의 표준 부분이 되었습니다. 다양한 방법으로 활용할 수 있는 많은 도구들이 있습니다. 이 글에서는 저에게 잘 맞는 몇 가지 팁이나, 일종의 모범 사례를 보여드리겠습니다.

이 글에서 배울 내용

NPE 체크 남용하지 마세요

우리는 모두 NullPointerException을 가능한 한 피하려고 노력하는 경향이 있습니다. 하지만 테스트에서 NPE를 피하는 것이 주 관심사가 아닙니다. 우리의 목표는 테스트된 구성 요소의 동작을 깨끗하고, 읽기 좋고, 신뢰할 수 있는 방식으로 확인하는 것입니다.

나쁜 관행

과거에 저는 isNotNull 어설션을 불필요하게 사용했던 적이 많았습니다.

Java

 

@Test
public void getMessage() {
	assertThat(service).isNotNull();
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

이런 테스트는 다음과 같은 오류를 생성합니다:

Plain Text

 

java.lang.AssertionError: 
Expecting actual not to be null
	at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)

좋은 관행

추가적인 isNotNull 어설션은 실제로 해롭지 않지만, 다음과 같은 이유로 피해야 합니다:

  • 이것은 추가적인 가치를 전혀 더하지 않습니다. 단지 읽고 유지 관리해야 할 코드가 더 많아질 뿐입니다.
  • 테스트는 servicenull일 때 어쨌든 실패하고, 실패의 진짜 근본 원인을 보게 됩니다. 그럼에도 테스트는 그 목적을 달성합니다.
  • AssertJ 어설션을 사용하면 오류 메시지가 더욱 향상됩니다.

아래에 수정된 테스트 어설션을 참조하세요.

Java

 

@Test
public void getMessage() {
	assertThat(service.getMessage()).isEqualTo("Hello world!");
}

수정된 테스트는 다음과 같은 오류를 생성합니다:

Java

 

java.lang.NullPointerException: Cannot invoke "com.github.aha.poc.junit.spring.HelloService.getMessage()" because "this.service" is null
	at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)

참고: 예시는 SimpleSpringTest에서 찾을 수 있습니다.

값을 주장하고 결과가 아닌

가끔씩 우리는 올바른 테스트를 작성하지만 “나쁜” 방법으로 작성합니다. 이는 테스트가 원하는대로 정확하게 작동하고 우리의 구성 요소를 확인하지만, 실패 메시지가 충분한 정보를 제공하지 못합니다. 따라서 우리의 목표는 값을 주장하고 비교 결과가 아닌 것입니다.

나쁜 관행

이러한 몇 가지 나쁜 테스트를 살펴보겠습니다:

Java

 

// #1
assertThat(argument.contains("o")).isTrue();

// #2
var result = "Welcome to JDK 10";
assertThat(result instanceof String).isTrue();

// #3
assertThat("".isBlank()).isTrue();

// #4
Optional<Method> testMethod = testInfo.getTestMethod();
assertThat(testMethod.isPresent()).isTrue();

위의 테스트에서 발생한 몇 가지 오류는 다음과 같습니다.

Plain Text

 

#1
Expecting value to be true but was false
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
  
#3
Expecting value to be true but was false
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
	at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)

좋은 관행

AssertJ와 그 것의 fluent API를 사용하면 해결책이 매우 간단합니다. 위에서 언급한 모든 경우를 쉽게 다시 작성할 수 있습니다:

Java

 

// #1
assertThat(argument).contains("o");

// #2
assertThat(result).isInstanceOf(String.class);

// #3
assertThat("").isBlank();

// #4
assertThat(testMethod).isPresent();

이전에 언급한 것과 동일한 오류들이 이제 더 많은 가치를 제공합니다.

Plain Text

 

#1
Expecting actual:
  "Hello"
to contain:
  "f" 
	at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
    
#3
Expecting blank but was: "a"
	at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)

Note: The example can be found in SimpleParamTests.

그룹 관련 단언문 모으기

단언문 연결과 관련된 코드 들여쓰기는 테스트의 명확성과 가독성에 많은 도움을 줍니다.

부적절한 사례

테스트를 작성하다 보면, 올바르지만 덜 읽기 좋은 테스트를 만들어낼 수 있습니다. 국가를 찾고 다음과 같은 검사를 수행하려는 테스트를 상상해 보십시오:

  1. 찾은 국가의 수를 계산합니다.
  2. 첫 번째 항목에 여러 값을 확인합니다.

이러한 테스트는 다음과 같은 예와 같아 보일 수 있습니다:

Java

 

@Test
void listCountries() {
	List<Country> result = ...;

	assertThat(result).hasSize(5);
	var country = result.get(0);
	assertThat(country.getName()).isEqualTo("Spain");
	assertThat(country.getCities().stream().map(City::getName)).contains("Barcelona");
}

좋은 사례

이전 테스트는 올바르지만, 관련 단언문을 함께 그룹화하여(9-11행) 가독성을 크게 개선해야 합니다. 여기서의 목표는 result를 한 번 확인하고 필요한 만큼 많은 연결된 단언문을 작성하는 것입니다. 수정된 버전을 참조하십시오.

Java

 

@Test
void listCountries() {
	List<Country> result = ...;

	assertThat(result)
		.hasSize(5)
		.singleElement()
		.satisfies(c -> {
			assertThat(c.getName()).isEqualTo("Spain");
			assertThat(c.getCities().stream().map(City::getName)).contains("Barcelona");
		});
}

Note: The example can be found in CountryRepositoryOtherTests.

거짓 긍정 성공 테스트 방지

어떤 주장 메서드가 ThrowingConsumer 인수를 사용할 때, 인수는 소비자에서 assertThat를 포함해야 합니다. 그렇지 않으면 테스트는 항상 통과할 것입니다. 비교가 실패해도 말이죠. 이는 잘못된 테스트를 의미합니다. 테스트는 주장이 RuntimeException 또는 AssertionError 예외를 던질 때만 실패합니다. 이해가 되시긴 했겠지만, 잊고 있을 수 있고 잘못된 테스트를 작성하기 쉽습니다. 저도 가끔씩 겪고 있어요.

부적절한 실천

국가 코드 몇 가지가 있다고 상상해 보세요. 우리는 각 코드가 어떤 조건을 만족하는지 확인하고 싶습니다. 가짜 사례에서 우리는 모든 국가 코드에 “a” 문자가 포함되어 있는지 주장하고 싶습니다. 보시다시피, 이건 말이 안 됩니다. 우리는 대문자 코드를 가지고 있지만, 주장에서 대소문자 구분을 적용하지 않고 있습니다.

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> countryCode.contains("a"));
}

놀랍게도, 우리의 테스트는 성공적으로 통과했습니다.

적절한 실천

이 섹션 시작에서 언급했듯이, 우리의 테스트는 소비자에 추가적인 assertThat로 쉽게 수정할 수 있습니다 (7행). 올바른 테스트는 다음과 같아야 합니다.

Java

 

@Test
void assertValues() throws Exception {
	var countryCodes = List.of("CZ", "AT", "CA");
	
	assertThat( countryCodes )
		.hasSize(3)
		.allSatisfy(countryCode -> assertThat( countryCode ).containsIgnoringCase("a"));
}

이제 테스트는 예상대로 올바른 오류 메시지와 함께 실패합니다.

Plain Text

 

java.lang.AssertionError: 
Expecting all elements of:
  ["CZ", "AT", "CA"]
to satisfy given requirements, but these elements did not:

"CZ"
error: 
Expecting actual:
  "CZ"
to contain:
  "a"
 (ignoring case)
	at com.github.aha.sat.core.clr.AppleTest.assertValues(AppleTest.java:45)

체인 주장

마지막 팁은 실천이 아니라 추천에 가깝습니다. AssertJ 유창한 API를 활용하여 더 읽기 좋은 테스트를 만들어야 합니다.

비연결 주장

listLogs 테스트를 고려해 보세요. 이 테스트의 목적은 구성 요소의 로깅을 테스트하는 것입니다. 여기서 목표는 다음을 확인하는 것입니다:

  • 수집된 로그의 주장된 수량
  • 존재를 주장하는 DEBUGINFO 로그 메시지
Java

 

@Test
void listLogs() throws Exception {
	ListAppender<ILoggingEvent> logAppender = ...;
	
	assertThat( logAppender.list ).hasSize(2);
	assertThat( logAppender.list ).anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
			assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
		});
	assertThat( logAppender.list ).anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(INFO);
			assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
		});
}

체인 어설션

말한 플런트 API와 체인을 사용하여 이런 식으로 테스트를 변경할 수 있습니다:

Java

 

@Test
void listLogs() throws Exception {
	ListAppender<ILoggingEvent> logAppender = ...;
	
	assertThat( logAppender.list )
		.hasSize(2)
		.anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
			assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
		})
		.anySatisfy(logEntry -> {
			assertThat( logEntry.getLevel() ).isEqualTo(INFO);
			assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
		});
}

참고: 예제는 다음에서 찾을 수 있습니다 AppleTest.

요약 및 소스 코드

AssertJ 프레임워크는 플런트 API로 많은 도움을 제공합니다. 이 기사에서는 더 명확하고 신뢰할 수 있는 테스트를 생성하기 위한 몇 가지 팁과 힌트를 제시했습니다. 대부분의 이러한 추천 사항은 주관적임에 유의하시기 바랍니다. 개인 선호와 코드 스타일에 따라 다릅니다.

사용된 소스 코드는 내 저장소에서 찾을 수 있습니다:

Source:
https://dzone.com/articles/hints-for-unit-testing-with-assertj