概要
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レイヤーのモックデータを作ります。
実装
テストコード→プロダクトコードの順で実装します。
テストコード
最初に書きます。
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
単純にショッピングカートの商品の合算値です。
プロダクトコード
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の差分
テストの結果
productCostはOkになります。shippingCostで赤になります。(スクショ取ってなかったです。)
配送料の実装 shippingCost
プロダクトコード
/**
*
* @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の差分
テストの結果
shippingCostはOKになります。subTotalで赤になります。
商品と運送料の合算値の実装 subTotal
プロダクトコード
/**
*
* @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の差分
テストの結果
消費税の実装 tax
プロダクトコード
/**
*
* @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の差分
テストの結果
顧客が支払う総額の実装 total
プロダクトコード
/**
*
* @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の差分
テストの結果
totalでOKになります。AssertThatがすべてOKなので、Greenになります。
感想
プロダクトコード書くのが楽でした。
一方でテストコードを書くのが苦労しました。(何のモックを作るかを検討するあたり)
ストレスなく実装できて、開発者体験は良いと私は思います。
今回のような数値計算が絡むメソッドはテスト駆動開発と相性が良いなと思いました。
参考
section21