강력한 유닛 및 JUnit과의 통합 테스트 가이드

게시 됨: 2022-03-11

자동화된 소프트웨어 테스트는 소프트웨어 프로젝트의 장기적인 품질, 유지 관리 가능성 및 확장성에 매우 중요하며 Java의 경우 JUnit은 자동화의 경로입니다.

이 기사의 대부분은 강력한 단위 테스트를 작성하고 스터빙, 조롱 및 종속성 주입을 활용하는 데 초점을 맞추지만 JUnit 및 통합 테스트에 대해서도 설명합니다.

JUnit 테스트 프레임워크는 Java 기반 프로젝트를 테스트하기 위한 일반적인 무료 오픈 소스 도구입니다.

이 글을 쓰는 시점에서 JUnit 4는 10년 이상 전에 출시된 현재의 주요 릴리스이며 마지막 업데이트는 2년 이상 된 것입니다.

JUnit 5(Jupiter 프로그래밍 및 확장 모델 포함)는 현재 개발 중입니다. Java 8에 도입된 언어 기능을 더 잘 지원하고 기타 새롭고 흥미로운 기능을 포함합니다. 일부 팀은 JUnit 5를 사용할 준비가 되었음을 알 수 있고 다른 팀은 5가 공식적으로 출시될 때까지 JUnit 4를 계속 사용할 수 있습니다. 우리는 둘 다의 예를 살펴볼 것입니다.

JUnit 실행

JUnit 테스트는 IntelliJ에서 직접 실행할 수 있지만 Eclipse, NetBeans 또는 명령줄과 같은 다른 IDE에서도 실행할 수 있습니다.

테스트, 특히 단위 테스트는 항상 빌드 시 실행해야 합니다. 테스트에 실패한 빌드는 문제가 프로덕션에 있는지 테스트 코드에 있는지에 관계없이 실패한 것으로 간주되어야 합니다. 이를 위해서는 팀의 규율과 실패한 테스트를 해결하는 데 가장 높은 우선 순위를 부여하려는 의지가 필요하지만 다음을 준수해야 합니다. 자동화 정신.

JUnit 테스트는 Jenkins와 같은 지속적 통합 시스템에서 실행하고 보고할 수도 있습니다. Gradle, Maven 또는 Ant와 같은 도구를 사용하는 프로젝트에는 빌드 프로세스의 일부로 테스트를 실행할 수 있다는 추가 이점이 있습니다.

호환성을 나타내는 기어 그룹: 하나에는 NetBeans가 포함된 JUnit 4, 다른 하나에는 Eclipse 및 Gradle이 포함된 JUnit 5, Maven 및 IntelliJ IDEA가 포함된 JUnit 5가 있습니다.

그라들

JUnit 5용 샘플 Gradle 프로젝트로 JUnit 사용자 가이드의 Gradle 섹션과 junit5-samples.git 저장소를 참조하세요. JUnit 4 API( "빈티지" 라고 함)를 사용하는 테스트도 실행할 수 있습니다.

IntelliJ에서 파일 > 열기… > junit-gradle-consumer sub-directory 이동 > 확인 > 프로젝트로 열기 > 확인을 통해 IntelliJ에서 프로젝트를 생성하여 Gradle에서 프로젝트를 가져올 수 있습니다.

Eclipse의 경우 Buildship Gradle 플러그인은 Help > Eclipse Marketplace...에서 설치할 수 있습니다. 그런 다음 파일 > 가져오기… > Gradle > Gradle 프로젝트 > 다음 > 다음 > junit-gradle-consumer 하위 디렉토리로 이동 > 다음을 사용하여 프로젝트를 가져올 수 있습니다. > 다음 > 마침.

IntelliJ 또는 Eclipse에서 Gradle 프로젝트를 설정한 후 Gradle build 작업을 실행하면 test 작업과 함께 모든 JUnit 테스트 실행이 포함됩니다. 코드가 변경되지 않은 경우 build 의 후속 실행에서 테스트를 건너뛸 수 있습니다.

JUnit 4의 경우 JUnit의 Gradle 위키 사용을 참조하세요.

