개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

기초개념 › Spring

테스트 코드

테스트 코드는 무엇이고, 통과와 실패는 누가 정하는가

테스트 코드는 "이 메서드에 이것을 넣으면 이것이 나와야 한다"를 코드로 적어 둔 것입니다. 이 실습의 CalcTest 클래스가 그 예입니다. 그 안의 adds() 메서드가 Calc 클래스의 add(2, 3)을 부르고, assertEquals(5, ...)가 add가 돌려준 값을 기대한 값 5와 견줍니다. 두 값이 다르면 그 줄에서 오류가 납니다. adds() 위의 @Test는 "이 메서드가 테스트"라는 표시입니다. 이 코드를 실행하는 것은 우리가 아니라 테스트 도구 JUnit입니다. JUnit이 @Test가 붙은 메서드를 찾아 부르고, 오류 없이 끝나면 통과, assertEquals가 낸 오류로 끝나면 실패로 셉니다. 이 실습은 add를 틀리게 고치거나, 기대한 값을 틀리게 적거나, @Test를 지워 가며 같은 코드를 실제 JUnit으로 실행해, 찾은 테스트 수와 통과와 실패 판정, 실패 메시지가 어떻게 달라지는지 봅니다. (화면의 코드는 import 줄을 줄였습니다.)

실행으로 대조 · JDK 21과 JUnit 6.0.3에서 여덟 가지 조합을 컴파일하고 JUnit Launcher로 실행해 찾은 테스트 수, 통과와 실패, 실패 메시지를 대조

이 실습에 나오는 개념

테스트 코드
테스트 코드는 다른 코드가 맞게 동작하는지 확인하려고 쓴 코드이다. "이 메서드에 이 값을 넣으면 이 값이 나와야 한다"를 코드로 적어 두고, 실제로 나온 값을 기대한 값과 견주는 줄(단언)을 둔다. JUnit에서는 테스트 메서드를 가진 클래스를 테스트 클래스라고 부르고, @Test가 붙은 인스턴스 메서드를 테스트 메서드라고 부른다. 테스트 코드를 실행하는 것은 우리가 쓴 main이 아니라 JUnit 같은 테스트 도구이고, 도구가 테스트 메서드를 부른 뒤 통과인지 실패인지 센다. 테스트가 하나도 실행되지 않으면 실패가 0개여도 아무것도 확인된 것이 아니므로, 실행된 테스트 수도 함께 보아야 한다.
JUnit
JUnit은 자바 테스트 코드를 쓰고 실행하는 데 널리 쓰이는 테스트 도구이다. 그 가운데 JUnit Jupiter는 테스트를 쓰는 방법(프로그래밍 모델)과 확장하는 방법(확장 모델)을 합친 것이고, @Test와 Assertions 클래스가 여기에 속한다. 개발 도구나 빌드 도구가 JUnit을 실행하면 JUnit이 테스트 클래스에서 테스트 메서드를 찾아 부르고, 테스트마다 통과, 실패 같은 결과를 센다. 판정은 사람이 출력을 읽어서가 아니라 JUnit이 하며, 실행한 JUnit 6.0.3에서는 오류 없이 끝난 테스트는 성공으로, 단언이 던진 오류로 끝난 테스트는 실패로 셌다.
@Test
@Test는 메서드 위에 붙여, 그 메서드가 테스트 메서드라고 JUnit에 알리는 애너테이션이다. 문서에 따르면 @Test가 붙은 메서드는 private이거나 static이면 안 되고 값을 돌려주면 안 된다. JUnit은 @Test(또는 @RepeatedTest, @ParameterizedTest 같은 애너테이션)가 붙은 메서드를 테스트로 세므로, 내용이 테스트처럼 생겼어도 표시가 없는 메서드는 테스트로 세지 않는다. 실행한 JUnit 6.0.3에서는 @Test가 없는 메서드를 가진 클래스를 실행하면 찾은 테스트가 0개이고, 오류도 실패도 없이 그 메서드가 한 번도 실행되지 않았다.
assertEquals
assertEquals(expected, actual)는 JUnit Jupiter의 Assertions 클래스에 있는 static 메서드로, 첫째 인자(기대한 값)와 둘째 인자(실제 값)가 같다고 단언한다. 같으면 아무 일 없이 끝나고, 다르면 오류를 던져서 그 줄에서 테스트 메서드가 끝난다. 문서는 실패한 단언이 따로 정한 것이 없으면 AssertionFailedError나 그 하위 타입을 던진다고 적고 있고, 실행한 JUnit 6.0.3에서는 메시지가 expected: <5> but was: <-1> 꼴로 나왔다. 두 값이 다르다는 것만 알려 줄 뿐 어느 쪽이 틀렸는지는 알려 주지 않으므로, 실패 메시지를 읽고 코드와 기대한 값 중 어느 쪽이 잘못인지는 사람이 판단한다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 테스트 도구 JUnit이 CalcTest 클래스를 실행합니다. JUnit은 테스트를 찾아 부르고 통과인지 실패인지 판정해 주는 프로그램입니다. 우리가 쓴 코드에는 main 메서드도, adds() 메서드를 부르는 줄도 없습니다. 코드를 움직이는 것은 JUnit입니다.
  2. JUnit이 CalcTest 클래스의 메서드를 살펴 @Test가 붙은 것을 찾습니다. adds()에 @Test가 붙어 있으므로 JUnit이 이 메서드를 테스트로 셉니다. 찾은 테스트는 1개입니다.
  3. JUnit이 adds() 메서드를 부릅니다. 이 줄도 우리가 쓰지 않았습니다. 테스트 메서드를 부르는 것은 우리 코드가 아니라 JUnit입니다.
  4. adds() 안의 new Calc().add(2, 3)이 실행됩니다. Calc 객체를 만들어 add에 2와 3을 넘기면, return a + b; 줄이 계산합니다(2 + 3).
  5. add가 계산한 결과는 5입니다. add가 이 값을 adds()로 돌려주고, 이것이 실제 값입니다.
  6. assertEquals(5, ...)가 두 값을 견줍니다. 기대한 값은 5, 실제 값은 5입니다. 두 값이 같으므로 assertEquals는 아무 일 없이 끝나고, adds()도 오류 없이 끝까지 갑니다.
  7. JUnit이 adds()가 오류 없이 끝난 것을 보고 통과로 셉니다. 통과인지는 사람이 출력을 눈으로 읽어서가 아니라 JUnit이 판정했습니다.
  8. JUnit이 끝납니다. 찾은 테스트는 1개, 통과는 1개, 실패는 0개입니다.

