CodeGym /Java Blog /무작위의 /단위 테스트에 관한 모든 것: 기술, 개념, 실습
John Squirrels
레벨 41
San Francisco

단위 테스트에 관한 모든 것: 기술, 개념, 실습

무작위의 그룹에 게시되었습니다
오늘날에는 테스트로 장식되지 않은 애플리케이션을 찾을 수 없으므로 이 주제는 초보 개발자에게 그 어느 때보다 관련성이 높을 것입니다. 테스트 없이는 성공할 수 없습니다. 원칙적으로 어떤 유형의 테스트가 사용되는지 고려한 다음 단위 테스트에 대해 알아야 할 모든 것을 자세히 연구합니다. 단위 테스트에 관한 모든 것: 기술, 개념, 연습 - 1

테스트 유형

테스트란 무엇입니까? Wikipedia에 따르면 "소프트웨어 테스트에는 하나 이상의 관심 속성을 평가하기 위해 소프트웨어 구성 요소 또는 시스템 구성 요소의 실행이 포함됩니다." 즉, 특정 상황에서 시스템의 정확성을 확인하는 것입니다. 일반적으로 어떤 유형의 테스트가 있는지 살펴보겠습니다.
  • 단위 테스트 — 시스템의 각 모듈을 개별적으로 확인하는 것이 목적인 테스트입니다. 이러한 테스트는 시스템의 가장 작은 원자 부분(예: 모듈)에 적용되어야 합니다.
  • 시스템 테스트 — 응용 프로그램의 더 큰 부분 또는 시스템 전체의 작동을 확인하기 위한 높은 수준의 테스트입니다.
  • 회귀 테스트 — 새로운 기능이나 버그 수정이 응용 프로그램의 기존 기능에 영향을 미치거나 오래된 버그를 도입하는지 여부를 확인하는 데 사용되는 테스트입니다.
  • 기능 테스트 — 애플리케이션의 일부가 사양, 사용자 스토리 등에 명시된 요구 사항을 충족하는지 확인합니다.

    기능 테스트 유형:

    • 화이트박스 테스트 — 시스템의 내부 구현을 파악하면서 애플리케이션의 일부가 요구 사항을 충족하는지 확인합니다.
    • 블랙박스 테스트 — 시스템의 내부 구현을 알지 못한 채 애플리케이션의 일부가 요구 사항을 충족하는지 확인합니다.

  • 성능 테스트 — 특정 부하에서 시스템 또는 시스템 일부가 어떻게 작동하는지 확인하기 위해 작성된 테스트입니다.
  • 부하 테스트 — 표준 부하에서 시스템의 안정성을 확인하고 응용 프로그램이 여전히 올바르게 작동하는 최대 부하를 찾기 위해 고안된 테스트입니다.
  • 스트레스 테스트 — 비표준 로드에서 애플리케이션의 성능을 확인하고 시스템 오류가 발생하기 전에 최대 로드를 결정하도록 설계된 테스트입니다.
  • 보안 테스트 — 해커, 바이러스, 기밀 데이터에 대한 무단 액세스 및 기타 유쾌한 공격으로부터 시스템의 보안을 확인하는 데 사용되는 테스트입니다.
  • 현지화 테스트 — 애플리케이션의 현지화 테스트.
  • 사용성 테스트 — 사용성, 이해 가능성, 매력 및 학습 가능성을 확인하기 위한 테스트입니다.