메이븐

JUnit 5의 경우 사용자 가이드의 Maven 섹션과 Maven 프로젝트의 예는 junit5-samples.git 저장소를 참조하십시오. 이것은 또한 빈티지 테스트(JUnit 4 API를 사용하는 테스트)를 실행할 수 있습니다.

IntelliJ에서 파일 > 열기… > junit-maven-consumer/pom.xml > 확인 > 프로젝트로 열기를 사용합니다. 그런 다음 테스트는 Maven 프로젝트 > junit5-maven-consumer > 수명 주기 > 테스트에서 실행할 수 있습니다.

Eclipse에서 File > Import… > Maven > Existing Maven Projects > Next > junit-maven-consumer 디렉토리로 이동 > pom.xml 을 선택한 상태에서 > Finish를 사용합니다.

테스트는 Maven 빌드로 프로젝트를 실행하여... > test 목표 지정 > 실행으로 실행할 수 있습니다.

JUnit 4의 경우 Maven 저장소의 JUnit을 참조하십시오.

개발 환경

Gradle 또는 Maven과 같은 빌드 도구를 통해 테스트를 실행하는 것 외에도 많은 IDE에서 JUnit 테스트를 직접 실행할 수 있습니다.

IntelliJ 아이디어

JUnit 5 테스트에는 IntelliJ IDEA 2016.2 이상이 필요하지만 JUnit 4 테스트는 이전 IntelliJ 버전에서 작동해야 합니다.

이 기사의 목적을 위해 간단한 Person 클래스 예제의 모든 파일을 포함하고 기본 제공 JUnit 라이브러리. 테스트는 실행 > '모든 테스트' 실행으로 실행할 수 있습니다. 테스트는 PersonTest 클래스의 IntelliJ에서도 실행할 수 있습니다.

이 리포지토리는 새로운 IntelliJ Java 프로젝트로 생성되었으며 디렉토리 구조 src/main/java/com/examplesrc/test/java/com/example 을 구축합니다. src/main/java 디렉토리는 소스 폴더로 지정되었고 src/test/java 는 테스트 소스 폴더로 지정되었습니다. @Test 주석이 달린 테스트 메서드로 PersonTest 클래스를 만든 후 컴파일에 실패할 수 있습니다. 이 경우 IntelliJ는 IntelliJ IDEA 배포판에서 로드할 수 있는 클래스 경로에 JUnit 4 또는 JUnit 5를 추가하라는 제안을 제공합니다. 자세한 내용은 스택 오버플로에 대한 답변). 마지막으로 모든 테스트에 대해 JUnit 실행 구성이 추가되었습니다.

IntelliJ 테스트 방법 지침도 참조하십시오.

Eclipse의 빈 Java 프로젝트에는 테스트 루트 디렉토리가 없습니다. 이것은 프로젝트 속성 > Java 빌드 경로 > 폴더 추가… > 새 폴더 생성… > 폴더 이름 지정 > 마침에서 추가되었습니다. 새 디렉토리가 소스 폴더로 선택됩니다. 나머지 두 대화 상자에서 모두 확인을 클릭합니다.

JUnit 4 테스트는 파일 > 새로 만들기 > JUnit 테스트 케이스로 생성할 수 있습니다. "New JUnit 4 test"를 선택하고 테스트를 위해 새로 생성된 소스 폴더를 선택합니다. 패키지가 테스트 중인 클래스와 일치하는지 확인하여 "테스트 중인 클래스" 및 "패키지"를 지정합니다. 그런 다음 테스트 클래스의 이름을 지정합니다. 마법사를 마친 후 메시지가 표시되면 빌드 경로에 "JUnit 4 라이브러리 추가"를 선택합니다. 그런 다음 프로젝트 또는 개별 테스트 클래스를 JUnit 테스트로 실행할 수 있습니다. Eclipse 작성 및 JUnit 테스트 실행도 참조하십시오.

넷빈

