inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

묻고 답해요

173만명의 커뮤니티!! 함께 토론해봐요.

캐시를 포함한 Service Layer 테스트 가이드

미해결

Practical Testing: 실용적인 테스트 가이드

안녕하세요. 우빈님! 캐시를 처음 적용해 보아서 캐시를 포함한 비지니스 로직을 어떻게 작성하는 것이 좋을까? 에 대한 고민이 있어 글 남깁니다...! AS IS @Transactional public UosRestaurantMenuResponse getUosRestaurantMenu(UosRestaurantInput input) { // 학식 조회 UosRestaurant findUosRestaurant = uosRestaurantRepository.findByCrawlingDateAndRestaurantNameAndMealType(input.getDate(), input.getRestaurantName(), input.getMealType()) .orElseThrow(() -> new UosRestaurantMenuException(UosRestaurantMenuException.NOT_FOUND_MENU)); // 조회수 증가 findUosRestaurant.increaseView(); return UosRestaurantMenuResponse.of(findUosRestaurant); } TO BE @Transactional public UosRestaurantMenuResponse getUosRestaurantMenu(UosRestaurantInput input) { // 캐시에서 학식 조회 Optional<CacheUosRestaurant> cacheUosRestaurant = cacheUosRestaurantRepository .findById(CacheUosRestaurant.createId(input)); // 캐시에 학식이 존재하면 if(cacheUosRestaurant.isPresent()) { // 조회수 증가 cacheUosRestaurant.get().increaseView(); CacheUosRestaurant saveCacheRestaurant = cacheUosRestaurantRepository.save(cacheUosRestaurant.get()); return UosRestaurantMenuResponse.of(saveCacheRestaurant); } // 학식 조회 UosRestaurant findUosRestaurant = uosRestaurantRepository.findByCrawlingDateAndRestaurantNameAndMealType(input.getDate(), input.getRestaurantName(), input.getMealType()) .orElseThrow(() -> new UosRestaurantMenuException(UosRestaurantMenuException.NOT_FOUND_MENU)); // 조회수 증가 findUosRestaurant.increaseView(); // 캐시에 저장 cacheUosRestaurantRepository.save(CacheUosRestaurant.of(findUosRestaurant)); return UosRestaurantMenuResponse.of(findUosRestaurant); } 캐시에 대한 로직이 추가될 때 위와 같이 하나의 서비스 레이어 메소드에 작성하는 것이 좋은걸까요? 캐시에 대한 테스트 코드를 작성할 때 캐시를 사용할 때와 캐시를 사용하지 않을 때를 상황을 구분하여 작성 하는 것이 맞을까요..?(그것이 좋겠죠....? -> 기존 비지니스 로직 테스트 코드를 수정하는것이 최선일까? 에대한 의문이 들어서 질문 드렸습니다.) 스프링에서 CacheManager를 이용하여 @Cacheable 을 활용하는 방법이 있는데, CrudRepository를 사용하는 방법과 CacheManager를 사용하는 방법 중 어느 것이 더 좋은(?) 방법 인지 말씀주시면 감사하겠습니다. 4. 마지막으로 캐시를 사용할 때 깔끔하게 비지니스 로직을 작성할 수 있는 노하우 말씀주시면 감사하겠습니다.!!! 다소 질문이 난해한데 너그럽게 이해해주시면 감사하겠습니다.ㅠㅠ 좋은 강의 잘 듣고 있습니다. 다음 강의도 기대할께요^^!! 감사합니다.

  • spring
  • tdd
  • jpa
  • mockito
  • 소프트웨어-테스트
  • junit5
개발하는쿼카 댓글 2 좋아요 1 조회수 450

@Transactional을 붙였을 때"만" Stock 감소 검증에 성공합니다

미해결

Practical Testing: 실용적인 테스트 가이드

각 테스트 케이스들을 검증하고, 이에 대해 값을 비워주기 위해 두 가지 방식을 소개해주셨습니다. 1) tearDown()을 사용해서 after each로 모두 날려주는 방식과 2) 클래스 레벨에서 테스트 클래스에 @Transactional 을 붙이는 방식 강의와 동일하게 "001" : 1000 : 2개, "002" : 3000 : 2개, "003" : 5000 가정 하에, "001", "001", "002", "003" 순으로 주문했다 하였습니다. 하지만, 문제는 1)을 사용하였을 때는 아래와 같은 에러가 발생하구요, 2)을 사용했을 때는 테스트 케이스가 정상 검증 됩니다. [Extracted: productNumbeㅏr, quantity] Expecting actual: [("001", 2), ("002", 2)] to contain exactly in any order: [("001", 0), ("002", 1)] elements not found: [("001", 0), ("002", 1)] and elements not expected: [("001", 2), ("002", 2)] 올려주신 깃허브 코드 5-7 또한 참고해보았으며, 코드 복붙까지 시도했는데 AfterEach 방식에서만 검증 실패가 등장하네요 // 보여주신 디버깅 방식 참조하여 디버깅도 해보았는데, 제 눈에는,,, 이상이 없었습니다... 미천한 디버깅 실력에 부끄러움만 앞서지만, 왜 트랜잭션 어노테이션에서만 검증 성공인지 의아합니다. 혹시 답변 가능하시다면 부탁드립니다,,! 코드 첨부합니다. OrderService package sample.cafekiosk.api.service.order; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import sample.cafekiosk.api.controller.order.OrderCreateRequest; import sample.cafekiosk.domain.order.Order; import sample.cafekiosk.domain.product.Product; import sample.cafekiosk.domain.product.ProductRepository; import sample.cafekiosk.domain.product.ProductType; import sample.cafekiosk.domain.stock.Stock; import sample.cafekiosk.domain.stock.StockRepository; import java.time.LocalDateTime; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class OrderService { // Product를 가져와야 하므로 의존 private final ProductRepository productRepository; private final OrderRepository orderRepository; private final StockRepository stockRepository; public OrderResponse createOrder(OrderCreateRequest request, LocalDateTime registeredDateTime) { List<String> productNumbers = request.getProductNumbers(); List<Product> products = findProductsBy(productNumbers); deductStockQuantities(products); Order order = Order.create(products, registeredDateTime); Order savedOrder = orderRepository.save(order); return OrderResponse.of(savedOrder); } private void deductStockQuantities(List<Product> products) { List<String> stockProductNumbers = extractStockProductNumbers(products); Map<String, Stock> stockMap = createStockMapBy(stockProductNumbers); Map<String, Long> productCountingMap = createCountingMapBy(stockProductNumbers); for (String stockProductNumber : new HashSet<>(stockProductNumbers)) { Stock stock = stockMap.get(stockProductNumber); int quantity = productCountingMap.get(stockProductNumber).intValue(); if (stock.isQuantityLessThan(quantity)) { throw new IllegalArgumentException("재고가 부족한 상품이 있습니다."); } stock.deductQuantity(quantity); } } private List<Product> findProductsBy(List<String> productNumbers) { List<Product> products = productRepository.findAllByProductNumberIn(productNumbers); Map<String, Product> productMap = products.stream() .collect(Collectors.toMap(Product::getProductNumber, p -> p)); return productNumbers.stream() .map(productMap::get) .collect(Collectors.toList()); } private static List<String> extractStockProductNumbers(List<Product> products) { return products.stream() .filter(product -> ProductType.containsStockType(product.getType())) .map(Product::getProductNumber) .collect(Collectors.toList()); } private Map<String, Stock> createStockMapBy(List<String> stockProductNumbers) { List<Stock> stocks = stockRepository.findAllByProductNumberIn(stockProductNumbers); return stocks.stream() .collect(Collectors.toMap(Stock::getProductNumber, s -> s)); } private static Map<String, Long> createCountingMapBy(List<String> stockProductNumbers) { return stockProductNumbers.stream() .collect(Collectors.groupingBy(p -> p, Collectors.counting())); } } OrderServiceTest package sample.cafekiosk.api.service.order; import jakarta.transaction.Transactional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.web.bind.annotation.RestController; import sample.cafekiosk.api.controller.order.OrderCreateRequest; import sample.cafekiosk.domain.product.Product; import sample.cafekiosk.domain.product.ProductRepository; import sample.cafekiosk.domain.product.ProductSellingStatus; import sample.cafekiosk.domain.product.ProductType; import sample.cafekiosk.domain.stock.Stock; import sample.cafekiosk.domain.stock.StockRepository; import java.time.LocalDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; import static org.junit.jupiter.api.Assertions.*; import static sample.cafekiosk.domain.product.ProductSellingStatus.SELLING; import static sample.cafekiosk.domain.product.ProductType.*; @ActiveProfiles("test") @SpringBootTest //@Transactional class OrderServiceTest { @Autowired private OrderService orderService; @Autowired private ProductRepository productRepository; @Autowired private OrderProductRepository orderProductRepository; @Autowired private OrderRepository orderRepository; @Autowired private StockRepository stockRepository; @AfterEach void tearDown() { orderProductRepository.deleteAllInBatch(); productRepository.deleteAllInBatch(); orderRepository.deleteAllInBatch(); stockRepository.deleteAllInBatch(); } @DisplayName("재고와 관련된 상품이 포함되어 있는 주문번호 리스트를 받아 주문을 생성한다.") @Test void createOrderWithStock() { // given LocalDateTime registeredDateTime = LocalDateTime.now(); Product product1 = createProduct(BOTTLE, "001", 1000); Product product2 = createProduct(BAKERY, "002", 3000); Product product3 = createProduct(HANDMADE, "003", 5000); productRepository.saveAll(List.of(product1, product2, product3)); Stock stock1 = Stock.create("001", 2); Stock stock2 = Stock.create("002", 2); stockRepository.saveAll(List.of(stock1, stock2)); OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001", "001", "002", "003")) .build(); // when OrderResponse orderResponse = orderService.createOrder(request, registeredDateTime); // then assertThat(orderResponse.getId()).isNotNull(); assertThat(orderResponse) .extracting("registeredDateTime", "totalPrice") .contains(registeredDateTime, 10000); assertThat(orderResponse.getProducts()).hasSize(4) .extracting("productNumber", "price") .containsExactlyInAnyOrder( tuple("001", 1000), tuple("001", 1000), tuple("002", 3000), tuple("003", 5000) ); List<Stock> stocks = stockRepository.findAll(); assertThat(stocks).hasSize(2) .extracting("productNumber", "quantity") .containsExactlyInAnyOrder( tuple("001", 0), tuple("002", 1) ); } @DisplayName("주문 번호리스트를 받아 주문을 생성한다.") @Test void createOrder() { // GIVEN LocalDateTime n = LocalDateTime.now(); Product product1 = createProduct(HANDMADE, "001", 1000); Product product2 = createProduct(HANDMADE, "002", 2000); Product product3 = createProduct(HANDMADE, "003", 3000); productRepository.saveAll(List.of(product1, product2, product3)); OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001","002")) .build(); // WHEN OrderResponse orderResponse = orderService.createOrder(request,n ); // THEN assertThat(orderResponse.getId()).isNotNull(); assertThat(orderResponse) .extracting("registeredDateTime","totalPrice") .containsExactlyInAnyOrder(n,3000); assertThat(orderResponse.getProducts()).hasSize(2) .extracting("productNumber", "price") .containsExactlyInAnyOrder( tuple("001", 1000), tuple("002", 2000) ); } @DisplayName("중복되는 상품번호 리스트로 주문을 생성할 수 있다.") @Test void createOrderWiuthDuplicatedProductNumbers() { // GIVEN LocalDateTime n = LocalDateTime.now(); Product product1 = createProduct(HANDMADE, "001", 1000); Product product2 = createProduct(HANDMADE, "002", 2000); Product product3 = createProduct(HANDMADE, "003", 3000); productRepository.saveAll(List.of(product1, product2, product3)); OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001","001")) .build(); // WHEN OrderResponse orderResponse = orderService.createOrder(request,n ); // THEN assertThat(orderResponse.getId()).isNotNull(); assertThat(orderResponse) .extracting("registeredDateTime","totalPrice") .containsExactlyInAnyOrder(n,2000); assertThat(orderResponse.getProducts()).hasSize(2) .extracting("productNumber", "price") .containsExactlyInAnyOrder( tuple("001", 1000), tuple("001", 1000) ); } private Product createProduct(ProductType type, String productNumber,int price) { return Product.builder() .productNumber(productNumber) .type(type) .sellingStatus(SELLING) .name("아메리카노") .price(price) .build(); } } Stock package sample.cafekiosk.domain.stock; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import sample.cafekiosk.domain.BaseEntity; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class Stock extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id; private String productNumber; private int quantity; @Builder public Stock(String productNumber, int quantity) { this.productNumber = productNumber; this.quantity = quantity; } public static Stock create(String s, int i) { return Stock.builder().productNumber(s).quantity(i).build(); } public boolean isQuantityLessThan(int quantity) { return this.quantity < quantity; } public void deductQuantity(int quantity) { if(this.quantity < quantity){ throw new IllegalArgumentException("주문 수량이 재고보다 많습니다."); } this.quantity-=quantity; } }

  • spring
  • tdd
  • jpa
  • mockito
  • 소프트웨어-테스트
  • junit5