이 모든 것이 좋아 보이지만 실제로는 어떻게 작동합니까? 단순한! 우리는 Mike Cohn의 테스트 피라미드를 사용합니다. 단위 테스트에 관한 모든 것: 기술, 개념, 연습 - 2이것은 피라미드의 단순화된 버전입니다. 이제 더 작은 부분으로 나뉩니다. 그러나 오늘 우리는 너무 정교하지 않을 것입니다. 우리는 가장 간단한 버전을 고려할 것입니다.
  1. 단위 — 이 섹션은 응용 프로그램의 여러 계층에 적용되는 단위 테스트를 나타냅니다. 애플리케이션 로직의 가장 작은 분할 가능 단위를 테스트합니다. 예를 들어 클래스가 있지만 대부분 메서드입니다. 이러한 테스트는 일반적으로 테스트 대상을 외부 로직에서 최대한 격리하려고 시도합니다. 즉, 응용 프로그램의 나머지 부분이 예상대로 실행되고 있다는 착각을 일으키려고 합니다.

    이러한 테스트는 작은 부분을 테스트하고 매우 가볍고 많은 리소스(RAM 및 시간을 의미)를 소비하지 않기 때문에 항상 많은 테스트(다른 유형보다 더 많이)가 있어야 합니다.

  2. 통합 — 이 섹션은 통합 테스트를 참조합니다. 이 테스트는 시스템의 더 큰 부분을 검사합니다. 즉, 여러 가지 로직(여러 메서드 또는 클래스)을 결합하거나 외부 구성 요소와의 상호 작용의 정확성을 확인합니다. 이러한 테스트는 무겁기 때문에 일반적으로 단위 테스트보다 작습니다.

    통합 테스트의 예는 데이터베이스에 연결하고 작업 방법의 정확성을 확인하는 것입니다.

  3. UI — 이 섹션은 사용자 인터페이스의 작동을 확인하는 테스트를 나타냅니다. 애플리케이션의 모든 수준에서 논리를 포함하므로 종단 간 테스트라고도 합니다. 일반적으로 가장 번거롭고 가장 필요한(사용된) 경로를 확인해야 하기 때문에 훨씬 적습니다.

    위의 그림에서 우리는 삼각형의 다른 부분이 크기가 다르다는 것을 알 수 있습니다. 실제 작업에서 다양한 종류의 테스트 수에 거의 동일한 비율이 존재합니다.

    오늘 우리는 가장 일반적인 테스트인 단위 테스트에 대해 자세히 살펴볼 것입니다. 자존심이 강한 모든 Java 개발자는 기본 수준에서 단위 테스트를 사용할 수 있어야 하기 때문입니다.

단위 테스트의 핵심 개념

테스트 커버리지(코드 커버리지)는 애플리케이션이 얼마나 잘 테스트되었는지를 측정하는 주요 척도 중 하나입니다. 테스트에서 다루는 코드의 백분율입니다(0-100%). 실제로 많은 사람들이 이 비율을 목표로 삼고 있습니다. 그것은 테스트가 필요하지 않은 곳에 적용되기 시작한다는 것을 의미하기 때문에 동의하지 않는 것입니다. 예를 들어 추가 로직 없이 서비스에 표준 CRUD(생성/가져오기/업데이트/삭제) 작업이 있다고 가정합니다. 이러한 메서드는 리포지토리와 함께 작업하는 계층에 작업을 위임하는 순전히 중개자입니다. 이 상황에서는 주어진 메서드가 DAO 메서드를 호출하는지 여부를 제외하고는 테스트할 것이 없지만 농담입니다. 일반적으로 테스트 범위를 평가하는 데 추가 도구(JaCoCo, Cobertura, Clover, Emma 등)가 사용됩니다. 이 주제에 대한 자세한 연구는 TDD는 테스트 주도 개발을 의미합니다. 이 접근 방식에서는 다른 작업을 수행하기 전에 특정 코드를 확인하는 테스트를 작성합니다. 이것은 블랙박스 테스트로 밝혀졌습니다. 우리는 입력이 무엇인지 알고 출력이 무엇인지 알고 있습니다. 이렇게 하면 코드 중복을 피할 수 있습니다. 테스트 기반 개발은 애플리케이션의 각 기능에 대한 테스트를 설계하고 개발하는 것으로 시작됩니다. TDD 접근 방식에서는 먼저 코드의 동작을 정의하고 테스트하는 테스트를 만듭니다. TDD의 주요 목표는 코드를 더 이해하기 쉽고 간단하며 오류 없이 만드는 것입니다. 단위 테스트에 관한 모든 것: 기술, 개념, 연습 - 3접근 방식은 다음과 같이 구성됩니다.
  • 우리는 테스트를 작성합니다.
  • 우리는 테스트를 실행합니다. 당연히 필요한 논리를 아직 구현하지 않았기 때문에 실패합니다.
  • 테스트를 통과시키는 코드를 추가합니다(테스트를 다시 실행합니다).
  • 우리는 코드를 리팩토링합니다.