NetBeans는 JUnit 4 테스트만 지원합니다. 테스트 클래스는 파일 > 새 파일… > 단위 테스트 > JUnit 테스트 또는 기존 클래스에 대한 테스트를 사용하여 NetBeans Java 프로젝트에서 생성할 수 있습니다. 기본적으로 테스트 루트 디렉터리의 이름은 프로젝트 디렉터리에서 test 입니다.

단순 프로덕션 클래스와 JUnit 테스트 케이스

프로덕션 코드의 간단한 예와 매우 간단한 Person 클래스에 대한 해당 단위 테스트 코드를 살펴보겠습니다. 내 github 프로젝트에서 샘플 코드를 다운로드하고 IntelliJ를 통해 열 수 있습니다.

src/main/java/com/example/Person.java
 package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ", " + givenName; } }

변경할 수 없는 Person 클래스에는 생성자와 getDisplayName() 메서드가 있습니다. getDisplayName() 이 예상대로 형식화된 이름을 반환하는지 테스트하려고 합니다. 다음은 단일 단위 테스트(JUnit 5)에 대한 테스트 코드입니다.

src/test/java/com/example/PersonTest.java
 package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person("Josh", "Hayden"); String displayName = person.getDisplayName(); assertEquals("Hayden, Josh", displayName); } }

PersonTest 는 JUnit 5의 @Test 및 어설션을 사용합니다. JUnit 4의 경우 PersonTest 클래스와 메서드는 공개되어야 하고 다른 가져오기를 사용해야 합니다. 다음은 JUnit 4 예제 Gist입니다.

IntelliJ에서 PersonTest 클래스를 실행하면 테스트가 통과하고 UI 표시기가 녹색입니다.

일반적인 JUnit 규칙

네이밍

필수는 아니지만 테스트 클래스의 이름을 지정할 때 일반적인 규칙을 사용합니다. 구체적으로, 테스트 중인 클래스의 이름( Person )으로 시작하고 "Test"를 추가합니다( PersonTest ). 테스트 메서드의 이름 지정은 테스트 중인 메서드( getDisplayName() )로 시작하여 "test"를 앞에 붙입니다( testGetDisplayName() ). 테스트 메서드 이름 지정에 대해 완벽하게 허용되는 다른 규칙이 많이 있지만 팀과 프로젝트 전체에서 일관성을 유지하는 것이 중요합니다.

프로덕션의 이름 테스트의 이름
사람 사람 테스트
getDisplayName() testDisplayName()

패키지

또한 프로덕션 코드의 Person 클래스와 동일한 패키지( com.example )에 테스트 코드 PersonTest 클래스를 생성하는 규칙을 사용합니다. 테스트에 다른 패키지를 사용했다면 적절하지 않은 경우에도 프로덕션 코드 클래스, 생성자 및 단위 테스트에서 참조하는 메서드에서 public 액세스 수정자를 사용해야 하므로 동일한 패키지에 유지하는 것이 좋습니다. . 그러나 일반적으로 릴리스된 프로덕션 빌드에 테스트 코드를 포함하지 않기 때문에 별도의 소스 디렉토리( src/main/javasrc/test/java )를 사용합니다.

구조 및 주석

@Test 주석(JUnit 4/5)은 JUnit에 testGetDisplayName() 메서드를 테스트 메서드로 실행하고 통과 또는 실패 여부를 보고하도록 지시합니다. 모든 어설션(있는 경우)이 통과하고 예외가 발생하지 않는 한 테스트는 통과한 것으로 간주됩니다.

테스트 코드는 Arrange-Act-Assert(AAA)의 구조 패턴을 따릅니다. 다른 일반적인 패턴에는 Given-When-Then 및 Setup-Exercise-Verify-Teardown(Teardown은 일반적으로 단위 테스트에 명시적으로 필요하지 않음)이 포함되지만 이 기사에서는 AAA를 사용합니다.

테스트 예제가 AAA를 어떻게 따르는지 살펴보겠습니다. 첫 번째 줄인 "arrange"는 테스트할 Person 객체를 생성합니다.

 Person person = new Person("Josh", "Hayden");

두 번째 줄인 "act"는 프로덕션 코드의 Person.getDisplayName Person.getDisplayName() 메서드를 실행합니다.

 String displayName = person.getDisplayName();