1026baby 댓글 2 좋아요 1 조회수 656

안녕하세요! 왜 h2를 이용하면 중형 테스트로 보게 되는걸까요??

미해결

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트

중형 테스트는 멀티 스레드, 멀티 프로세스를 허용하는 테스트라고 이해했는데 그 설명을 하시면서 "멀티 프로세스와 멀티 스레드를 사용할 수 있는 완화조건이 생기죠. 이는 다시 말해 h2 같은 테스트용 DB를 사용할 수 있게 된다는 의미입니다." 라고 하셔서요! 혹시 h2를 사용하려면 멀티 스레드나 멀티 프로세스 환경에 있어야 한다고 이해해도 되는걸까요? "h2는 멀티 스레드, 멀티 프로세스 환경에 있어야 하기 때문에 h2를 사용하는 순간 중형 테스트로 보면 된다"로 이해하면 될까요??

  • spring
  • tdd
  • jpa
  • 소프트웨어-테스트
  • unittest
무자비한 낭만주먹 댓글 2 좋아요 1 조회수 764

안녕하세요! 로딩 상태 테스트에 관련된 질문입니다.

미해결

따라하며 배우는 리액트 테스트 [2023.11 업데이트]

제 나름대로 강의를 응용해서 테스트를 짜보려고 강의 시점과 다른 userEvent 버전을 쓰고 있기는 하는데요. 로딩 상태를 테스트할때 마치 로딩 상태를 건너뛰고 바로 리스폰스를 받은 듯이 작동하고 있습니다.... 혹시 userEvent 버전이 달라서 동작이 달라진걸까요? 강의와 코드 내용이 조금 달라진 것에 대한 질문이라 죄송합니다만 아무리 찾아도 이유를 모르곘네요..ㅠㅠㅠ import { screen, render } from '@testing-library/react'; import App from './App'; import userEvent from '@testing-library/user-event'; describe('MainPage', () => { it('should change page when click buttons', async () => { const user = userEvent.setup(); render(<App />); const americaInput = await screen.findByRole('spinbutton', { name: /america/i, }); await user.clear(americaInput); await user.type(americaInput, '2'); const englandInput = await screen.findByRole('spinbutton', { name: /england/i, }); await user.clear(englandInput); await user.type(englandInput, '1'); const dinnerCheckbox = await screen.findByText(/dinner/i); await user.click(dinnerCheckbox); const orderButton = screen.getByRole('button', { name: '주문하기' }); await user.click(orderButton); const summeryHeading = screen.getByRole('heading', { name: '주문 확인' }); expect(summeryHeading).toBeInTheDocument(); const productHeading = screen.getByRole('heading', { name: '여행상품: 3000', }); expect(productHeading).toBeInTheDocument(); const americaLi = screen.getByText('2 America'); const englandLi = screen.getByText('1 England'); expect(americaLi).toBeInTheDocument(); expect(englandLi).toBeInTheDocument(); const optionHeading = screen.getByRole('heading', { name: '옵션: 500' }); const dinnerLi = screen.getByText('Dinner'); expect(optionHeading).toBeInTheDocument(); expect(dinnerLi).toBeInTheDocument(); const confirmCheckbox = screen.getByRole('checkbox'); const submitButton = screen.getByRole('button'); await user.click(confirmCheckbox); await user.click(submitButton); const loading = screen.getByText(/loading/i); expect(loading).toBeInTheDocument(); const completeDiv = await screen.findByRole('heading', { name: '주문이 성공했습니다!', }); const subHeadingDiv = await screen.findByRole('heading', { name: '지금까지 모든 주문', }); const loadingAfterResponse = screen.queryByText(/loading/i); expect(completeDiv).toBeInTheDocument(); expect(subHeadingDiv).toBeInTheDocument(); expect(loadingAfterResponse).not.toBeInTheDocument(); const priceDiv = await screen.findByText('3500', { exact: false }); expect(priceDiv).toBeInTheDocument(); const backBtn = await screen.findByRole('button'); await user.click(backBtn); const newAmericaInput = await screen.findByRole('spinbutton', { name: /america/i, }); expect(newAmericaInput).toBeInTheDocument(); expect(newAmericaInput).toHaveValue(0); }); });

  • react
  • 웹앱
  • React-Context
  • jest
  • 소프트웨어-테스트
임유빈 댓글 1 좋아요 0 조회수 424

토이프로젝트 깃헙 링크 어디있나요?

미해결

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트

섹션 2 1부 실기 수업 - 어쨌든 테스트 코드 에서 사용하는 기존 프로젝트 위치가 혹시 어디 있나요?

  • spring
  • tdd
  • jpa
  • 소프트웨어-테스트
  • unittest
Jay 댓글 2 좋아요 1 조회수 1264

폐쇄망에서 package 추가

미해결

따라하며 배우는 리액트 A-Z[19버전 반영]

구글링을 해서 폐쇄망에 react 프로젝트를 어찌어찌 설치를 했습니다. 정상적으로 실행도 되고 프로그래밍도 가능합니다. 근데, 중간에 패키지를 추가하려고 인터넷이 가능한 곳에서 패키지를 추가해서 옮겨보려 했으나 잘 안되더군요. 폐쇄망에서 패키지 추가는 어떻게 해야 하는지 도움 부탁드립니다.

  • react
  • redux
  • tdd
  • typescript
  • next.js
  • 소프트웨어-테스트
  • 오프라인
  • 폐쇄망
metlin 댓글 1 좋아요 0 조회수 1166

문제로 남겨주신 uuid에 대한 코드는 없는건가요 ?

미해결

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트

UUID에 대해 제가 작성한 로직의 방향이 맞는지 확인해 보고싶은데, 문제관련 코드는 어디에 있나요 ?

  • spring
  • tdd
  • jpa
  • 소프트웨어-테스트
  • unittest
라부베리laveberry 댓글 2 좋아요 0 조회수 900

ControllerTestSupport 관련 질문입니다.

미해결

Practical Testing: 실용적인 테스트 가이드

안녕하세요 강사님 현재 회사에서 진행중인 프로젝트에서 ControllerTestSupport를 생성하여 controllerTest시 스프링부트를 띄우는 횟수를 줄이기 위해 사용하였는데 테스트 속도가 줄어들거나 하지 않고 거의 똑같더라구요,, 그래도 스프링부트를 띄우는걸 최소화 하는게 나을까요? ControllerTestSupport 사용해야하는 더 나은 이점이 있는지 궁금합니다!

  • spring
  • tdd
  • jpa
  • mockito
  • 소프트웨어-테스트
  • junit5
하하하 댓글 2 좋아요 0 조회수 406

Uuid의 의존성 주입과 의존성 방향에 대한 질문입니다.

해결됨

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트

안녕하세요! 강의 너무 잘듣고있습니다. 소스코드 내에서 UuidHolder 와 User 의 관련 의존성 방향에 대해 질문이 있어 글 남깁니다 public static User from(UserCreate userCreate, UuidHolder uuidHolder) { return User.builder() .email(userCreate.getEmail()) .nickname(userCreate.getNickname()) .address(userCreate.getAddress()) .status(UserStatus.PENDING) .certificationCode(uuidHolder.random()) .build(); } 위 코드에서 Parameter값을 통해 User 객체가 UuidHolder 라는 클래스에 의존하게 되는데요. UuidHolder 의 경우 common 패키지의 application layer에 존재하는 것으로 알고 있습니다. Domain layer의 User객체가 application layer의 객체에 의존하는 것이 parameter형태로는 허용이 되는지 질문드립니다.

  • spring
  • tdd
  • jpa
  • 소프트웨어-테스트
  • unittest
강병선 댓글 2 좋아요 0 조회수 507

영속성 객체에 아이디를 포함한 생성자

해결됨

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트

안녕하세요, 강의 잘보고 필요할떄마다 계속 보고 있습니다. 현재 강의에선 도메인 객체와 영속성 객체(Entity class)를 분리해서 사용하여서 문제가 없을 것 같은데, 보통 Entity class 에서 ID를 자동으로 생성하는 경우가 대부분이어서, 생성자를 생성 할 때, ID를 포함하지 않는 생성자를 많이 만드는 것으로 알고 있습니다. 이 때, 테스트코드는 어떻게 작성해야할까요? 소형 테스트를 작성한다면 H2 같읕 데이터베이스에서 객체를 가져오지 않는 한 아이디가 존재하지 않을텐데 아이디로 비교하지 말고 다른 값들만 비교해야할까요? 그렇다면 로직내에 findById() 같은 메서드로 객체를 불러와서 처리하는 경우가 있다면 어떻게 하시는지 궁금합니다. 객체의 아이디가 없으므로 로직내에서 아이디를 가져오거나 비교하는 경우가 있다면, NPE가 발생하여 테스트가 제대로 진행이 되지 않는 경우가 많을 것 같습니다. 다들 보통 어떻게 하시는지 궁금하여 질문 남깁니다 감사합니다

  • spring
  • tdd
  • jpa
  • 소프트웨어-테스트
  • unittest
연못지 댓글 2 좋아요 0 조회수 559

의존성조언에서 UserService의 login은 Clock에 의존하는지 모르지 않나요?

미해결

Java/Spring 주니어 개발자를 위한 오답노트

의존성 조언 두번째 방법에서 문제점이 UserService.login은 여전히 의존성이 감춰져있다. 위와 마찬가지로 Userservice.login을 테스트할 때 clock에 의존하고 있는지 알 수 없다. 이거였는데 세번 째 방법도 UserService를 사용하는 메서드는 login(user)만 남겨서 user가 Clock에 의존하는지 모르지 않나요? 아까 맥도날드 예제에서는 인터페이스를 사용하면 일을 시킨다고 설명하셨는데 이것도 인터페이스를 사용해서 일을 시키는거니 이렇게 구현하면 내부에서 clockHolder를 사용해도 외부에서 몰라도 되나요?

  • java
  • spring
  • 객체지향
  • 소프트웨어-테스트
seiyoung an 댓글 2 좋아요 0 조회수 475

상품등록 API 개발, JPA로 변환하기 에서

미해결

실전! 스프링부트 상품-주문 API 개발로 알아보는 TDD

org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "PRODUCTS" not found (this database is empty); SQL statement: select product0_.name as name1_0_0_, product0_.discount_policy as discount2_0_0_, product0_.id as id3_0_0_, product0_.price as price4_0_0_ from products product0_ where product0_.name=? [42104-214] 이런 에러가 나옵니다.. application.properties 에서 jpa 관련 설정해준건 없고 따라하기만 했을 뿐인데 저런 에러가 뜨네요.. 제가 알기론 jpa 의 ddl-auto 가 create 라서 저절로 생성해주는걸로 알고있는데 ,, 왜 저런 에러가 뜨는지 그리고 강사님이랑 같은 코드를 입력했는데 발생하는지 궁금하네요

  • tdd
  • spring-boot
  • pojo
  • 소프트웨어-테스트
  • api
The Beeeast 댓글 2 좋아요 0 조회수 904

jwt + OAuth2 문의

미해결

스프링부트 JUnit 테스트 - 시큐리티를 활용한 Bank 애플리케이션

안녕하세요 현재 jwt 코드 리뷰 까지 듣고 수업 코드를 참고해 oauth2 + jwt를 만들어보고 있는데요 응답한 jwt토큰이 요청 헤더에 포함되지 않는 문제 를 도무지 해결하지 못해서 질문드립니다 ㅠㅠ OAuth2 로그인-> loadUser에서 유저 정보를 받아 임시 세션에 저장 -> 핸들러에서 토큰 생성후 응답. 순서로 만들었습니다. OAuth2User 구현체에 유저 정보 세팅해 임시 세션에 저장시켰습니다. @Service public class CustomOauth2UserService extends DefaultOAuth2UserService { @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2User oAuth2User = super.loadUser(userRequest); UserEntity userEntity = new UserEntity(); userEntity.setProvider(userRequest.getClientRegistration().getClientName()); userEntity.setUsername(oAuth2User.getAttribute("name")); userEntity.setEmail(oAuth2User.getAttribute("email")); userEntity.setRole("ROLE_USER"); // oAuth2User 와 userEntity 로 OAuth2User 구현체 생성 return new DefaultOauth2User(userEntity, oAuth2User.getAttributes()); } } 핸들러로 넘어와 토큰을 생성하고 응답 헤더에 추가시켰습니다. JwtProcess나 JwtVO는 선생님 코드랑 똑같이 만들었습니다. @Component public class Oauth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { DefaultOauth2User oAuth2User = (DefaultOauth2User) authentication.getPrincipal(); String jwtToken = JwtProcess.create(oAuth2User); response.addHeader(JwtVO.HEADER, jwtToken); getRedirectStrategy().sendRedirect(request, response, "/"); } } 시큐리티 설정파일입니다. cors설정인 configurationSource도 선생님과 같습니다. @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final CustomOauth2UserService userService; private final Oauth2SuccessHandler successHandler; @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ http.headers().frameOptions().disable(); http.csrf().disable(); http.httpBasic().disable(); http.cors().configurationSource(configurationSource()); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.authorizeRequests() .antMatchers("/user/**").authenticated() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().permitAll() .and() .oauth2Login() .loginPage("/loginForm") .successHandler(successHandler) .userInfoEndpoint().userService(userService); return http.build(); } 구글 로그인 시 /로 리다이렉트는 정상적으로 됩니다. 그런데 크롬 개발자 도구를 보면 response 헤더에는 jwt토큰이 있는데 요청 헤더에는 jwt토큰이 없습니다... 프론트 쪽은 html 파일들만 생성했는데 혹시 자바스크립트로 뭔가 해줘야 하나요? 검색 결과들 참고해서 만들었는데 혹시 oauth2와 jwt를 합치는 방향 자체가 틀렸을까요? 아니면 구글에서 받은 access토큰이 jwt인가요?

  • spring-boot
  • junit
  • 소프트웨어-테스트
pansakr 댓글 1 좋아요 0 조회수 505

@Valid 어노테이션에 대한 테스트 질문입니다.

미해결

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트

안녕하세요. 강의에 매우 깊은 감명을 받고 토이 프로젝트에 말씀해주신 부분들 적용해보고 있습니다. 강의에서 가이드 주신대로 유효성 검증에 대한 테스트도 mock 라이브러리 없이 순수 자바 코드로 해보고 있습니다. 기존 코드에서는 입력값 유효성 검증을 위해 @Valid 어노테이션을 사용했는데요. 스프링부트를 실행시키지 않으니 @Valid 어노테이션이 정상작동을 안해서 테스트를 작성하는데 어려움을 겪고 있습니다....ㅎㅎ 그래서 유효성 검증을 하는 코드를 직접 작성해야 하나 고민하다가 질문을 남기게 되었습니다. 강사님께서는 유효성 검증에 대한 테스트 코드를 작성하는데 어떤 방법을 사용하시는지 궁금합니다. (@Valid는 자바 표준 스펙으로 알고 있어서 스프링을 실행시키지 않아도 방법을 잘 찾아보면 가능할 것 같다 라고도 생각하고 있습니다.)

  • spring
  • tdd
  • jpa
  • 소프트웨어-테스트
  • unittest
베지밀 댓글 2 좋아요 0 조회수 686

컨트롤러 테스트의 Mock을 통한 Stubbing 관련 질문

해결됨

Practical Testing: 실용적인 테스트 가이드

안녕하세요. 우빈님! 먼저 우빈님의 테스트 강의가 너무나 재미있어, 테스트에 대한 막연한 지식을 구체화하고 테스트 작성 열망을 크게 키울 수 있어 서 감사하다는 말씀을 드리고 싶습니다! 다름이 아니라, Controller 테스트를 위해 @WebMvcTest 를 통해 Service와 Repository를 Mocking하여 단위 테스트의 형식으로 작성 한다는 것을 배웠습니다. 이렇게 배운 것을 사이드 프로젝트에 적용해보며, 의문점이 생겼는데 능력 부족으로 인해 의문이 해결되지 않아 질문을 드리려 합니다. 상황 강의에서 작성한 컨트롤러 테스트 중 일부인 OrderControllerTest의 테스트 메서드는 다음과 같습니다. @DisplayName("신규 주문을 등록한다.") @Test void createOrder() throws Exception { // given OrderCreateRequest request = OrderCreateRequest.builder() .productNumbers(List.of("001")) .build(); // when & then mockMvc.perform( post("/api/v1/orders/new") .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON) ) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value("200")) .andExpect(jsonPath("$.status").value("OK")) .andExpect(jsonPath("$.message").value("OK")); } 또한, 해당 메서드의 실행 로그를 보면 MockHttpServletResponse 이 다음과 같다는 것을 볼 수 있었습니다. MockHttpServletResponse: Status = 200 Error message = null Headers = [Content-Type:"application/json"] Content type = application/json Body = {"code":200,"status":"OK","message":"OK","data":null} Forwarded URL = null Redirected URL = null Cookies = [] 이후, 저의 사이드 프로젝트의 컨트롤러 테스트를 위와 동일한 방식으로 작성하였지만, 아래와 같이 MockHttpServletResponse의 Body가 빈 채로 응답 이 되어 테스트가 실패하게 되는 문제가 발생했습니다. MockHttpServletResponse: Status = 200 Error message = null Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"] Content type = null Body = Forwarded URL = null Redirected URL = null Cookies = [] 이때, given()을 통해 Service에 대한 행위를 Stubbing해주니 정상적으로 테스트가 성공 하는 것을 확인했습니다. 문제 파악을 위해 조사한 결과 추측되는 차이점은 다음과 같습니다. 강사님의 프로젝트에서 Controller의 응답 타입은 커스텀 응답 객체인 ApiResponse 를 사용합니다. 저의 사이드 프로젝트에서 Controller의 응답 타입은 HttpEntity를 상속하는 ResponseEntity 를 사용합니다. 질문 Q1. 강사님의 코드를 보면, 아래와 같이 createOrder 메서드에 대한 Stubbing 없이도 정상적으로MockHttpServletResponse의 Body가 응답되어 테스트가 성공합니다. // given ... given(orderService.createOrder(any())) .willReturn(OrderResponse.builder() ... // 생략 .build() ); // 없어도 테스트는 성공한다. 저는 given() 절에 @MockBean을 통해 Mock 객체로 설정한 OrderService가 어떤 행위를 해야할지 Stubbing 해주어야 하는 것으로 이해하고 있었는데, 어떻게 Stubbing 없이 Body가 정상적으로 채워져 테스트가 성공한 것인지 궁금 합니다. Q2. Q1과 연관하여 강사님의 코드에서는 ApiResponse 라는 커스텀한 응답 객체를 컨트롤러 메서드의 응답으로 사용하는데, 제 사이드 프로젝트에서의 응답 타입은 ResponseEntity 를 사용하고 있습니다. 이 차이 때문에 발생하는 문제인지 궁금합니다. Q3. 이번 의문점을 통해 컨트롤러 테스트에서 메서드의 행위에 대한 기댓값을 Stubbing하여 검증하는 것이 일종의 답정너(?)와 같은 테스트를 작성하는 것은 아닐까? 라는 생각과 함께, 컨트롤러 테스트 방식에 많은 고민을 해야 하겠다는 다짐을 하게 되었습니다. 이에 대한 우빈님의 생각은 어떠하신지 궁금합니다. 질문이 수준이 다소 떨어지지만, 이 의문점을 해결하고 싶은 마음에 장황하게 나열할 수 밖에 없었음을 양해 부탁드립니다. 답변 기다리겠습니다. 감사합니다!

  • spring
  • tdd
  • jpa
  • mockito
  • 소프트웨어-테스트
  • junit5
  • @webmvctest
  • stubbing
  • @mockbean
lango 댓글 1 좋아요 0 조회수 558

테스트 데이터 셋업

해결됨

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트

안녕하세요 강의 잘 보고 있습니다. 다름이 아니라 이렇게 H2등을 쓰지않고 테스트 할 경우, 많은 테스트 데이터들을 셋업하는 것이 힘들것 같은데 보통 어떻게 처리하시나요? h2등을 사용한다고 하면 강의에서 알려주신 것 처럼 보통 .sql을 실행시켜서 사용할텐데, 이렇게 소형테스트들에 테스트데이터를 java code로 반복문 돌면서 생성하자니 좀 번거러운 것 같은데 혹시 다른 방법이 있을런지 혹은 어떻게 하시는지 궁금하여 여쭤봅니다. 환절기가 다가오는데 더위 조심, 감기 조심 하시길 바랍니다. 감사합니다.

  • spring
  • jpa
  • 소프트웨어-테스트
  • unittest
연못지 댓글 2 좋아요 0 조회수 690

@NotNull 관련 질문 드립니다.

미해결

Practical Testing: 실용적인 테스트 가이드

안녕하세요. 먼저, 강의 잘 듣고 있습니다. 강의 따라하던 중 아래 이슈가 발생해서 문의 드립니다. Q1. @NotNull 지정 시, Not-null fields must be initialized 라고 경고창이 뜨며.. Q2. 테스트를 진행하면 아래와 같이 메세지 부분에 기대했던 값 외에.. 추가 적인 스트링이 들어가서 테스트 성공이 되지 않는 것 같습니다. Body = {"code":400,"status":"BAD_REQUEST","message":"Field error in object 'productCreateRequest' on field 'type': rejected value [null]; codes [NotNull.productCreateRequest.type,NotNull.type,NotNull.sample.cafekiosk.spring.domain.product.ProductType,NotNull]; arguments [ org.springframework.context.support .DefaultMessageSourceResolvable: codes [productCreateRequest.type,type]; arguments []; default message [type]]; default message [상품 타입은 필수입니다.]","data":null} 그 외에 @NotBlank, @Postivie 도 유사한 에러가 발생하고 있습니다. Expected :상품 가격은 양수여야 합니다. Actual :Field error in object 'productCreateRequest' on field 'price': rejected value [0]; codes [Positive.productCreateRequest.price,Positive.price, Positive.int ,Positive]; arguments [ org.springframework.context.support .DefaultMessageSourceResolvable: codes ... Q2 의 경우 하여.. 우선은 아래와 같이 문자열을 포함하는지로 수정해서 테스트는 넘어갔습니다. .andExpect(jsonPath("$.message", "상품 가격은 양수여야 합니다.").exists()) Q1 내용은 어떻게 수정하면 된느지 궁금합니다. 확인 부탁드립니다~

  • spring
  • tdd
  • jpa
  • mockito
  • 소프트웨어-테스트
  • junit5
illhumored 댓글 2 좋아요 0 조회수 793

소형 테스트 실행 시 빌드 타임은 어떻게 극복하셨는지요

해결됨

Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트

강의를 굉장히 감명 깊게 보고 Spring 기반으로 되어 있는 레거시 프로젝트에 도메인을 분리하고 유닛 테스트를 넣는 작업을 하고 있습니다. 여기에서 가장 답답한 게 단순한 유닛 테스트임에도 빌드 타임을 그대로 기다려야 한다는 점인데요. 비록 빌드 캐시 덕에 아주 오래 걸리는 건 아니긴 해도 몇십 ms면 끝날 테스트를 몇 초씩 기다려야 하는 점이 많이 답답합니다. 시간 상으로는 인메모리 db를 띄우는 것과 별 차이 없는 것 아닌가? 하는 생각이 들 정도예요. (물론 테스트 코드를 작성하기 쉬운 구조가 좋은 코드 구조라는 명제에는 이견이 없기 때문에 별 차이가 없다 하더라도 계속 진행할 예정입니다.) 선생님께서도 담당하시던 시스템에 유닛 테스트를 붙이는 작업을 하셨다고 들었는데 이러한 부분은 어떻게 극복하셨는지 궁금합니다.

  • spring
  • tdd
  • jpa
  • 소프트웨어-테스트
  • unittest
auguster 99 댓글 2 좋아요 1 조회수 835

JPQL 동적쿼리 부분 - > QUERYDSL 마이그레이션 후 질문

미해결

스프링부트 JUnit 테스트 - 시큐리티를 활용한 Bank 애플리케이션

querydsl설정은 각종 블로그 보고 설정 적용을 한상태입니다. 일단 엔티티 구조가 Transaction 엔티티에서 Accout 엔티티를 @MantToOne 다대일 단방향 매핑중이고, 입출금 내역 조회부분에서 jpql 부분을 그대로 챗 gpt 에 querydsl 기준 동적쿼리 코드를 돌려보면 이런 코드를 알려줍니다 우선 여기서 질문이 WithdrawAccount, DepositAccount라는 클래스는 없고 Account를 참조 하는데 QWithdrawAccount, QDepositAccount Q클래스가 필요한 이유를 모르겠습니다. 저 코드를 해당 코드와 같이 바꿨습니다 그리고 테스트 코드를 돌려보면 transaction.withdrawAccount is not a root path 라는 에러가 나는데 root path 관련 질문글 찾아보면 " 이를 해결하려면 from 절에서 해당 객체를 다시 선언해주면 됩니다. 즉, querydsl에서는 join을 할 때 alias를 줘서 해당 객체를 다시 선언해주는 방식으로 작성해야 해결할 수 있습니다. " 이런 답변이있는데 변경 된 코드에서 innerjoin의 오른쪽 파라미터에 별칭을 잘못 줘서 에러가 나는거 같긴 한데 QTransaction.transaction.withdrawaccount 와 같은 별칭을 부여하면 안되는 걸까요,,?

  • spring-boot
  • junit
  • 소프트웨어-테스트
ykmykm4608 댓글 2 좋아요 0 조회수 545

username으로 select를 왜하는지 잘모르겠습니다...

미해결

스프링부트 JUnit 테스트 - 시큐리티를 활용한 Bank 애플리케이션

하이버네이트가 로그인한 user의 username으로 조회하는 쿼리를 발생시키는데 어떤 이유로 이게 발생되는지 잘모르겠습니다,,,!!! 컨트롤러 들어가기 전부터 조회가 되던데 어떠한 이유로 쿼리가 발생하는지 너무 궁금해서 질문글을 작성하게 되었습니다 ㅠㅠㅠ!!!

  • spring-boot
  • junit
  • 소프트웨어-테스트
ten 댓글 2 좋아요 0 조회수 316

인기 태그

인프런 TOP Writers

주간 인기글