TDD는 테스트 자동화 피라미드에서 가장 작은 빌딩 블록이기 때문에 단위 테스트를 기반으로 합니다. 단위 테스트를 사용하면 모든 클래스의 비즈니스 로직을 테스트할 수 있습니다. BDD는 행동 중심 개발을 의미합니다. 이 접근 방식은 TDD를 기반으로 합니다. 보다 구체적으로, 개발에 관련된 모든 사람을 위해 시스템 동작을 설명하는 일반 언어 예제를 사용합니다. 이 용어는 주로 테스터와 비즈니스 분석가에게 영향을 미치므로 자세히 다루지 않겠습니다. 테스트 사례는 테스트 중인 코드를 확인하는 데 필요한 단계, 특정 조건 및 매개 변수를 설명하는 시나리오입니다. 테스트 픽스처는 테스트 중인 메서드가 성공적으로 실행되는 데 필요한 상태를 갖도록 테스트 환경을 설정하는 코드입니다. 미리 정의된 개체 집합과 지정된 조건에서 해당 동작입니다.

테스트 단계

테스트는 세 단계로 구성됩니다.
  • 테스트 데이터(픽스처)를 지정합니다.
  • 테스트 중인 코드를 실행합니다(테스트된 메서드 호출).
  • 결과를 확인하고 예상 결과와 비교하십시오.
단위 테스트에 관한 모든 것: 기술, 개념, 연습 - 4테스트 모듈성을 보장하려면 애플리케이션의 다른 계층과 격리해야 합니다. 이는 스텁, 모의 객체 및 스파이를 사용하여 수행할 수 있습니다. Mock은 사용자 지정할 수 있는 개체입니다(예: 각 테스트에 맞게 조정). 이를 통해 메서드 호출에서 기대하는 것, 즉 예상되는 응답을 지정할 수 있습니다. 모의 개체를 사용하여 예상한 결과를 얻었는지 확인합니다. 스텁은 테스트 중에 호출에 대한 하드 코딩된 응답을 제공합니다. 또한 호출에 대한 정보(예: 매개변수 또는 호출 수)를 저장할 수 있습니다. 이들은 때때로 스파이라고 불립니다. 때때로 사람들은 스텁과 모의라는 용어를 혼동합니다. 차이점은 스텁은 아무 것도 검사하지 않는다는 것입니다. 단지 주어진 상태를 시뮬레이트할 뿐입니다. 모의 객체는 예상되는 객체입니다. 예를 들어 주어진 메서드는 특정 횟수만큼 호출되어야 합니다. 다시 말해서,

테스트 환경

