순서 1. USER LOGIN 기능을 구현해야하기때문에 CONTROLLER의 GETMAPPING으로 코드를 구현하자 2. kakaoservice에서 카카오 로그인을 구현하며, jwt토큰을 controller에 전달해준다 3. rest template가 원래는 rest template builder로 생성하고 있었는데. bean 수동등록을 해서 관리해본다.
package com.sparta.myselectshop.config;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
// RestTemplate 으로 외부 API 호출 시 일정 시간이 지나도 응답이 없을 때
// 무한 대기 상태 방지를 위해 강제 종료 설정
.setConnectTimeout(Duration.ofSeconds(5)) // 5초
.setReadTimeout(Duration.ofSeconds(5)) // 5초
.build();
}
}
rest template는 추가 옵션을 설정하는 경우가 많아서 수동으로 bean을 등록한다. 위 설정은 외부 api를 호출하였을때 응답이 장시간 없을경우 무한대기에 걸릴 수 있다. 외부 api가 5초안에 응답을 내려주지 않는다면, 요청을 끊어버리는것이다.
@RepeatedTest(value = 반복횟수, name = "설명") 테스트를 반복 할 수 있다
@ParameterizedTest 전달되는 파라미터 수만큼 반복된다
Assertions
assertEquals(기대값, 검증값) : 검증값이 기대값에 맞는지 확인하는 메서드 assertNotEquals(기대값, 검증값) : 검증값이 기대값이랑 틀린지 확인하는 메서드 assertTrue(검증값) : 검증값이 true인지 확인하는 메서드 assertFalse(검증값) : 검증값이 false인지 확인하는 메서드 assertNotNull(검증값) : 검증값이 null이 아닌지 확인하는 메서드 assertNull(검증값) : 검증값이 null인지 확인하는 메서드 assertThrows(예외클래스타입, 함수) : 해당 예외클래스 타입이 나오는지 확인하는 메서드
given - when - then 테스트 코드 작성의 대중적인 작성 양식이라고 생각하면 된다.
@Test
@DisplayName("계산기 연산 성공 테스트")
void test1() {
// given
int num1 = 5;
String op = "/";
int num2 = 2;
// when
Double result = calculator.operate(num1, op, num2);
// then
assertNotNull(result);
assertEquals(2.5, result);
}
given 테스트 하고자 하는 대상의 값들을 미리 선언해놓는다
when 테스트하고자하는 메서드를 실행한다
then 발생하고자 하는 결과를 assertion으로 예측하고 맞는지 확인한다.
@Test
@DisplayName("계산기 연산 실패 테스트 : 연산자가 잘못됐을 경우")
void test1_2() {
// given
int num1 = 5;
String op = "?";
int num2 = 2;
// when - then
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> calculator.operate(5, "?", 2));
assertEquals("잘못된 연산자입니다.", exception.getMessage());
}
실행과 예측이 만약 하나로 되는 경우가 있다. 그럴경우에는 위 처럼 when과 then을 같이 넣는다.
Mokito service를 테스트하고 싶다, 하지만 기본 service로직은 repository를 주입받고 있다 테스트 코드를 작성시에는 어떻게 해야할까? 진짜 레포지토리와 연동해야하나?
우리는 service의 메서드의 동작만 체크하면된다.
그래서 가짜객체를 생성해주는 mock Object를 사용한다
@ExtendWith(MockitoExtension.class) // @Mock 사용을 위해 설정합니다.
class ProductServiceTest {
@Mock
ProductRepository productRepository;
@Mock
FolderRepository folderRepository;
@Mock
ProductFolderRepository productFolderRepository;
@ExtendWith으로 mockito를 사용하기 위한 설정을하고
@mock 어노테이션으로 repository를 가져온다.
@Test
@DisplayName("관심 상품 희망가 - 최저가 이상으로 변경")
void test1() {
// given
Long productId = 100L;
int myprice = ProductService.MIN_MY_PRICE + 3_000_000;
//희망 가격을 설정할 DTO 객체 생성 및 희망가격 적용
ProductMypriceRequestDto requestMyPriceDto = new ProductMypriceRequestDto();
requestMyPriceDto.setMyprice(myprice);
//관심상품을 등록할 유저객체 생성
User user = new User();
//PRODUCT 객체 생성에 필요한 관심상품 등록
ProductRequestDto requestProductDto = new ProductRequestDto(
"Apple <b>맥북</b> <b>프로</b> 16형 2021년 <b>M1</b> Max 10코어 실버 (MK1H3KH/A) ",
"https://shopping-phinf.pstatic.net/main_2941337/29413376619.20220705152340.jpg",
"https://search.shopping.naver.com/gate.nhn?id=29413376619",
3515000
);
//PRODUCT 객체 생성
Product product = new Product(requestProductDto, user);
//ProductService 객체 생성 및 테스트에 필요한 레파지토리가 주입됨
ProductService productService = new ProductService(productRepository, folderRepository, productFolderRepository);
//REPOSITORY에서 PRODUCTID로 상품을 찾았을경우, 해당 상품이 존재한다고 가정을 하고
//willReturn(Optional.of(product)) Optional.of(product) 이 객체를 반환하도록 설정
given(productRepository.findById(productId)).willReturn(Optional.of(product));
// when
//updateProduct 메서드를 호출하여 희망가격으로 업데이트를함
ProductResponseDto result = productService.updateProduct(productId, requestMyPriceDto);
// then
// update 메서드를 통해 변경한 값과, 희망 가격이 같은지 조회
assertEquals(myprice, result.getMyprice());
}
위는 구현된 단위테스트이다.
통합 테스트
두개 이상의 모듈이 상호 연결된 상태를 테스트 할 수 있음. 모듈 간에 연결에서 발생하는 에러를 검증 가능함.
단위 테스트와 다른점
모듈간 상호 검증 불가능 vs 모듈간의 연결에서 발생하는 오류를 검증 가능함
단위 테스트시에는 spring은 동작하지 않아도 됐음 vs 통합 테스트 시에는 spring이 동작되어야하며, @SpringBootTest 어노테이션을 부착하면됌 테스트 수행시 스프링이 동작을 하며 ioc나, di기능을 수행가능함.
failed to lazily initialize a collection of role: com.sparta.myselectshop.entity.Product.productFolderList: could not initialize proxy - no Session
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.sparta.myselectshop.entity.Product.productFolderList: could not initialize proxy - no Session
at org.hibernate.collection.spi.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:634)
at org.hibernate.collection.spi.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:217)
at org.hibernate.collection.spi.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:613)
at org.hibernate.collection.spi.AbstractPersistentCollection.read(AbstractPersistentCollection.java:136)
at org.hibernate.collection.spi.PersistentBag.iterator(PersistentBag.java:369)
at com.sparta.myselectshop.dto.ProductResponseDto.<init>(ProductResponseDto.java:29)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.ArrayList$Itr.forEachRemaining(ArrayList.java:1003)
at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1845)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at org.springframework.data.domain.Chunk.getConvertedContent(Chunk.java:121)
at org.springframework.data.domain.PageImpl.map(PageImpl.java:86)
at com.sparta.myselectshop.service.ProductService.getProducts(ProductService.java:74)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:355)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:716)
at com.sparta.myselectshop.service.ProductService$$SpringCGLIB$$0.getProducts(<generated>)
at com.sparta.myselectshop.service.ProductServiceIntegrationTest.test3(ProductServiceIntegrationTest.java:97)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
public Page<ProductResponseDto> getProducts(User user,
int page, int size, String sortBy, boolean isAsc) {
// 페이징 처리
Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
Sort sort = Sort.by(direction, sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
// 사용자 권한 가져와서 ADMIN 이면 전체 조회, USER 면 본인이 추가한 부분 조회
UserRoleEnum userRoleEnum = user.getRole();
Page<Product> productList;
if (userRoleEnum == UserRoleEnum.USER) {
// 사용자 권한이 USER 일 경우
productList = productRepository.findAllByUser(user, pageable);
} else {
productList = productRepository.findAll(pageable);
}
return productList.map(ProductResponseDto::new);
}
위 코드를 확인하면, transactional 어노테이션이 적용되어있지 않다.
@OneToMany(mappedBy = "product")
private List<ProductFolder> productFolderList = new ArrayList<>();
productFolderList는 one to many로 lazy로딩이 설정되어 있다.
일단, all by user은 user를 찾아오며 그와 연관된 객체(폴더리스트)까지 찾아온다,
다만 lazy로딩이라 사용되지 않는 객체는 proxy라는 객체를 가리키게 한다
proxy는 실제 클래스를 상속 받아서 만들어지는 가짜 객체이다, 하이버네이트가 자동으로 생성하며, 사용하는 입장에서는 프록시인지 아닌지 모른다, 프록시 객체는 실제 데이터를 저장하지 않지만, 실제 데이터를 저장하는 참조만 보관한다.
레이지 로딩의 경우 프록시 객체가 연관된 참조를 불러올때 같이 불러와지며 실제 객체를 참조하려고 할때 실제 객체를 가져오게 된다
현지 productFolderList는 findAllByUser를 사용하였을때 프록시객체가 만들어졌지만, transaction을 사용하지않아 저 메소드를 호출한후 사라져버린다, 그래서 productlist를 참조하는순간 lazyInitializationException이 생기는것,