세 번째 줄인 "assert"는 결과가 예상대로인지 확인합니다.

 assertEquals("Hayden, Josh", displayName);

내부적으로 assertEquals() 호출은 "Hayden, Josh" String 객체의 equals 메서드를 사용하여 프로덕션 코드( displayName )에서 반환된 실제 값이 일치하는지 확인합니다. 일치하지 않으면 테스트가 실패로 표시되었을 것입니다.

테스트에는 이러한 AAA 단계 각각에 대해 하나 이상의 라인이 있는 경우가 많습니다.

단위 테스트 및 프로덕션 코드

이제 몇 가지 테스트 규칙을 다루었으므로 프로덕션 코드를 테스트 가능하게 만드는 방법에 대해 알아보겠습니다.

우리는 사람의 생년월일을 기반으로 사람의 나이를 반환하는 메서드를 구현한 Person 클래스로 돌아갑니다. 코드 예제에서는 Java 8이 새로운 날짜 및 기능 API를 활용해야 합니다. 새로운 Person.java 클래스는 다음과 같습니다.

사람.자바
 // ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }

이 클래스를 실행하면(작성 당시) Joey가 4세임을 알 수 있습니다. 테스트 메서드를 추가해 보겠습니다.

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }

오늘은 지나가지만 1년 뒤에 실행하면 어떨까요? 예상 결과는 테스트를 실행하는 시스템의 현재 날짜에 따라 달라지므로 이 테스트는 비결정적이며 깨지기 쉽습니다.

가치 공급자 스터빙 및 주입

프로덕션 환경에서 실행할 때 사람의 나이를 계산하기 위해 현재 날짜 LocalDate.now() 를 사용하고 싶지만 지금부터 1년 후에도 결정론적 테스트를 하려면 테스트가 자체 currentDate 값을 제공해야 합니다.

이것을 의존성 주입이라고 합니다. 우리는 Person 객체가 현재 날짜 자체를 결정하는 것을 원하지 않지만 대신 이 논리를 종속성으로 전달하기를 원합니다. 단위 테스트는 알려진 스텁 값을 사용하며 프로덕션 코드에서는 런타임에 시스템에서 실제 값을 제공할 수 있습니다.

Person.javaLocalDate 공급자를 추가해 보겠습니다.

사람.자바
 // ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }

getAge() 메서드를 더 쉽게 테스트할 수 있도록 현재 날짜를 검색하기 위해 LocalDate 공급자인 currentDateSupplier 를 사용하도록 변경했습니다. 공급업체가 무엇인지 모르는 경우 Lambda 내장 기능 인터페이스에 대해 읽어보는 것이 좋습니다.

또한 종속성 주입을 추가했습니다. 새로운 테스트 생성자는 테스트가 자체 현재 날짜 값을 제공할 수 있도록 합니다. 원래 생성자는 이 새 생성자를 호출하여 LocalDate 개체를 제공하는 LocalDate::now 의 정적 메서드 참조를 전달하므로 기본 메서드는 여전히 이전과 같이 작동합니다. 우리의 테스트 방법은 어떻습니까? PersonTest.java 를 업데이트합시다.

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse("2013-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-17"); Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }

테스트는 이제 자체 currentDate 값을 주입하므로 테스트는 내년 또는 어느 해에 실행될 때 여전히 통과할 것입니다. 이것은 일반적으로 stubbing 또는 반환될 알려진 값을 제공한다고 하지만 먼저 이 종속성을 주입할 수 있도록 Person 을 변경해야 했습니다.

Person 객체를 생성할 때 람다 구문( ()->currentDate )에 유의하십시오. 이것은 새 생성자에서 요구하는 대로 LocalDate 의 공급자로 처리됩니다.

웹 서비스 조롱 및 스터빙

