SpringBoot でテスト駆動開発　ドメインはECショップで注文情報を作成する部分

# 概要
SpringBootのウェブアプリーケーションでテスト駆動を行います。
アーキテクチャはRestController-Service-Repositoryであり、本記事ではServiceレイヤーのテストコードを書きます。
Repositoryレイヤーの確認は行われているものとして進めます。
Cntrollerの実装はしません。


# 開発環境
IDE: IntelliJ Community
spring-boot-starter-parent 2.75
java　：　11
テスト系: Junit5 Mockito
# 図とか

# ユースケース
ショッピングカートと顧客IDから注文情報を作成する機能を実装します。
顧客が支払う金額を確認するために、総額、税抜き価格、配送料、消費税といった情報を表示します。
税抜き価格で4000円以上だと送料が無料になります。
3999円以下だと送料は300円掛かります。
あくまでも金額情報を表示するだけで、DBの書き込みは行いません。

# クラス図
本記事的には赤丸で囲まれているクラスを実装します。
テストコード的にはRepositoryレイヤーのモックデータを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/0f7244e9-1362-08f9-5975-df3f8ce3b5be.png)

# 実装
テストコード→プロダクトコードの順で実装します。

## テストコード
最初に書きます。

```OrderServiceImplement.java
package com.example.restapi.order;

import com.example.restapi.domain.order.*;
import com.example.restapi.domain.product.Product;
import com.example.restapi.domain.product.ProductRepository;
import com.example.restapi.implement.cartItem.CartItemDto;
import com.example.restapi.implement.order.OrderDto;
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;

    @MockBean
    private OrderDetailRepository orderDetailRepository;

    @MockBean
    private ProductRepository productRepository;
    @InjectMocks
    private OrderServiceImplement orderServiceImplement;


    @Test
    @DisplayName("ショッピングカートとCustomerを引数とし、Createを実行したとき、注文情報が作成される。")
    public void givenCartItemDtoAndCustomerId_whenCreate_newOrder() {
        // given-precondition or Setup

        //  GET METHODで使われる想定
        List<CartItemDto> cartItemDtos = new ArrayList<>();
        cartItemDtos.add(new CartItemDto(1,3,3,5));
        cartItemDtos.add(new CartItemDto(1,3,7,1));
        cartItemDtos.add(new CartItemDto(1,3,10,9));
        cartItemDtos.add(new CartItemDto(1,3,13,9));
        cartItemDtos.add(new CartItemDto(1,3,15,3));

        // mock
        Mockito.doReturn(new Product(3,"Salmon","This is a Salmon",true, 1,200,0.01f,0,"maguro_image")).when(productRepository).getProductById(3);
        Mockito.doReturn(new Product(7,"Chicken","This is a chicken",true, 2,800,0.01f,0,"Chicken_image")).when(productRepository).getProductById(7);
        Mockito.doReturn(new Product(10,"Carrot","This is a Carrot",true, 3,100,0.01f,0,"Carrot_image")).when(productRepository).getProductById(10);
        Mockito.doReturn(new Product(13,"Bean","This is a Bean",true, 3,100,0.01f,0,"Bean_image")).when(productRepository).getProductById(13);
        Mockito.doReturn(new Product(15,"Nuts","This is a Nuts",true, 3,100,0.01f,0,"Nuts_image")).when(productRepository).getProductById(15);

        float expectedProductCost = 3900f;
        float expectedShippingCost = 300f;
        float expectedSubtotal = 4200f;
        float expectedTax = 390f;
        float expectedTotal = 4590f;
        PaymentMethod paymentMethod = PaymentMethod.CASH;


        //Mockito.doReturn().when(orderRepository).save()

        //when - action or the behavior that we are going test
        OrderDto orderDto = orderServiceImplement.create(3,cartItemDtos,paymentMethod);

        //then - verify the output
        assertThat(orderDto.getProductCost()).isEqualTo(expectedProductCost);
        assertThat(orderDto.getShippingCost()).isEqualTo(expectedShippingCost);
        assertThat(orderDto.getSubtotal()).isEqualTo(expectedSubtotal);
        assertThat(orderDto.getTax()).isEqualTo(expectedTax);
        assertThat(orderDto.getTotal()).isEqualTo(expectedTotal);

    }

    private Date toDate(LocalDateTime localDateTime) {
        ZoneId zone = ZoneId.systemDefault();
        ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, zone);
        Instant instant = zonedDateTime.toInstant();
        return Date.from(instant);
    }
}

```