이제 요점입니다. Java에 사용할 수 있는 여러 테스트 환경(프레임워크)이 있습니다. 가장 인기 있는 것은 JUnit과 TestNG입니다. 여기서 검토를 위해 다음을 사용합니다. 단위 테스트에 관한 모든 것: 기술, 개념, 연습 - 5JUnit 테스트는 테스트용으로만 사용되는 클래스의 메서드입니다. 클래스는 일반적으로 테스트하는 클래스와 동일하게 이름이 지정되며 끝에 "Test"가 추가됩니다. 예를 들어 CarService -> CarServiceTest입니다. Maven 빌드 시스템은 이러한 클래스를 테스트 범위에 자동으로 포함합니다. 사실 이 클래스를 테스트 클래스라고 합니다. 기본 주석을 간단히 살펴보겠습니다.

  • @Test는 메서드가 테스트임을 나타냅니다(기본적으로 이 주석이 표시된 메서드는 단위 테스트입니다).
  • @Before는 각 테스트 전에 실행될 메서드를 나타냅니다. 예를 들어 테스트 데이터로 클래스를 채우려면 입력 데이터를 읽습니다.
  • @After는 각 테스트 후에 호출될 메서드를 표시하는 데 사용됩니다(예: 데이터 지우기 또는 기본값 복원).
  • @BeforeClass는 @Before와 유사하게 메서드 위에 배치됩니다. 그러나 이러한 메서드는 주어진 클래스에 대한 모든 테스트 전에 한 번만 호출되므로 정적이어야 합니다. 테스트 데이터베이스 회전과 같이 리소스를 많이 사용하는 작업을 수행하는 데 사용됩니다.
  • @AfterClass는 @BeforeClass의 반대입니다. 주어진 클래스에 대해 한 번만 실행되지만 모든 테스트 후에만 실행됩니다. 예를 들어 영구 리소스를 지우거나 데이터베이스에서 연결을 끊는 데 사용됩니다.
  • @Ignore는 메서드가 비활성화되어 전체 테스트 실행 중에 무시됨을 나타냅니다. 예를 들어, 기본 방법이 변경되었고 변경 사항을 수용하기 위해 테스트가 아직 재작업되지 않은 경우와 같이 다양한 상황에서 사용됩니다. 이러한 경우 @Ignore("Some description")와 같은 설명을 추가하는 것도 바람직합니다.
  • @Test(expected = Exception.class)는 음성 테스트에 사용됩니다. 이는 오류가 발생한 경우 메서드가 어떻게 작동하는지 확인하는 테스트입니다. 즉, 테스트는 메서드가 어떤 종류의 예외를 throw할 것으로 예상합니다. 이러한 메서드는 @Test 주석으로 표시되지만 어떤 오류를 잡아야 하는지 표시됩니다.
  • @Test(timeout = 100) 메서드가 100밀리초 이내에 실행되는지 확인합니다.
  • @Mock은 모의 개체를 할당하기 위해 필드 위에서 사용됩니다(이는 JUnit 주석이 아니라 대신 Mockito에서 가져옴). 필요에 따라 특정 상황에 대한 모의 동작을 테스트 메서드에서 직접 설정합니다.
  • @RunWith(MockitoJUnitRunner.class)는 클래스 위에 위치합니다. 이 주석은 JUnit에게 클래스의 테스트를 호출하도록 지시합니다. MockitoJUnitRunner, JUnitPlatform 및 SpringRunner를 포함하여 다양한 러너가 있습니다. JUnit 5에서 @RunWith 주석은 더 강력한 @ExtendWith 주석으로 대체되었습니다.
결과를 비교하는 데 사용되는 몇 가지 방법을 살펴보겠습니다.

  • assertEquals(Object expecteds, Object actuals) — 전달된 객체가 동일한지 확인합니다.
  • assertTrue(부울 플래그) — 전달된 값이 참인지 확인합니다.
  • assertFalse(부울 플래그) — 전달된 값이 거짓인지 확인합니다.
  • assertNull(Object object) — 전달된 객체가 null인지 확인합니다.
  • assertSame(Object firstObject, Object secondObject) — 전달된 값이 동일한 객체를 참조하는지 확인합니다.
  • assertThat(T t, 매처 매처) — t가 matcher에 지정된 조건을 만족하는지 확인합니다.
AssertJ는 또한 유용한 비교 방법인 assertThat(firstObject).isEqualTo(secondObject) 를 제공합니다 . 여기서는 기본 방법에 대해 언급했습니다. 다른 방법은 위의 변형입니다.

실제 테스트