실행 결과

코드

// 검사받는 코드
class Calc {
    int add(int a, int b) {
        return a + b;
    }
}

// 테스트 코드: Calc가 맞게 동작하는지 확인하는 코드
class CalcTest {
    // 표시: JUnit은 @Test가 붙은 메서드를 찾아 테스트로 실행한다
    @Test
    void adds() {
        // 판정: 앞의 값(기대한 값)과 뒤의 값(실제 값)이 다르면 실패
        assertEquals(5, new Calc().add(2, 3));
    }
}

이 모형의 전제

실행한 것은 JDK 21.0.12.1(javac 21)과 JUnit Jupiter 6.0.3, JUnit Platform Launcher 6.0.3입니다. 여덟 가지 조합을 모두, 화면의 코드 맨 위에 import 두 줄(org.junit.jupiter.api.Test, Assertions.assertEquals)만 더한 같은 소스를 javax.tools로 컴파일하고, JUnit Platform Launcher를 코드로 불러 CalcTest 클래스를 실행해 요약을 얻은 결과와 대조했습니다. 터미널에서 ConsoleLauncher를 쓴 것이 아니라 같은 도구의 Launcher API를 직접 썼습니다. 대조한 것은 찾은 테스트 수, 시작한 수, 성공과 실패의 수, 실패한 예외의 종류(org.opentest4j.AssertionFailedError)와 메시지의 첫 줄입니다. 메시지 글자 expected: <5> but was: <-1>은 이 JUnit이 실제로 낸 글 그대로입니다. 단계 문장이 기대는 사실로, 호출 횟수를 세는 필드를 더한 별도의 소스를 같은 방식으로 실행해, @Test가 있으면 우리 코드가 부르는 줄 없이 adds()와 add가 한 번씩 실행되고 @Test를 지우면 둘 다 0번인 것을 확인했습니다. 문서는 JUnit User Guide(docs.junit.org, 6.1.3판)의 Definitions 절에서 테스트 메서드가 @Test 등이 붙은 인스턴스 메서드이고 테스트 클래스는 테스트 메서드를 가진 클래스라는 것, Test Classes and Methods 절에서 테스트 클래스와 메서드가 public일 필요는 없지만 private이면 안 되고 값을 돌려주면 안 된다는 것, Assertions 절에서 JUnit Jupiter의 단언이 org.junit.jupiter.api.Assertions의 static 메서드이고 예제가 assertEquals(2, calculator.add(1, 1))처럼 기대한 값을 앞에 적는다는 것을, API 문서에서 @Test가 메서드가 테스트 메서드임을 알리는 표시이고 private이거나 static이면 안 된다는 것, Assertions의 설명(실패한 단언은 따로 정한 것이 없으면 AssertionFailedError나 그 하위 타입을 던진다)과 assertEquals(int expected, int actual)의 요약(expected와 actual이 같다고 단언한다)을 열어 확인했습니다. 문서는 6.1.3판이고 실행한 JUnit은 6.0.3판입니다. 문서가 정한 것과 이 실행에서 관찰한 것은 다릅니다. 오류 없이 끝나면 통과이고 오류로 끝나면 실패로 세는 것, @Test가 없는 메서드를 찾지 않아 찾은 테스트가 0개이고 실패도 0개로 세는 것은 이 JUnit에서 관찰한 동작입니다(ConsoleLauncher의 종료 코드나 빌드 도구가 테스트 0개를 어떻게 다루는지는 실행하지 않았습니다). assertEquals의 앞뒤 값 중 앞이 기대한 값이라는 것은 메서드 이름과 인자 이름(expected, actual)으로 확인했고, 문서가 순서를 규칙으로 따로 적어 두지는 않았습니다. 화면은 테스트 한 개만 다루고, 테스트 이름 붙이기, 여러 개의 단언, 준비와 정리 메서드, 스프링 컨텍스트를 띄우는 테스트, @MockitoBean 같은 가짜 객체, 테스트를 자동으로 돌리는 빌드 과정은 다루지 않습니다. 스프링 컨텍스트를 띄우는 테스트는 "세부 동작 실습: 테스트", 테스트를 올릴 때마다 자동으로 돌리는 것은 "세부 동작 실습: CI/CD 파이프라인"에서 다룹니다.

앞 실습: 스프링 부트 · 다음 실습: 엔티티

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요