자신있게 빌드: JUnit 테스트 가이드
게시 됨: 2022-03-11기술과 산업의 발전이 폭포수 모델에서 애자일로, 그리고 이제는 DevOps로 이동함에 따라 애플리케이션의 변경 사항과 개선 사항은 변경 사항이 만들어지는 순간 프로덕션에 배포됩니다. 코드가 이렇게 빠르게 프로덕션에 배포됨에 따라 변경 사항이 작동하고 기존 기능이 손상되지 않는다는 확신이 필요합니다.
이러한 신뢰를 구축하려면 자동 회귀 테스트를 위한 프레임워크가 있어야 합니다. 회귀 테스트를 수행하려면 API 수준의 관점에서 수행해야 하는 테스트가 많이 있지만 여기서는 두 가지 주요 유형의 테스트를 다룹니다.
- 주어진 테스트가 프로그램(함수 또는 프로시저)의 가장 작은 단위를 다루는 단위 테스트 . 일부 입력 매개변수를 사용하거나 사용하지 않을 수 있으며 일부 값을 반환하거나 반환하지 않을 수 있습니다.
- 통합 테스트 , 모든 단위가 예상대로 서로 상호 작용하는지 확인하기 위해 개별 단위를 함께 테스트합니다.
모든 프로그래밍 언어에 사용할 수 있는 수많은 프레임워크가 있습니다. Java의 Spring 프레임워크로 작성된 웹 앱에 대한 쓰기 단위 및 통합 테스트에 중점을 둘 것입니다.
대부분의 경우, 우리는 클래스에 메소드를 작성하고, 이는 차례로 다른 클래스의 메소드와 상호 작용합니다. 오늘날의 세계, 특히 엔터프라이즈 애플리케이션에서 애플리케이션의 복잡성은 단일 메소드가 여러 클래스의 둘 이상의 메소드를 호출할 수 있을 정도로 복잡합니다. 따라서 이러한 메서드에 대한 단위 테스트를 작성할 때 해당 호출에서 모의 데이터를 반환하는 방법이 필요합니다. 이것은 이 단위 테스트의 목적이 이 특정 메서드가 만드는 모든 호출이 아니라 하나의 메서드만 테스트하는 것이기 때문입니다.
JUnit 프레임워크를 사용하여 Spring에서 Java 단위 테스트를 시작하겠습니다. 우리는 당신이 들어봤을 것인 조롱부터 시작할 것입니다.
조롱이란 무엇이며 언제 그림에 나타납니까?
지정된 유형(원, 정사각형 또는 직사각형)의 모양 영역을 계산하는 calculateArea(Type type, Double... args)
함수가 있는 CalculateArea
클래스가 있다고 가정합니다.
코드는 종속성 주입을 사용하지 않는 일반 애플리케이션에서 다음과 같이 진행됩니다.
public class CalculateArea { SquareService squareService; RectangleService rectangleService; CircleService circleService; CalculateArea(SquareService squareService, RectangleService rectangeService, CircleService circleService) { this.squareService = squareService; this.rectangleService = rectangeService; this.circleService = circleService; } public Double calculateArea(Type type, Double... r ) { switch (type) { case RECTANGLE: if(r.length >=2) return rectangleService.area(r[0],r[1]); else throw new RuntimeException("Missing required params"); case SQUARE: if(r.length >=1) return squareService.area(r[0]); else throw new RuntimeException("Missing required param"); case CIRCLE: if(r.length >=1) return circleService.area(r[0]); else throw new RuntimeException("Missing required param"); default: throw new RuntimeException("Operation not supported"); } } }
public class SquareService { public Double area(double r) { return r * r; } }
public class RectangleService { public Double area(Double r, Double h) { return r * h; } }
public class CircleService { public Double area(Double r) { return Math.PI * r * r; } }
public enum Type { RECTANGLE,SQUARE,CIRCLE; }
이제, CalculateArea
클래스의 computeArea calculateArea()
함수를 단위 테스트하려면 switch
사례와 예외 조건이 작동하는지 확인하는 것이 동기가 되어야 합니다. 셰이프 서비스가 올바른 값을 반환하는지 여부를 테스트해서는 안 됩니다. 앞서 언급한 것처럼 함수 단위 테스트의 동기는 함수가 수행하는 호출의 논리가 아니라 함수의 논리를 테스트하는 것이기 때문입니다.
따라서 개별 서비스 함수(예: rectangleService.area()
서비스.area())에서 반환된 값을 모의하고 이러한 모의 값을 기반으로 호출 함수(예: CalculateArea.calculateArea()
)를 테스트합니다.
직사각형 서비스에 대한 간단한 테스트 케이스 calculateArea()
rectangleService.area()
정확한 매개변수로 직사각형 서비스.area()를 실제로 호출하는지 테스트하는 것)는 다음과 같습니다.
import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; public class CalculateAreaTest { RectangleService rectangleService; SquareService squareService; CircleService circleService; CalculateArea calculateArea; @Before public void init() { rectangleService = Mockito.mock(RectangleService.class); squareService = Mockito.mock(SquareService.class); circleService = Mockito.mock(CircleService.class); calculateArea = new CalculateArea(squareService,rectangleService,circleService); } @Test public void calculateRectangleAreaTest() { Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d); Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d); Assert.assertEquals(new Double(20d),calculatedArea); } }
여기서 주목해야 할 두 가지 주요 라인은 다음과 같습니다.
-
rectangleService = Mockito.mock(RectangleService.class);
— 이것은 실제 객체가 아닌 모의 객체인 모의 객체를 생성합니다. -
Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d);
— 이것은 조롱될 때 지정된 매개변수로rectangleService
객체의area
메소드가 호출될 때20d
를 반환한다는 것을 의미합니다.
이제 위의 코드가 Spring 애플리케이션의 일부이면 어떻게 될까요?
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class CalculateArea { SquareService squareService; RectangleService rectangleService; CircleService circleService; public CalculateArea(@Autowired SquareService squareService, @Autowired RectangleService rectangeService, @Autowired CircleService circleService) { this.squareService = squareService; this.rectangleService = rectangeService; this.circleService = circleService; } public Double calculateArea(Type type, Double... r ) { // (same implementation as before) } }
여기에 컨텍스트 초기화 시 감지할 기본 Spring 프레임워크에 대한 두 가지 주석이 있습니다.
-
@Component
:CalculateArea
유형의 빈을 생성합니다. -
@Autowired
:rectangleService
,squareService
, circleService 를 검색하고 이를 BeancalculatedArea
된circleService
에 주입합니다.
유사하게, 우리는 다른 클래스를 위한 빈도 생성합니다:
import org.springframework.stereotype.Service; @Service public class SquareService { public Double area(double r) { return r*r; } }
import org.springframework.stereotype.Service; @Service public class CircleService { public Double area(Double r) { return Math.PI * r * r; } }
import org.springframework.stereotype.Service; @Service public class RectangleService { public Double area(Double r, Double h) { return r*h; } }
이제 테스트를 실행해도 결과는 동일합니다. 여기에서 생성자 주입을 사용했으며 다행히 테스트 케이스를 변경하지 않았습니다.
그러나 정사각형, 원형 및 직사각형 서비스의 빈을 주입하는 또 다른 방법이 있습니다. 바로 필드 주입입니다. 그것을 사용한다면 테스트 케이스에 약간의 변경이 필요할 것입니다.
어떤 주입 메커니즘이 더 나은지에 대한 논의는 이 기사의 범위에 포함되지 않으므로 다루지 않을 것입니다. 그러나 우리는 이렇게 말할 수 있습니다. 빈을 주입하는 데 사용하는 메커니즘의 유형에 관계없이 항상 이에 대한 JUnit 테스트를 작성하는 방법이 있습니다.
필드 주입의 경우 코드는 다음과 같습니다.
@Component public class CalculateArea { @Autowired SquareService squareService; @Autowired RectangleService rectangleService; @Autowired CircleService circleService; public Double calculateArea(Type type, Double... r ) { // (same implementation as before) } }
참고: 필드 주입을 사용하기 때문에 매개변수화된 생성자가 필요하지 않으므로 기본 생성자를 사용하여 객체를 생성하고 필드 주입 메커니즘을 사용하여 값을 설정합니다.
서비스 클래스의 코드는 위와 동일하지만 테스트 클래스의 코드는 다음과 같습니다.
public class CalculateAreaTest { @Mock RectangleService rectangleService; @Mock SquareService squareService; @Mock CircleService circleService; @InjectMocks CalculateArea calculateArea; @Before public void init() { MockitoAnnotations.initMocks(this); } @Test public void calculateRectangleAreaTest() { Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d); Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d); Assert.assertEquals(new Double(20d),calculatedArea); } }
여기에는 몇 가지 사항이 다릅니다. 기본 사항이 아니라 이를 달성하는 방식입니다.
첫째, 객체를 조롱하는 방법: @Mock
주석을 initMocks()
와 함께 사용하여 모의 객체를 생성합니다. 둘째, initMocks()
와 함께 @InjectMocks
를 사용하여 실제 객체에 mock을 주입합니다.
이것은 코드 줄 수를 줄이기 위해 수행됩니다.
테스트 러너란 무엇이며 어떤 유형의 러너가 있습니까?
위의 샘플에서 모든 테스트를 실행하는 데 사용되는 기본 러너는 모든 주석을 감지하고 그에 따라 모든 테스트를 실행하는 BlockJUnit4ClassRunner
입니다.
더 많은 기능을 원한다면 커스텀 러너를 작성할 수 있습니다. 예를 들어 위의 테스트 클래스에서 MockitoAnnotations.initMocks(this);
그런 다음 BlockJUnit4ClassRunner
위에 구축된 다른 러너(예: MockitoJUnitRunner
)를 사용할 수 있습니다.
MockitoJUnitRunner
를 사용하면 모의 객체를 초기화하고 주입할 필요조차 없습니다. 이는 주석을 읽는 것만으로 MockitoJUnitRunner
자체에서 수행됩니다.
(Spring 애플리케이션이 시작될 때 ApplicationContext
가 생성되는 것처럼 Spring 통합 테스트에 필요한 ApplicationContext
를 초기화하는 SpringJUnit4ClassRunner
도 있습니다. 이에 대해서는 나중에 다룹니다.)
부분 조롱
테스트 클래스의 객체가 일부 메서드를 모의하고 일부 실제 메서드도 호출하도록 하려면 부분적인 모의가 필요합니다. 이것은 JUnit의 @Spy
를 통해 달성됩니다.
@Mock
을 사용하는 것과 달리 @Spy
와 함께 실제 객체가 생성되지만 해당 객체의 메소드는 우리가 필요로 하는 모든 것을 모의하거나 실제로 호출할 수 있습니다.
예를 들어, RectangleService
클래스의 area
메서드가 추가 메서드 log()
를 호출하고 실제로 해당 로그를 인쇄하려는 경우 코드는 아래와 같이 변경됩니다.
@Service public class RectangleService { public Double area(Double r, Double h) { log(); return r*h; } public void log() { System.out.println("skip this"); } }
rectangleService
서비스의 @Mock
주석을 @Spy
로 변경하고 아래와 같이 일부 코드를 변경하면 결과에서 실제로 로그가 인쇄되는 것을 볼 수 있지만 메소드 area()
는 조롱됩니다. 즉, 원래 기능은 부작용에 대해서만 실행됩니다. 반환 값은 모의 값으로 대체됩니다.

@RunWith(MockitoJUnitRunner.class) public class CalculateAreaTest { @Spy RectangleService rectangleService; @Mock SquareService squareService; @Mock CircleService circleService; @InjectMocks CalculateArea calculateArea; @Test public void calculateRectangleAreaTest() { Mockito.doCallRealMethod().when(rectangleService).log(); Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d); Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d); Assert.assertEquals(new Double(20d),calculatedArea); } }
Controller
또는 RequestHandler
를 테스트하는 방법은 무엇입니까?
위에서 배운 것에서 우리 예제에 대한 컨트롤러의 테스트 코드는 다음과 같을 것입니다.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class AreaController { @Autowired CalculateArea calculateArea; @RequestMapping(value = "api/area", method = RequestMethod.GET) @ResponseBody public ResponseEntity calculateArea( @RequestParam("type") String type, @RequestParam("param1") String param1, @RequestParam(value = "param2", required = false) String param2 ) { try { Double area = calculateArea.calculateArea( Type.valueOf(type), Double.parseDouble(param1), Double.parseDouble(param2) ); return new ResponseEntity(area, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity(e.getCause(), HttpStatus.INTERNAL_SERVER_ERROR); } } }
import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @RunWith(MockitoJUnitRunner.class) public class AreaControllerTest { @Mock CalculateArea calculateArea; @InjectMocks AreaController areaController; @Test public void calculateAreaTest() { Mockito .when(calculateArea.calculateArea(Type.RECTANGLE,5.0d, 4.0d)) .thenReturn(20d); ResponseEntity responseEntity = areaController.calculateArea("RECTANGLE", "5", "4"); Assert.assertEquals(HttpStatus.OK,responseEntity.getStatusCode()); Assert.assertEquals(20d,responseEntity.getBody()); } }
위의 컨트롤러 테스트 코드를 보면 잘 작동하지만 한 가지 기본적인 문제가 있습니다. 실제 API 호출이 아닌 메서드 호출만 테스트한다는 것입니다. API 매개변수와 API 호출 상태가 서로 다른 입력에 대해 테스트해야 하는 모든 테스트 사례가 누락되었습니다.
이 코드가 더 좋습니다.
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; @RunWith(SpringJUnit4ClassRunner.class) public class AreaControllerTest { @Mock CalculateArea calculateArea; @InjectMocks AreaController areaController; MockMvc mockMvc; @Before public void init() { mockMvc = standaloneSetup(areaController).build(); } @Test public void calculateAreaTest() throws Exception { Mockito .when(calculateArea.calculateArea(Type.RECTANGLE,5.0d, 4.0d)) .thenReturn(20d); mockMvc.perform( MockMvcRequestBuilders.get("/api/area?type=RECTANGLE¶m1=5¶m2=4") ) .andExpect(status().isOk()) .andExpect(content().string("20.0")); } }
여기에서 MockMvc
가 실제 API 호출을 수행하는 작업을 수행하는 방법을 볼 수 있습니다. 또한 status()
및 content()
)와 같은 특수 매처가 있어 콘텐츠 유효성을 쉽게 확인할 수 있습니다.
JUnit 및 Mock을 사용한 Java 통합 테스트
이제 코드 작업의 개별 단위를 알았으므로 예상대로 서로 상호 작용하는지 확인하겠습니다.
먼저 애플리케이션 시작 중 Spring 컨텍스트 초기화 시 발생하는 것과 동일한 모든 빈을 인스턴스화해야 합니다.
이를 위해 TestConfig.java
라고 합시다. 클래스의 모든 빈을 정의합니다.
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class TestConfig { @Bean public AreaController areaController() { return new AreaController(); } @Bean public CalculateArea calculateArea() { return new CalculateArea(); } @Bean public RectangleService rectangleService() { return new RectangleService(); } @Bean public SquareService squareService() { return new SquareService(); } @Bean public CircleService circleService() { return new CircleService(); } }
이제 이 클래스를 사용하고 통합 테스트를 작성하는 방법을 살펴보겠습니다.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {TestConfig.class}) public class AreaControllerIntegrationTest { @Autowired AreaController areaController; MockMvc mockMvc; @Before public void init() { mockMvc = standaloneSetup(areaController).build(); } @Test public void calculateAreaTest() throws Exception { mockMvc.perform( MockMvcRequestBuilders.get("/api/area?type=RECTANGLE¶m1=5¶m2=4") ) .andExpect(status().isOk()) .andExpect(content().string("20.0")); } }
여기에서 몇 가지 사항이 변경됩니다.
-
@ContextConfiguration(classes = {TestConfig.class})
— 이것은 모든 빈 정의가 있는 테스트 케이스를 알려줍니다. - 이제
@InjectMocks
대신 다음을 사용합니다.
@Autowired AreaController areaController;
다른 모든 것은 동일하게 유지됩니다. 테스트를 디버그하면 RectangleService
에서 return r*h
가 계산되는 area()
메서드의 마지막 줄까지 코드가 실제로 실행되는 것을 볼 수 있습니다. 즉, 실제 비즈니스 로직이 실행됩니다.
이것은 통합 테스트에서 사용할 수 있는 메서드 호출이나 데이터베이스 호출에 대한 조롱이 없다는 것을 의미하지 않습니다. 위의 예에서는 타사 서비스나 데이터베이스가 사용되지 않았으므로 mock을 사용할 필요가 없었습니다. 실생활에서 이러한 애플리케이션은 드물며 데이터베이스나 타사 API 또는 둘 다를 사용하는 경우가 많습니다. 이 경우 TestConfig
클래스에 Bean을 생성할 때 실제 객체를 생성하지 않고 Mocked 객체를 생성하여 필요할 때마다 사용합니다.
보너스: 대형 개체 테스트 데이터를 생성하는 방법
종종 백엔드 개발자가 단위 또는 통합 테스트를 작성하지 못하게 하는 것은 모든 테스트를 위해 준비해야 하는 테스트 데이터입니다.
일반적으로 데이터가 충분히 작고 하나 또는 두 개의 변수가 있는 경우 테스트 데이터 클래스의 개체를 만들고 일부 값을 할당하는 것이 쉽습니다.
예를 들어, 만약 우리가 모의 객체가 다른 객체를 반환할 것으로 예상한다면, 모의 객체에 대해 함수가 호출될 때 우리는 다음과 같이 할 것입니다:
Class1 object = new Class1(); object.setVariable1(1); object.setVariable2(2);
그리고 이 객체를 사용하기 위해 다음과 같이 할 것입니다.
Mockito.when(service.method(arguments...)).thenReturn(object);
위의 JUnit 예제에서는 괜찮지만 위의 Class1
클래스의 멤버 변수가 계속 증가하면 개별 필드를 설정하는 것이 상당히 어렵습니다. 때로는 클래스에 기본이 아닌 다른 클래스 멤버가 정의되어 있는 경우도 있습니다. 그런 다음 해당 클래스의 개체를 만들고 개별 필수 필드를 설정하면 일부 상용구를 달성하기 위한 개발 노력이 더욱 늘어납니다.
해결책은 위 클래스의 JSON 스키마를 생성하고 해당 데이터를 JSON 파일에 한 번 추가하는 것입니다. 이제 Class1
객체를 생성하는 테스트 클래스에서 객체를 수동으로 생성할 필요가 없습니다. 대신 JSON 파일을 읽고 ObjectMapper
를 사용하여 필요한 Class1
클래스에 매핑합니다.
ObjectMapper objectMapper = new ObjectMapper(); Class1 object = objectMapper.readValue( new String(Files.readAllBytes( Paths.get("src/test/resources/"+fileName)) ), Class1.class );
이것은 JSON 파일을 만들고 값을 추가하는 일회성 작업입니다. 그 이후의 모든 새 테스트는 새 테스트의 필요에 따라 필드가 변경된 해당 JSON 파일의 사본을 사용할 수 있습니다.
JUnit 기본 사항: 다중 접근 방식 및 양도 가능한 기술
빈을 주입하기로 선택하는 방법에 따라 Java 단위 테스트를 작성하는 많은 방법이 있다는 것은 분명합니다. 불행히도 이 주제에 대한 대부분의 기사는 한 가지 방법만 있다고 가정하는 경향이 있으므로 특히 다른 가정으로 작성된 코드로 작업할 때 혼동하기 쉽습니다. 바라건대, 여기에서 우리의 접근 방식은 개발자가 조롱하는 올바른 방법과 사용할 테스트 러너를 파악하는 데 시간을 절약해 줍니다.
우리가 사용하는 언어나 프레임워크(Spring 또는 JUnit의 새 버전일 수도 있음)와 상관없이 개념적 기반은 위의 JUnit 튜토리얼에서 설명한 것과 동일하게 유지됩니다. 즐거운 테스트!