전체 존재가 JVM 메모리에 있는 Person 객체가 외부 세계와 통신할 준비가 되었습니다. 사람의 현재 나이를 게시하는 getThoseInCommon() 메서드와 생일이 같거나 같은 나이 Person 유명인의 이름을 반환하는 getThoseInCommon( publishAge() 메서드의 두 가지 메서드를 추가하려고 합니다. "People Birthday"라는 상호 작용할 수 있는 RESTful 서비스가 있다고 가정합니다. 단일 클래스 BirthdaysClient 로 구성된 Java 클라이언트가 있습니다.

com.example.birthdays.BirthdaysClient
 package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println("publishing " + name + "'s age: " + age); // HTTP POST with name and age and possibly throw an exception } public Collection<String> findFamousNamesOfAge(long age) throws IOException { System.out.println("finding famous names of age " + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }

Person 클래스를 향상시키자. publishAge() 의 원하는 동작에 대한 새 테스트 메서드를 추가하는 것으로 시작합니다. 기능보다 테스트를 시작하는 이유는 무엇입니까? 우리는 테스트 주도 개발(TDD라고도 함)의 원칙을 따르고 있습니다. 여기서는 테스트를 먼저 작성한 다음 테스트를 통과하도록 코드를 작성합니다.

PersonTest.java
 // … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate); person.publishAge(); } }

이 시점에서 테스트 코드는 호출하는 publishAge() 메서드를 생성하지 않았기 때문에 컴파일에 실패합니다. 빈 Person.publishAge() 메서드를 생성하면 모든 것이 통과합니다. 이제 사람의 나이가 실제로 BirthdaysClient 에 게시되는지 확인하기 위한 테스트가 준비되었습니다.

모의 객체 추가

이것은 단위 테스트이므로 메모리에서 빠르게 실행되어야 하므로 테스트는 실제 웹 요청을 하지 않도록 모의 BirthdaysClient 를 사용하여 Person 개체를 구성합니다. 그런 다음 테스트는 이 모의 객체를 사용하여 예상대로 호출되었는지 확인합니다. 이를 위해 모의 객체를 생성하기 위한 Mockito 프레임워크(MIT 라이선스)에 대한 종속성을 추가한 다음 모의 BirthdaysClient 객체를 생성합니다.

PersonTest.java
 // ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); // ... } }

또한 BirthdaysClient 개체를 사용하도록 Person 생성자의 서명을 보강하고 조롱된 BirthdaysClient 개체를 주입하도록 테스트를 변경했습니다.

모의 기대 추가

다음으로, 우리는 testPublishAge 의 끝에 BirthdaysClient 가 호출된다는 기대를 추가합니다. 새로운 PersonTest.java 에 표시된 것처럼 Person.publishAge() 가 이를 호출해야 합니다.

PersonTest.java
 // ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); } }

Mockito로 강화된 BirthdaysClient 는 메서드에 대한 모든 호출을 추적합니다. 이를 통해 publishAge()를 호출하기 전에 publishAge() verifyZeroInteractions() 메서드로 BirthdaysClient 에 대한 호출이 수행되지 않았는지 확인합니다. 반드시 필요한 것은 아니지만 이렇게 함으로써 생성자가 악성 호출을 하지 않도록 할 수 있습니다. verify() 행에서 BirthdaysClient 에 대한 호출이 표시되는 방식을 지정합니다.

publishRegularPersonAge의 서명에 IOException이 있으므로 테스트 메서드 서명에도 추가합니다.

이 시점에서 테스트는 실패합니다.

 Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)

테스트 주도 개발을 따르고 있기 때문에 Person.java 에 필요한 변경 사항을 아직 구현하지 않았다는 점을 감안할 때 이는 예상된 결과입니다. 이제 필요한 변경을 수행하여 이 테스트를 통과할 것입니다.

사람.자바
 // ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + " " + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }

예외 테스트

프로덕션 코드 생성자가 새 BirthdaysClient 인스턴스를 생성하고 publishAge() 가 이제 birthdaysClient 를 호출합니다. 모든 테스트를 통과합니다. 모든 것이 녹색입니다. 엄청난! 그러나 publishAge() 가 IOException을 삼키는 것을 주목하십시오. 거품을 내도록 하는 대신 PersonException.java 라는 새 파일에서 자체 PersonException으로 랩핑하려고 합니다.