## 商品の合算値の実装　productCost
単純にショッピングカートの商品の合算値です。
### プロダクトコード
``` OrderServiceImplement.java
package com.example.restapi.implement.order;

import com.example.restapi.domain.order.*;
import com.example.restapi.domain.product.ProductRepository;
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;


    @Autowired
    ProductRepository productRepository;



    @Override
    public OrderDto create(Integer customerId, List<CartItemDto> cartItemDtos, PaymentMethod paymentMethod) {
        OrderDto orderDto = new OrderDto();
        // cal productCost
       for(var cartItem :cartItemDtos){
           float miniSum = cartItem.getQuantity()*productRepository.getProductById(cartItem.getProductId()).getPrice();
           orderDto.setProductCost(orderDto.getProductCost() + miniSum);
       }
        // cal shipping

        // cal subtotal

        // cal tax

        // cal total;

        return orderDto;
    }

    @Override
    public void save(Order order) {

    }

    @Override
    public OrderDto get(Integer orderId) {
        OrderDto orderDto = mapToDTO(orderRepository.findOrderById(orderId));
        return orderDto;
    }

    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;
    }

}


```

githubの差分

https://github.com/RYA234/springboot-ecshop-rest-api/commit/06266b93cff77ea68152b61f294bec68ec9e1669

テストの結果
productCostはOkになります。shippingCostで赤になります。(スクショ取ってなかったです。)


## 配送料の実装　shippingCost
### プロダクトコード
``` OrderServiceImplement.java

/**
 *
 * @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;

    @Autowired
    ProductRepository productRepository;

    @Override
    public OrderDto create(Integer customerId, List<CartItemDto> cartItemDtos, PaymentMethod paymentMethod) {
        OrderDto orderDto = new OrderDto();
        // cal productCost
       for(var cartItem :cartItemDtos){
           float miniSum = cartItem.getQuantity()*productRepository.getProductById(cartItem.getProductId()).getPrice();
           orderDto.setProductCost(orderDto.getProductCost() + miniSum);
       }

        **// cal shipping 
        if( orderDto.getProductCost() <= 4000f){
            orderDto.setShippingCost(300);
        }else{
            orderDto.setShippingCost(0);
        }

        // cal subtotal

        // cal tax

        // cal total;

        return orderDto;
    }



}


```


### githubの差分


https://github.com/RYA234/springboot-ecshop-rest-api/commit/4c976f2a7c6e2f4a93d3e882885be1f688366f18


### テストの結果
shippingCostはOKになります。subTotalで赤になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/a018e076-4ea5-15c0-bf91-6d53b0fc2b7b.png)

## 商品と運送料の合算値の実装　subTotal
### プロダクトコード
``` OrderServiceImplement.java
/**
 *
 * @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;


    @Autowired
    ProductRepository productRepository;



    @Override
    public OrderDto create(Integer customerId, List<CartItemDto> cartItemDtos, PaymentMethod paymentMethod) {
        OrderDto orderDto = new OrderDto();
        // cal productCost
       for(var cartItem :cartItemDtos){
           float miniSum = cartItem.getQuantity()*productRepository.getProductById(cartItem.getProductId()).getPrice();
           orderDto.setProductCost(orderDto.getProductCost() + miniSum);
       }
        // cal shipping
        if( orderDto.getProductCost() <= 4000f){
            orderDto.setShippingCost(300);
        }else{
            orderDto.setShippingCost(0);
        }

        **// cal subtotal
        orderDto.setSubtotal(orderDto.getProductCost() + orderDto.getShippingCost());
       // cal tax

        // cal total;

        return orderDto;
    }



}


```

