0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SpringBoot Pagenationをテストコード書いてからプロダクトコードを実装する

Last updated at Posted at 2022-11-30

概要

SpringBootでPaginationの機能を実装します。
テストコードを書いてからプロダクトコードを書いています。

環境

OS:Windows10
IDE: IntelliJ Community
spring-boot-starter-parent 2.75
java : 11
テスト系: Junit5 Mockito

ユースケース

ドメインはECサイトで 顧客が今までの注文内容を表示するAPIを作成します。
ページナンバー、ページサイズを指定することによって、一度に得られる過去の注文数を制限します。

クラス図

image.png

実装部分

Repository Layer

テストコード

OrderRepositoryTest.java
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);
    }
}


プロダクトコード

Order.java
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;
    }


}


OrderRepository.java
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の値
image.png

Serviceで作成したモックの値
image.png

コード部分

OrderServiceImplementTest.java
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部分はテスト項目にしていません。

プロダクトコード

OrderService.java
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);

}

OrderServiceImplement.java
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;
    }

}


OrderResponse.java
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;
    }
}


OrderDto.java
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/

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?