PersonException.java
 package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }

이 시나리오를 PersonTest.java 에서 새로운 테스트 메소드로 구현합니다.

PersonTest.java
 // ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); try { person.publishAge(); fail("expected exception not thrown"); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage()); } } }

Mockito doThrow() 호출 스텁 birthdaysClientpublishRegularPersonAge() 메서드가 호출될 때 예외를 발생시킵니다. PersonException 이 발생하지 않으면 테스트에 실패합니다. 그렇지 않으면 우리는 예외가 IOException과 적절하게 연결되었다고 주장하고 예외 메시지가 예상대로인지 확인합니다. 지금 당장은 프로덕션 코드에서 처리를 구현하지 않았기 때문에 예상한 예외가 발생하지 않았기 때문에 테스트가 실패했습니다. 테스트를 통과하기 위해 Person.java 에서 변경해야 하는 사항은 다음과 같습니다.

사람.자바
 // ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }

스텁: 시기와 주장

이제 Person.getThoseInCommon() 메서드를 구현하여 Person.Java 클래스를 다음과 같이 만듭니다.

testGetThoseInCommon() 와 달리 testPublishAge()birthdaysClient 메서드에 대한 특정 호출이 있는지 확인하지 않습니다. 대신 스텁에 대한 호출이 getThoseInCommon getThoseInCommon() 이 수행해야 하는 findFamousNamesOfAge findFamousNamesOfAge()findFamousNamesBornOn() 에 대한 호출에 대한 값을 반환 when 사용합니다. 그런 다음 우리가 제공한 3개의 스텁된 이름이 모두 반환되었다고 주장합니다.