### githubの差分

https://github.com/RYA234/springboot-ecshop-rest-api/commit/89e5469c48af502c96b53159f5b201d96364b2b8


### テストの結果
subTotalはOKになります。taxで赤になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/8488e129-bbd6-3de2-843f-e11e4be60199.png)


## 消費税の実装　tax
### プロダクトコード
``` OrderServiceImplement.java
/**
 *
 * @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;


    @Autowired
    ProductRepository productRepository;



    @Override
    public OrderDto create(Integer customerId, List<CartItemDto> cartItemDtos, PaymentMethod paymentMethod) {
        OrderDto orderDto = new OrderDto();
        // cal productCost
       for(var cartItem :cartItemDtos){
           float miniSum = cartItem.getQuantity()*productRepository.getProductById(cartItem.getProductId()).getPrice();
           orderDto.setProductCost(orderDto.getProductCost() + miniSum);
       }
        // cal shipping
        if( orderDto.getProductCost() <= 4000f){
            orderDto.setShippingCost(300);
        }else{
            orderDto.setShippingCost(0);
        }

        // cal subtotal
        orderDto.setSubtotal(orderDto.getProductCost() + orderDto.getShippingCost());
       **// cal tax
        for(var cartItem :cartItemDtos){
            float eachTax = cartItem.getQuantity()*productRepository.getProductById(cartItem.getProductId()).getPrice()*productRepository.getProductById(cartItem.getProductId()).getTaxRate();
            orderDto.setTax(orderDto.getTax() + eachTax);
        }
        // cal total;

        return orderDto;
    }



}


```

### githubの差分

https://github.com/RYA234/springboot-ecshop-rest-api/commit/bf49c4b09a2c08a1c8aa1308693fac26e6684685

### テストの結果
taxはOKになります。totalで赤になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/987648b9-6281-b0a6-734d-b502282c5e49.png)


## 顧客が支払う総額の実装　total
### プロダクトコード
``` OrderServiceImplement.java
/**
 *
 * @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;

    @Autowired
    ProductRepository productRepository;

    @Override
    public OrderDto create(Integer customerId, List<CartItemDto> cartItemDtos, PaymentMethod paymentMethod) {
        OrderDto orderDto = new OrderDto();
        // cal productCost
       for(var cartItem :cartItemDtos){
           float miniSum = cartItem.getQuantity()*productRepository.getProductById(cartItem.getProductId()).getPrice();
           orderDto.setProductCost(orderDto.getProductCost() + miniSum);
       }
        // cal shipping
        if( orderDto.getProductCost() <= 4000f){
            orderDto.setShippingCost(300);
        }else{
            orderDto.setShippingCost(0);
        }

        // cal subtotal
        orderDto.setSubtotal(orderDto.getProductCost() + orderDto.getShippingCost());
        // cal tax
        for(var cartItem :cartItemDtos){
            float eachTax = cartItem.getQuantity()*productRepository.getProductById(cartItem.getProductId()).getPrice()*productRepository.getProductById(cartItem.getProductId()).getTaxRate();
            orderDto.setTax(orderDto.getTax() + eachTax);
        }
        **// cal total;
        orderDto.setTotal(orderDto.getSubtotal() + orderDto.getTax());
        return orderDto;
    }
}


```


### githubの差分

https://github.com/RYA234/springboot-ecshop-rest-api/commit/5e9c960ddb9b4847c7ebbc79ca469c2917da82cd

### テストの結果
totalでOKになります。AssertThatがすべてOKなので、Greenになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/e5574787-6f9f-7376-5c0d-6de597fadf5c.png)


# 感想
プロダクトコード書くのが楽でした。
一方でテストコードを書くのが苦労しました。（何のモックを作るかを検討するあたり）
ストレスなく実装できて、開発者体験は良いと私は思います。
今回のような数値計算が絡むメソッドはテスト駆動開発と相性が良いなと思いました。


# 参考
section21

https://www.udemy.com/course/spring-boot-e-commerce-ultimate/