이제 특정 예에서 위의 자료를 살펴보겠습니다. 서비스의 업데이트 방법을 테스트합니다. 기본값을 사용하고 있으므로 DAO 레이어는 고려하지 않습니다. 테스트를 위한 스타터를 추가해 보겠습니다.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <version>2.2.2.RELEASE</version>
   <scope>test</scope>
</dependency>
여기에 서비스 클래스가 있습니다.

@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
   private final RobotDAO robotDAO;

   @Override
   public Robot update(Long id, Robot robot) {
       Robot found = robotDAO.findById(id);
       return robotDAO.update(Robot.builder()
               .id(id)
               .name(robot.getName() != null ? robot.getName() : found.getName())
               .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
               .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
               .build());
   }
}
8행 — 데이터베이스에서 업데이트된 개체를 가져옵니다. 9-14행 — 빌더를 통해 객체를 생성합니다. 들어오는 개체에 필드가 있으면 설정합니다. 그렇지 않은 경우 데이터베이스에 있는 것을 그대로 둡니다. 이제 테스트를 살펴보십시오.

@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
   @Mock
   private RobotDAO robotDAO;

   private RobotServiceImpl robotService;

   private static Robot testRobot;

   @BeforeClass
   public static void prepareTestData() {
       testRobot = Robot
               .builder()
               .id(123L)
               .name("testRobotMolly")
               .cpu("Intel Core i7-9700K")
               .producer("China")
               .build();
   }

   @Before
   public void init() {
       robotService = new RobotServiceImpl(robotDAO);
   }
라인 1 — 러너. 4행 — 모형을 대체하여 DAO 계층에서 서비스를 분리합니다. 11행 — 클래스에 대한 테스트 엔터티(기니피그로 사용할 엔터티)를 설정합니다. 22행 — 테스트할 서비스 개체를 설정합니다.

@Test
public void updateTest() {
   when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
   when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
   Robot robotForUpdate = Robot
           .builder()
           .name("Vally")
           .cpu("AMD Ryzen 7 2700X")
           .build();

   Robot resultRobot = robotService.update(123L, robotForUpdate);

   assertNotNull(resultRobot);
   assertSame(resultRobot.getId(),testRobot.getId());
   assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
   assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
   assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
여기에서 테스트에 세 가지 명확한 구분이 있음을 알 수 있습니다. 3-9행 — 픽스처 지정. 11행 - 테스트 중인 코드를 실행합니다. 13-17행 — 결과 확인. 자세히 알아보기: Lines 3-4 — DAO 모의 동작을 설정합니다. 5행 — 표준 위에 업데이트할 인스턴스를 설정합니다. 11행 — 메서드를 사용하고 결과 인스턴스를 가져옵니다. 13행 — null이 아닌지 확인합니다. 14행 — 결과의 ID와 주어진 메서드 인수를 비교합니다. 15행 — 이름이 업데이트되었는지 확인합니다. 16행 - CPU 결과를 봅니다. 17행 — 인스턴스에서 이 필드를 지정하지 않았으므로 동일하게 유지되어야 합니다. 여기에서 그 조건을 확인합니다. 실행해 봅시다:단위 테스트에 관한 모든 것: 기술, 개념, 실습 - 6테스트는 녹색입니다! 우리는 안도의 한숨을 쉴 수 있습니다 :) 요약하면 테스트는 코드의 품질을 향상시키고 개발 프로세스를 보다 유연하고 안정적으로 만듭니다. 수백 개의 클래스 파일이 포함된 소프트웨어를 재설계하는 데 얼마나 많은 노력이 드는지 상상해 보십시오. 이러한 모든 클래스에 대해 작성된 단위 테스트가 있으면 안심하고 리팩터링할 수 있습니다. 그리고 가장 중요한 것은 개발 중에 버그를 쉽게 찾는 데 도움이 된다는 것입니다. 여러분, 오늘은 여기까지입니다. 좋아요와 댓글을 남겨주세요 :)
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION