概要
SpringBootでPaginationの機能を実装します。
テストコードを書いてからプロダクトコードを書いています。
環境
OS:Windows10
IDE: IntelliJ Community
spring-boot-starter-parent 2.75
java : 11
テスト系: Junit5 Mockito
図
ユースケース
ドメインはECサイトで 顧客が今までの注文内容を表示するAPIを作成します。
ページナンバー、ページサイズを指定することによって、一度に得られる過去の注文数を制限します。
クラス図
実装部分
Repository Layer
テストコード
package com.example.restapi.order;
import com.example.restapi.domain.order.Order;
import com.example.restapi.domain.order.OrderRepository;
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.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
/**
*
* @brief: Order Repository Unit Test class
*
* @description this class is Unit test for {@link OrderRepository}.
* Format is base on BDD style(given-when-then).
* Database is h2 because of testing in memory.
* @Auther RYA234
*
*/
@DataJpaTest
public class OrderRepositoryTest {
@Autowired
private OrderRepository orderRepository;
@Test
@DisplayName("Orderを引数とし、Saveを実行したとき、Orderが返される。")
public void givenOrder_whenSave_thenReturnOrder() {
// given-precondition or Setup
Order newOrder = new Order(99, toDate(LocalDateTime.now()),6000f,100f,6100f,610f,6710f,"pending");
//when - action or the behavior that we are going test
Order savedOrder = orderRepository.save(newOrder);
//then - verify the output
assertThat(savedOrder.getId()).isGreaterThan(0);
}
@Test
@DisplayName("cutomerIdとPageableを引数とし、findByCustomerIdを実行したとき、ページネーション化されたOrdersが返される。")
public void givenCustomerIdandPageable_whenFindByCustomerId_thenReturnPageOrder() {
// given-precondition or Setup
Integer customerId = 0;
orderRepository.save(new Order(3, toDate(LocalDateTime.now()),6000f,100f,6100f,610f,6710f,"pending"));
orderRepository.save(new Order(0, toDate(LocalDateTime.now()),6000f,100f,6100f,610f,6710f,"pending"));
orderRepository.save(new Order(0, toDate(LocalDateTime.now()),6000f,100f,6100f,610f,6710f,"pending"));
orderRepository.save(new Order(0, toDate(LocalDateTime.now()),6000f,100f,6100f,610f,6710f,"pending"));
orderRepository.save(new Order(3, toDate(LocalDateTime.now()),6000f,100f,6100f,610f,6710f,"pending"));
orderRepository.save(new Order(3, toDate(LocalDateTime.now()),6000f,100f,6100f,610f,6710f,"pending"));
// make pageable
int pageNo = 0;
int pageSize =2;
Pageable pageable = PageRequest.of(pageNo,pageSize);
//when - action or the behavior that we are going test
Page<Order> actualOrder = orderRepository.findByCustomerId(customerId,pageable);
//then - verify the output
int expectedTotal =3;
int expectedContentSize = 2;
assertThat(actualOrder.getTotalElements()).isEqualTo(expectedTotal);
assertThat(actualOrder.getContent().size()).isEqualTo(expectedContentSize);
}
private Date toDate(LocalDateTime localDateTime) {
ZoneId zone = ZoneId.systemDefault();
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, zone);
Instant instant = zonedDateTime.toInstant();
return Date.from(instant);
}
}
プロダクトコード
package com.example.restapi.domain.order;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.*;
import java.util.Date;
@Entity
@Getter
@Setter
@ToString
@RequiredArgsConstructor
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
// @Column(name = "id", nullable = false)
private Integer id;
// 注文した顧客のID
@Column(name = "customer_id")
private Integer customerId;
// 注文日 YYYY:MM:DD
@Column(name = "order_time")
private Date orderTime;
// 商品の値段/円
@Column(name = "product_cost")
private float productCost;
// 配送費 購入金額によって変化する。
@Column(name = "shipping_cost")
private float shippingCost;
// 税別
@Column(name = "subtotal")
private float subtotal;
// 消費税の和 8%と10%に分ける
@Column(name = "tax")
private float tax;
// お客様払う総額(税+税別)
@Column(name = "total")
private float total;
// 注文の状態、
@Column(name = "status")
private String status;
public Order(Integer customerId, Date orderTime, float productCost, float shippingCost, float subtotal, float tax, float total, String status) {
this.customerId = customerId;
this.orderTime = orderTime;
this.productCost = productCost;
this.shippingCost = shippingCost;
this.subtotal = subtotal;
this.tax = tax;
this.total = total;
this.status = status;
}
public Order(Integer id, Integer customerId, Date orderTime, float productCost, float shippingCost, float subtotal, float tax, float total, String status) {
this.id = id;
this.customerId = customerId;
this.orderTime = orderTime;
this.productCost = productCost;
this.shippingCost = shippingCost;
this.subtotal = subtotal;
this.tax = tax;
this.total = total;
this.status = status;
}
}
package com.example.restapi.domain.order;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
/**
*
* @brief: Order Repository Class
*
* @description 本番ではMYSQL、単体テストではH2データベースを使う想定です。
*
*
* @Auther RYA234
*
*/
@Repository
public interface OrderRepository extends PagingAndSortingRepository<Order,Integer> {
Page<Order> findByCustomerId(Integer customerId, Pageable pageable);
Order findOrderById(Integer id);
}
Service Layer
テストコード
モックデータを作る。
Repositoryのテストをデバッグモードで実行し、orderRepository.findByCustomerId(customerId,pageable)の値を確認します。
デバッグ結果を参考にServiceのテストのメソッド内にモックデータを作成します。
RepositoryでのactualOrderの値
コード部分
package com.example.restapi.order;
import com.example.restapi.domain.order.Order;
import com.example.restapi.domain.order.OrderRepository;
import com.example.restapi.domain.order.OrderResponse;
import com.example.restapi.implement.order.OrderServiceImplement;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
/**
*
* @brief: Order Service Unit Test class
*
* @description this class is Unit test for {@link OrderServiceImplement}.
* Format is base on BDD style(given-when-then).
*
* @Auther RYA234
*
*/
@ExtendWith(MockitoExtension.class)
@ExtendWith(SpringExtension.class)
public class OrderServiceImplementTest {
@MockBean
private OrderRepository orderRepository;
@InjectMocks
private OrderServiceImplement orderServiceImplement;
@Test
@DisplayName("customerIdとPageableIdを引数とし、listByPageByCustomerを実行したとき、OrderResponseが返り値となる。")
public void givenCustomerIdandPageable_whenListByPageByCustomer_thenReturnOrderResponse() {
// given-precondition or Setup
Integer customerId = 0;
int pageNo =0;
int pageSize = 2;
Pageable pageable = PageRequest.of(pageNo,pageSize);
int total = 3;
List<Order> content = new ArrayList<>();
content.add(new Order(2,0, toDate(LocalDateTime.now()),6000f,100f,6100f,610f,6710f,"pending"));
content.add(new Order(3,0, toDate(LocalDateTime.now()),6000f,100f,6100f,610f,6710f,"pending"));
Mockito.doReturn(mockPageOrder(pageable,total,content)).when(orderRepository).findByCustomerId(customerId,pageable);
//when - action or the behavior that we are going test
OrderResponse actualOrderResponse = orderServiceImplement.listByPageByCustomer(customerId,pageNo,pageSize);
//then - verify the output
OrderResponse expectedOrderResopnse = new OrderResponse(0,2,3,2,false);
assertThat(actualOrderResponse.getPageNo()).isEqualTo(expectedOrderResopnse.getPageNo());
assertThat(actualOrderResponse.getTotalPages()).isEqualTo(expectedOrderResopnse.getTotalPages());
assertThat(actualOrderResponse.getTotalElements()).isEqualTo(expectedOrderResopnse.getTotalElements());
assertThat(actualOrderResponse.getPageSize()).isEqualTo(expectedOrderResopnse.getPageSize());
}
private Page<Order> mockPageOrder(Pageable pageable, int total, List<Order> content){
final int start = (int)pageable.getOffset();
final int end = Math.min((start + pageable.getPageSize()), content.size());
return new PageImpl<>(content.subList(start,end),pageable,total);
}
private Date toDate(LocalDateTime localDateTime) {
ZoneId zone = ZoneId.systemDefault();
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, zone);
Instant instant = zonedDateTime.toInstant();
return Date.from(instant);
}
}
Paginationの部分を確認したいので、content部分はテスト項目にしていません。
プロダクトコード
package com.example.restapi.domain.order;
import com.example.restapi.implement.cartItem.CartItemDto;
import org.springframework.stereotype.Service;
import java.util.List;
/**
*
* @brief: Order Service(domain)
*
* @description OrderのServiceのdomainです。ユースケース的には顧客が主語になります。
*
*
* @Auther RYA234
*
* @See {@link com.example.restapi.implement.order.OrderServiceImplement}
*
* */
@Service
public interface OrderService {
// 顧客が注文を作成するサービス。ショッピングカート内に入っている商品から注文する内容を決める。
void create(Integer customerId, List<CartItemDto> cartItemDtos, PaymentMethod paymentMethod);
// 顧客が今までの注文全てを表示する。
OrderResponse listByPageByCustomer(Integer customerId, int pageNo,int pageSize);
// 顧客の注文を一見表示する。
Order get(Integer orderId);
}
package com.example.restapi.implement.order;
import com.example.restapi.domain.order.*;
import com.example.restapi.implement.cartItem.CartItemDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
*
* @brief: Order service implement class
*
* @description Software architecture is based on Controller-"Service"-Repository Pattern
* This class is "Service" and charge of business logic in Order Domain.
* @Auther RYA234
*
* @Entity: {@link Order}
* @UseCase: {@link OrderService}
*/
@Service
public class OrderServiceImplement implements OrderService {
@Autowired
OrderRepository orderRepository;
@Override
public void create(Integer customerId, List<CartItemDto> cartItemDtos, PaymentMethod paymentMethod) {
}
@Override
public OrderResponse listByPageByCustomer(Integer customerId, int pageNo,int pageSize) {
Pageable pageable = PageRequest.of(pageNo,pageSize);
Page<Order> orders = orderRepository.findByCustomerId(customerId, pageable);
List<Order> orderList = orders.getContent();
List<OrderDto> content = orderList.stream().map(order -> mapToDTO(order)).collect(Collectors.toList());
return new OrderResponse(content,pageNo,pageSize,orders.getTotalElements(),orders.getTotalPages(),orders.isLast());
}
@Override
public com.example.restapi.domain.order.Order get(Integer orderId) {
return orderRepository.findOrderById(orderId);
}
private Order mapToEntity(OrderDto orderDto){
Order order = new Order();
order.setId(orderDto.getId());
order.setCustomerId(orderDto.getCustomerId());
order.setOrderTime(orderDto.getOrderTime());
order.setProductCost(orderDto.getProductCost());
order.setShippingCost(orderDto.getShippingCost());
order.setSubtotal(orderDto.getSubtotal());
order.setTax(orderDto.getTax());
order.setTotal(orderDto.getTotal());
order.setStatus(orderDto.getStatus());
return order;
}
private OrderDto mapToDTO(Order order){
OrderDto orderDto = new OrderDto();
orderDto.setId(order.getId());
orderDto.setCustomerId(order.getCustomerId());
orderDto.setOrderTime(order.getOrderTime());
orderDto.setProductCost(order.getProductCost());
orderDto.setShippingCost(order.getShippingCost());
orderDto.setSubtotal(order.getSubtotal());
orderDto.setTax(order.getTax());
orderDto.setTotal(order.getTotal());
orderDto.setStatus(order.getStatus());
return orderDto;
}
}
package com.example.restapi.domain.order;
import com.example.restapi.implement.order.OrderDto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
*
* @brief: Order GET Responese
*
* @description 顧客がいままで注文した履歴をすべて取得するときに使われるレスポンスボディです。
* Page<Order>から変換される形でService層で使われます。
* Page<Order>から変換する理由としては、restapi側からするとPage<Order>がわかりにくいためです。
*
*
* @Auther RYA234
*
* @See: {@link OrderRestController}
*/
@Data
@NoArgsConstructor
public class OrderResponse {
private List<OrderDto> content;
private int pageNo;
private int pageSize;
private long totalElements;
private int totalPages;
private boolean last;
public OrderResponse(int pageNo, int pageSize, long totalElements, int totalPages, boolean last) {
this.pageNo = pageNo;
this.pageSize = pageSize;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.last = last;
}
public OrderResponse(List<OrderDto> content, int pageNo, int pageSize, long totalElements, int totalPages, boolean last) {
this.content = content;
this.pageNo = pageNo;
this.pageSize = pageSize;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.last = last;
}
}
package com.example.restapi.implement.order;
import lombok.Data;
import java.util.Date;
/**
* @brief: This Class is Data To Object in Order.
*
* @description when Service method is done,Order Class convert to OrderDto Class.
* OrderDto can do change OrderClass not changing OrderEntity and write test code easily.
*
* @Auther RYA234
*
* @Entity: {@link com.example.restapi.domain.order.Order}
* @UseCase: {@link com.example.restapi.domain.order.OrderService}
*/
@Data
public class OrderDto {
private Integer id;
private Integer customerId;
private Date orderTime;
private float productCost;
private float shippingCost;
private float subtotal;
private float tax;
private float total;
private String status;
}
参考
テストコード
https://www.udemy.com/course/testing-spring-boot-application-with-junit-and-mockito/
プロダクトコード
セクション7
https://www.udemy.com/course/building-real-time-rest-apis-with-spring-boot/