단위테스트란?
- 소스코드의 특정 모듈이 의도된 대로 정확히 작동하는지 검증하는 절차
- 모든 함수와 메소드에 대한 테스트 케이스(Test case)를 작성하는 절차
- JUnit은 보이지 않고 숨겨진 단위 테스트를 끌어내어 정형화시켜 단위테스트를 쉽게 해주는 테스트 지원 프레임워크
junit
- 단정(assert) 메서드로 테스트 케이스의 수행 결과를 판별합니다. ( assertEquals(예상val, 실제val))
- JUnit4 부터는 테스트를 지원하는 어노테이션을 제공합니다. ( @Test, @Before, @After)
- @Test 메서드가 호출할 때마다 새로운 인스턴스를 생성하여 독립적인 테스트가 이루어지게 합니다.
Juiit 지원 어노테이션
@Test
- 해당 어노테이션이 선언되면 메서드는 테스트를 수행하는 메서드가 됩니다. (단위 테스트 선언)
- JUnit은 각각의 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 원칙으로 @Test 마다 객체를 생성합니다.
- @Test(timeout=6000) 단위는 밀리, 해당 시간을 넘기면 실패합니다.
- @Test(expected=NullPointerException)은 NullPointerException이 발생하면 통과입니다.
@Ignore
- 해당 어노테이션의 메서드는 테스트를 실행하지 않습니다.
@Before
- 해당 어노테이션의 메서드는 @Test 메서드가 실행되기 전에 먼저 실행됩니다.
- 테스트 이전에 실행 할 메소드를 지정합니다.
- @Test 메소드가 실행 될 때마다 객체를 생성하여 실행합니다.
- @Test 메서드에서 공통으로 사용하는 코드를 @Before 메서드에 선언하여 사용하면 됩니다.
@After
- 해당 어노테이션의 메서드는 @Test 메소드가 실행된 후 실행됩니다.
- 테스트 이후에 실행 할 메소드를 지정합니다.
- @Test 메소드가 실행 될 때마다 객체를 생성하여 실행합니다.
@BeforeClass
- 해당 어노테이션의 메소드는 @Test 메소드보다 먼저 한번만 수행되어야 할 경우에 사용하면 됩니다.
- 테스트 이전에 실행 할 메소드를 지정합니다.
- @Before와 차이점은 한번만 실행되며 static으로 선언하여야 합니다.
@AfterClass
- 해당 어노테이션의 메소드는 @Test 메소드보다 나중에 한번만 수행되어야 할 경우에 사용하면 됩니다.
- 테스트 이후에 실행 할 메소드를 지정합니다.
- @After와 차이점은 한번만 실행되고 static으로 선언하여야 합니다.
MockMvc, perform()
assert 메소드
- assertEquals(a, b); : 객체 a와 b의 값이 일치함을 확인합니다.
- assertArrayEquals(a, b) : 배열 a와 b의 값이 일치함을 확인합니다.
- assertSame(a, b); : 객체 a와 b가 같은 객체임을 확인합니다. 두 객체의 레퍼런스가 동일한가를 확인합니다.
- assertTrue(a); : 조건 a가 참인가를 확인합니다.
- assertNotNull(a); : 객체 a가 null이 아님을 확인합니다.
perform() 메소드
- DispatcherServlet에 요청합니다.
- get, post, put, delete, fileUpload 등의 메소드 제공합니다.
- ResultActions() 호출합니다.
MockMvc 메소드
- 서버를 실행하지 않고 스프링 MVC 동작을 재현할 수 있는 클래스입니다.
- MockMvc는 TestDispatcherServlet에게 요청합니다.
MockHttpServletRequestBuilder() 메소드
- param/params : 요청 파라미터 설정
- header/headers : 요청 헤더 설정
- cookie : 쿠키 설정
- content : 요청 본문 설정
- requestAttr : 요청 스코프에 객체 설정
- flashAttr : 플래시 스코프에 객체를 설정
- sessionAttr : 세션 스코프에 객체를 설정
MockMvcResultMatchers() 메소드
- status : HTTP 상태 코드 검증
- header : 응답 헤더의 상태 검증
- cookie : 쿠키 상태 검증
- content : 응답 한 본문 내용 검증
- view : 반환 된 뷰 이름 검증
- forwardedUrl : 경로 검증
- redirectedUrl : 경로나 url 검증
- model : 모델 상태 검증
- flash : 플래시 스코프 상태 검증
- request : 비동기 처리의 상태나 요청 스코프의 상태, 세션 스코프 상태 검증
ResultActions().andExpect()
- 실행 결과를 검증 인수 설정합니다.
ResultActions().andDo()
- 실행 결과를 처리할 수 있는 인수 지정합니다.
log()
- 디버깅 레벨에서 로그를 출력합니다.
print()
- 실행 결과를 출력합니다.
스프링테스트지원 어노테이션
@RunWith(SpringJUnit4ClassRunner.class)
- 해당 어노테이션은 JUnit 프레임워크의 테스트 실행방법을 확장할 때 사용하는 어노테이션입니다.
- SpringJUnit4ClassRunner 클래스를 지정하면 JUnit이 테스트를 진행하는 중에 ApplicationContext를 만들고 관리하는 작업을 진행해줍니다.
- 각각의 테스트 별로 객체가 생성되더라도 싱글톤(Singletone)의 ApplicationContext를 보장합니다.
- 지정하지 않으면 SpringRunner.class로 사용됩니다.
@ContextConfiguration
- Spring Bean 메타 설정 파일의 패키지에서 설정 파일을 찾습니다. (설정 파일의 위치를 지정할 때 사용함.)
- 지정하지 않는다면 테스트 파일의 패키지에서 설정 파일을 찾습니다.
@Autowired
- 스프링 DI에서 사용되는 어노테이션입니다
- 해당 변수에 자동으로 빈(Bean)을 매핑해줍니다.
- 스프링 빈(Bean) 설정 파일을 읽이 위해 GenericXmlApplicationContext를 사용할 필요가 없습니다.
- 변수, setter메서드, 생성자, 일반메서드에 적용이 가능합니다.
- 의존하는 객체를 주입할 때 주로 Type을 이용합니다.
- xml 빈 설정 파일의 <property>, <constructor-arg> 태그와 동일한 역할을 합니다.
@WebAppConfiguration
- 웹 애플리케이션 전용 DI 컨테이너로 처리합니다.
컨트롤러 테스트시 확인해야 하는 것은 아래와 같다.
- 작동 테스트 -> 해당 주소에 적합한 데이터를 보냈을 때 정상적인 답이 와야 한다.
- 예외 테스트(실패테스트) -> 예외발생시 정보를 그대로 내보내면 안됨. 어떤 예외가 발생할지 예상하고 적당한 정보만 클라이언트로 넘기고 DB등에 로그를 남기는 방식으로 구현하여야 한다.
@WebMvcTest
@WebMvcTest는 스프링 부트에서 제공하는 어노테이션 중 하나로, 웹 어플리케이션의 MVC컨트롤러를 테스트하기 위해 사용된다.특정 컨트롤러를 대상으로 하는 단위 테스트를 작성할 때 사용됩니 다. 이 어노테이션을 사용하면 웹 레이어에서 발생하는 요청과 응답을 테스트할 수 있습니다.
@WebMvcTest(value = {UserRestController.class}) 이렇게 하면 특정 Controller를 IoC 컨테이너 에 등록할 수 있습니다.
- 원하는 컨트롤러를 등록하여 직접 메모리에 올려줘야 한다.,
- Security Config를 등록하여 직접 메모리에 올려줘야 한다.
컨트롤러 단위테스트
- Mock -> Mockito에서 사용되며, 진짜 객체를 추상화된 가짜(mock) 객체로 만듭니다. 이 가짜 객체는 Mockito 환경에 주입됩니다.
- InjectMocks->Mockito에서 사용되며, Mock된 가짜 객체를 진짜 객체에 주입함
- MockBean -> IoC컨테이너에서 사용되며, IoC 컨테이너에 Mock 객체를 등록하여 스프링 의 의존성 주입(Dependency Injection)을 받을 수 있게 합니다.
- Spy -> Mockito에서 사용되며, 진짜 객체를 만들고 Mockito 환경에 주입합니다. *SpyBean->IoC컨테이너에서 사용되며,@Spy로 생성된 객체를 스프링 애플리케이션 컨 텍스트에 주입합니다.
컨트롤러 테스트 시 서비스를 진짜로 올리지 않는다. 하지만 가짜 빈 껍데기라도 있어야 오류가 나지 않기 때문에 생성을 해준다. 실제 내용은 없고 작동만 하게 해줌
@MockBean 서비스클래스 를 입력해주면 가짜 서비스가 생성됨. 이를 stub이라고 한다. (더 정확하게는 반환값까지 설정하여 서비스의 반환값까지 가정)
하지만 일부기능들은 가짜가 아닌 진짜를 메모리에 올려야 한다. 예를 등어 GlobalExceptionHandler는 진짜로 메모리에 올려서 테스트를 해봐야 한다.
@Import를 사용하면 진짜 메모리에 올릴 수 있다.
하지만 GlobalExceptionHandler가 의존하고 있는 ErrorLogJPARepository는 @MockBean으로 등록해줘야 한다.
MockBean이란 ErrorLogJPARepository의 메서드만 동일하게 구현체로 만들어서 스프링 컨텍스트에 추가해준다. 즉 행위의 구체적인 부분은 없고, 추상적 행위만 제공되는 가짜 객체이다.
@Import 는 스프링 컨텍스트에 추가적인 구성 요소를 등록하기 위해 사용되며, @SpyBean 은 스프 링 애플리케이션 컨텍스트에 있는 빈을 스파이(Spy) 객체로 대체하기 위해 사용됩니다. @Import 는 스프링 컨텍스트에 구성 요소를 추가로 등록하고 관리하는 데 사용되며, @SpyBean 은 스프링 빈 을 스파이 객체로 대체하여 테스트에 사용합니다.
@Import({
GlobalExceptionHandler.class,
SecurityConfig.class
})
@WebMvcTest(controllers = {CartRestController.class})
public class CartRestControllerTest extends DummyEntity {
@MockBean
private ErrorLogJPARepository errorLogJPARepository;
@MockBean CartService cartService;
@Autowired
private MockMvc mvc; //@webMvcTest를 하면 SpringContext에 등록되기 때문에 DI할 수 있다. mvc를 이용해 api요청을 할 수 있다.
@Autowired
private ObjectMapper om; /jsom으로 직렬화하기 위해 DI, @webMvcTest를 하면 ObjectMapper가 SpringContext에 등록되기 때문에 DI할 수 있다.
@Test //테스트코드임을 정의
@DisplayName("/carts/update 장바구니 수정")
@WithMockCustomUser(username = "yunzae", roles = "ROLE_USER" , userId = 1 )
public void update_test() throws Exception {
// given
List<CartRequest.UpdateDTO> requestDTOs = new ArrayList<>();
CartRequest.UpdateDTO d1 = new CartRequest.UpdateDTO();
d1.setCartId(1);
d1.setQuantity(10);
CartRequest.UpdateDTO d2 = new CartRequest.UpdateDTO();
d2.setCartId(2);
d2.setQuantity(10);
requestDTOs.add(d1);
requestDTOs.add(d2);
// 위의 코드는 요청DTO 생성, 아래는 직렬화시킨 데이터
String requestBody = om.writeValueAsString(requestDTOs);
System.out.println("테스트 : "+requestBody);
// stub, 서비스가 실행됐을 때 반환되는 값을 가정(설정)한다.
List<Product> productDummy = productDummyList();
List<Option> optionDummy = optionDummyList(productDummy);
List<Cart> carts = new ArrayList<>();
Cart cart1 = Cart.builder().id(d1.getCartId()).option(optionDummy.get(0)).price(optionDummy.get(0).getPrice()*d1.getQuantity()).quantity(d1.getQuantity()).user(null).build();
Cart cart2 = Cart.builder().id(d2.getCartId()).option(optionDummy.get(1)).price(optionDummy.get(1).getPrice()*d2.getQuantity()).quantity(d2.getQuantity()).user(null).build();
carts.add(cart1);
carts.add(cart2);
CartResponse.UpdateDTO responseDTO= new CartResponse.UpdateDTO(carts);
Mockto을 이용하여 stub을 설정할 수 있다.
Mockito.when(cartService.update(anyList(),anyInt())).thenReturn(responseDTO); //이 코드에서 cartService의 update메소드의 반환값을 설정한다. update(int형파라미터,int형파라미터)가 실행될 때 responseDTO를 반환한다는 의미이다.
String DTOjson = om.writeValueAsString(responseDTO); //DTO를 json으로 직렬화한다.
System.out.println("테스트 : "+DTOjson);
// when , api에 요청을 하고 반환 데이트를 받아온다.
ResultActions result = mvc.perform( //perform을 이용하면 요청을 보낼 수 있다.
MockMvcRequestBuilders
.post("/carts/update") //요청메소드 선택, get,post등 괄호안엔 주소
.content(requestBody) // body에 데이터 넣기
.contentType(MediaType.APPLICATION_JSON) //json타입
);
String responseBody = result.andReturn().getResponse().getContentAsString(); //리턴값을 문자열로 가져오기
System.out.println("테스트 : "+responseBody);
// then
result.andExpect(MockMvcResultMatchers.jsonPath("$.success").value("true")); 값 비교, 응답받은 데이터에서 값을 가져올때는 MockMvcResultMatchers.jsonPath("위치")를 이용하면 된다. 위치에는 "$.response.carts[0].cartId" 처럼 위치가 들어가야한다. (.은 루트이고 response의 값 중에서 carts의 첫번째 값의 cartid)
result.andExpect(MockMvcResultMatchers.jsonPath("$.response.carts[0].cartId").value(1)); //아래 응답예시와 비교해보기
result.andExpect(MockMvcResultMatchers.jsonPath("$.response.carts[0].optionName").value("01. 슬라이딩 지퍼백 크리스마스에디션 4종"));
result.andExpect(MockMvcResultMatchers.jsonPath("$.response.carts[0].quantity").value(10));
result.andExpect(MockMvcResultMatchers.jsonPath("$.response.carts[0].price").value(100000));
}
}
{
"success": true,
"response": {
"carts": [
{
"cartId": 4,
"optionId": 1,
"optionName": "01. 슬라이딩 지퍼백 크리스마스에디션 4종",
"quantity" : 10,
"price": 100000
},
{
"cartId": 5,
"optionId": 2,
"optionName": "02. 슬라이딩 지퍼백 플라워 에디션 4종",
"quantity" : 10,
"price": 109000
}
],
"totalPrice": 209000
},
"error": null
}
mvc.perform 할때 Path Variable은 .get(주소).param(키,밸류)로 넣어도 되고 주소에 주소?키=밸류 형태로 넣어도 된다.
이외의 기능들도 많이 제공한다. 메소드를 보거나 검색을 해서 필요에 따라 사용하면 된다. 특정데이터를 넣거나 가져올 때
유저인증이 필요한 컨트롤러 테스트
컨트롤러 테스트를 하다보면 유저인증(로그인)이 필요한 경우가 있다. 이 때 인증된 가짜 유저를 만들어 줄 수 있다.
@WithMockUser(username="dsa@naver.com", roles="User") 어노테이션을 이용하면 이 유저정보로 로그인이 된다.
@WithMockUser(username = "dsadsa@naver.com", roles = "USER")
@Test
public void update_test() throws Exception {
// ...생략 }
하지만 만약 컨트롤러혹은 서비스에서 userdetalis 정보(Priciple,Authentic등으로)를 이용한다면 다른 방식을 써야한다. 위의 방식을 쓴다면 다른 방법을 써야한다.
여러방법이 있지만 여러 상황에 모두 사용할 수 있는 어노테이션을 만드는 방식이 좋다고 생각한다.
(간단하게 하려면
setup()에서 시큐리티컨텍스트에[ 등록할 순 있지만 롤이 바뀌거나 테스트마다 상황이 다른 경우 불편하다.)
@WithMockCustomUser 만드는 방법
테스트코드
@Test
@WithMockCustomUser(username = "yunzae", roles = "ROLE_USER" , userId = 1 )
public void update_test() throws Exception {)
어노테이션 코드
@withSecurityContext의 팩토리를 내가 만든 커스텀 팩토리로 설정해준다. 기본적인 팩토리는 유저디테일이 비어있다.
여기서 데이터를 정의해주면 어노테이션에서도 수정해줄 수 있다.(위의 코드 처럼)
package com.example.kakao._core.util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.springframework.security.test.context.support.WithSecurityContext;
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
int userId() default 1;
String username() default "yunzae";
String roles() default "ROLE_USER";
}
시큐리티 컨텍스트 팩토리:
userdetails에 값을 넣어서 유저인스턴스를 생성하여 컨텍스트에 직접등록 customUser 파라티머로 어노에티션의 데이터(userId,usename,rolse)가 넘어온다.
package com.example.kakao._core.util;
import com.example.kakao.user.User;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.test.context.support.WithSecurityContextFactory;
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> {
@Override
public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
User principal = User.builder().id(customUser.userId()).username(customUser.username()).roles(customUser.roles()).build();
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
principal, principal.getPassword(), principal.getAuthorities());
context.setAuthentication(authentication);
return context;
}
}
위의 방법이 아닌 @UserDetails(value="유저", userDetailsServiceBeanName = "내가 커스텀한 유저디테일서비스")를 써도 된다.
이 방법은 UserdetailServiced의 loadByUsername 메소드를 이용해서 DB에서 유저를 가져온다.
하지만 DB를 이용하는 것이기 때문에 단위테스트 보다는 통합테스트에서 사용이 된다.
이 방법을 사용하려면 Before에서 유저를 미리 넣어줘야 한다.
valused에는 username에 들어간다. 구현에 따라 다르다. 이메일로 했으면 이메일이 들어가고, 불변 닉네임으로 했으면 닉네임이 들어간다.
userDetailsServiceBeanName에는 내가 커스텀한 유저디테일 서비스가 들어간다. 유저디테일서비스 빈이 하나라면 생략해도된다.
하지만 user에 userdetail을 implement한 경우에는 userService 를 넣어줘야 한다. U가 아닌 u이다.
참고: https://show400035.tistory.com/165 , 카카오테크캠퍼스 강의(메타코딩 강사님)
'I leaned > 스프링,스프링부트' 카테고리의 다른 글
@Transaction(readOnly=true) (0) | 2023.07.25 |
---|---|
AOP(관점지향프로그래밍) (0) | 2023.07.25 |
리포지토리 단위 테스트 (0) | 2023.07.17 |
Open In View (0) | 2023.07.17 |
DTO (0) | 2023.07.17 |