assertAll() JUnit 5 메서드를 사용하여 여러 주장을 래핑하면 첫 번째 실패한 주장 이후에 중지하지 않고 모든 주장을 전체적으로 확인할 수 있습니다. 또한 포함되지 않은 특정 이름을 식별하기 위해 assertTrue() 메시지를 포함합니다. 다음은 우리의 "행복한 경로"(이상적인 시나리오) 테스트 방법의 모습입니다(참고로 이것은 "행복한 경로"의 본질적인 테스트 세트가 아니지만 그 이유는 나중에 설명하겠습니다.

PersonTest.java
 // ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person")); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown")); Set<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }

테스트 코드를 깨끗하게 유지

종종 간과되기는 하지만 테스트 코드가 중복되지 않도록 유지하는 것도 마찬가지로 중요합니다. "자신을 반복하지 마십시오"와 같은 깨끗한 코드와 원칙은 고품질 코드베이스, 프로덕션 및 테스트 코드를 모두 유지하는 데 매우 중요합니다. 가장 최근의 PersonTest.java에 몇 가지 테스트 메소드가 있으므로 중복이 있음을 주목하십시오.

이 문제를 해결하기 위해 다음과 같은 몇 가지 작업을 수행할 수 있습니다.

  • IOException 개체를 개인 최종 필드로 추출합니다.

  • 대부분의 Person 객체가 동일한 매개변수로 생성되기 때문에 Person 객체 생성을 자체 메서드(이 경우 createJoeSixteenJan2() 로 추출합니다.

  • throw PersonExceptions 을 확인하는 다양한 테스트에 대해 assertCauseAndMessage() 를 만듭니다.

깨끗한 코드 결과는 PersonTest.java 파일의 이 변환에서 볼 수 있습니다.

행복한 길 그 이상을 테스트하십시오

Person 객체의 생년월일이 현재 날짜보다 늦은 경우 어떻게 해야 합니까? 응용 프로그램의 결함은 종종 예기치 않은 입력이나 모서리, 가장자리 또는 경계 사례에 대한 예측 부족으로 인해 발생합니다. 우리가 할 수 있는 한 최선을 다해 이러한 상황을 예상하는 것이 중요하며, 단위 테스트는 종종 그렇게 하기에 적절한 장소입니다. PersonPersonTest 를 빌드할 때 예상되는 예외에 대한 몇 가지 테스트를 포함했지만 결코 완전한 것은 아닙니다. 예를 들어 시간대 데이터를 나타내거나 저장하지 않는 LocalDate 를 사용합니다. 그러나 LocalDate.now() 에 대한 호출은 시스템의 기본 시간대를 기반으로 LocalDate 를 반환합니다. 이 시간대는 시스템 사용자의 시간대보다 하루 빠르거나 늦을 수 있습니다. 이러한 요소는 적절한 테스트와 구현된 동작으로 고려되어야 합니다.

경계도 테스트해야 합니다. getDaysUntilBirthday() 메소드가 있는 Person 객체를 고려하십시오. 테스트에는 올해에 그 사람의 생일이 이미 지났는지 여부, 생일이 오늘인지, 윤년이 일 수에 미치는 영향이 포함되어야 합니다. 이러한 시나리오는 다음 해가 윤년인 사람의 생일 하루 전, 생일 당일, 생일 후 하루를 확인하여 커버할 수 있습니다. 다음은 관련 테스트 코드입니다.

PersonTest.java
 // ... class PersonTest { private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02"); private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01"); private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02"); private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03"); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }

통합 테스트

우리는 주로 단위 테스트에 중점을 두었지만 JUnit은 통합, 수락, 기능 및 시스템 테스트에도 사용할 수 있습니다. 이러한 테스트에는 종종 더 많은 설정 코드가 필요합니다(예: 서버 시작, 알려진 데이터가 있는 데이터베이스 로드 등). 수천 개의 단위 테스트를 몇 초 만에 실행할 수 있지만 대규모 통합 테스트 제품군을 실행하는 데 몇 분 또는 몇 시간이 걸릴 수 있습니다. 통합 테스트는 일반적으로 코드를 통한 모든 순열이나 경로를 다루기 위해 사용되어서는 안 됩니다. 단위 테스트가 더 적합합니다.

양식 채우기, 버튼 클릭, 콘텐츠 로드 대기 등의 웹 브라우저를 구동하는 웹 애플리케이션에 대한 테스트 생성은 일반적으로 '페이지 개체 패턴'(SeleniumHQ github wiki 참조)과 결합된 Selenium WebDriver(Apache 2.0 라이선스)를 사용하여 수행됩니다. 및 페이지 개체에 대한 Martin Fowler의 기사).

JUnit은 Apache HTTP 클라이언트 또는 Spring Rest Template과 같은 HTTP 클라이언트를 사용하여 RESTful API를 테스트하는 데 효과적입니다(HowToDoInJava.com이 좋은 예를 제공합니다).

Person 객체의 경우 통합 테스트에는 People Birthdays 서비스의 기본 URL을 지정하는 구성으로 모의 객체가 아닌 실제 BirthdaysClient 를 사용하는 것이 포함될 수 있습니다. 그런 다음 통합 테스트는 이러한 서비스의 테스트 인스턴스를 사용하고 생일이 게시되었는지 확인하고 반환될 서비스에서 유명인을 생성합니다.

기타 JUnit 기능

JUnit에는 예제에서 아직 탐색하지 않은 많은 추가 기능이 있습니다. 우리는 일부를 설명하고 다른 것에 대한 참조를 제공할 것입니다.

테스트 설비

JUnit은 각 @Test 메소드를 실행하기 위한 테스트 클래스의 새 인스턴스를 생성한다는 점에 유의해야 합니다. JUnit은 또한 @Test 메소드 전체 또는 각각의 전후에 특정 메소드를 실행하기 위한 주석 후크를 제공합니다. 이러한 후크는 데이터베이스 또는 모의 객체를 설정하거나 정리하는 데 자주 사용되며 JUnit 4와 5에서 다릅니다.

JUnit 4 JUnit 5 정적 메서드의 경우?
@BeforeClass @BeforeAll
@AfterClass @AfterAll
@Before @BeforeEach 아니요
@After @AfterEach 아니요

PersonTest 예제에서 우리는 @Test 메소드 자체에서 BirthdaysClient 모의 객체를 구성하기로 선택했지만 때로는 여러 객체를 포함하는 더 복잡한 모의 구조를 구축해야 할 필요가 있습니다. @BeforeEach (JUnit 5) 및 @Before (JUnit 4)는 종종 이에 적합합니다.

@After* 주석은 JVM 가비지 수집이 단위 테스트를 위해 생성된 대부분의 개체를 처리하므로 단위 테스트보다 통합 테스트에서 더 일반적입니다. @BeforeClass@BeforeAll 주석은 각 테스트 방법보다 비용이 많이 드는 설정 및 해제 작업을 한 번 수행해야 하는 통합 테스트에 가장 일반적으로 사용됩니다.

JUnit 4의 경우 테스트 픽스처 가이드를 참조하세요(일반 개념은 여전히 ​​JUnit 5에 적용됨).

테스트 스위트

때로는 여러 관련 테스트를 실행하고 싶지만 모든 테스트를 실행하고 싶은 것은 아닙니다. 이 경우 테스트 그룹을 테스트 스위트로 구성할 수 있습니다. JUnit 5에서 이 작업을 수행하는 방법은 HowToProgram.xyz의 JUnit 5 기사와 JUnit 팀의 JUnit 4 문서를 참조하세요.

JUnit 5의 @Nested 및 @DisplayName

JUnit 5는 테스트 간의 관계를 더 잘 보여주기 위해 비정적 중첩 내부 클래스를 사용하는 기능을 추가합니다. 이것은 Jasmine for JavaScript와 같은 테스트 프레임워크에서 중첩된 설명으로 작업한 사람들에게 매우 친숙할 것입니다. 내부 클래스는 이것을 사용하기 위해 @Nested 로 주석 처리됩니다.

@DisplayName 주석은 JUnit 5의 새로운 기능이기도 하므로 테스트 메서드 식별자에 추가하여 표시할 문자열 형식으로 보고 테스트를 설명할 수 있습니다.

@Nested@DisplayName 은 서로 독립적으로 사용할 수 있지만 함께 사용하면 시스템 동작을 설명하는 보다 명확한 테스트 결과를 제공할 수 있습니다.

햄크레스트 매처

Hamcrest 프레임워크는 그 자체가 JUnit 코드베이스의 일부는 아니지만 테스트에서 기존의 assert 메서드를 사용하는 것에 대한 대안을 제공하여 보다 표현적이고 읽기 쉬운 테스트 코드를 허용합니다. 기존 assertEquals와 Hamcrest assertThat을 모두 사용하여 다음 확인을 참조하십시오.

 //Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));

Hamcrest는 JUnit 4 및 5와 함께 사용할 수 있습니다. Hamcrest에 대한 Vogella.com의 자습서는 매우 포괄적입니다.

추가 리소스

  • 단위 테스트, 테스트 가능한 코드 작성 방법 및 중요한 이유 기사에서는 깨끗하고 테스트 가능한 코드 작성에 대한 보다 구체적인 예를 다룹니다.

  • 자신감 있게 빌드: JUnit 테스트 가이드에서는 단위 및 통합 테스트에 대한 다양한 접근 방식과 그 중 하나를 선택하여 고수하는 것이 가장 좋은 이유를 살펴봅니다.

  • JUnit 4 Wiki 및 JUnit 5 사용자 가이드는 항상 훌륭한 참고 자료입니다.

  • Mockito 문서는 추가 기능 및 예제에 대한 정보를 제공합니다.

JUnit은 자동화의 길입니다

우리는 JUnit을 사용하여 Java 세계에서 테스트의 여러 측면을 탐구했습니다. 우리는 Java 코드베이스용 JUnit 프레임워크를 사용하는 단위 및 통합 테스트, 개발 및 빌드 환경에서 JUnit 통합, 공급자 및 Mockito와 함께 모의 및 스텁을 사용하는 방법, 일반적인 규칙 및 모범 코드 사례, 테스트 대상 및 일부 다른 훌륭한 JUnit 기능.

이제 독자는 JUnit 프레임워크를 사용하여 자동화된 테스트의 이점을 능숙하게 적용, 유지 관리 및 수확하는 데 있어 성장할 차례입니다.