오늘 받은 코드 리뷰에 대해서 정리해 보려고 합니다.
문제가 되는 부분이 있다면 댓글로 자유롭게 남겨주세요.
public class CustomerResponse { // record
private final String email;
private final LocalDateTime createdAt;
private final String name;
private final LocalDateTime lastLoginAt;
public CustomerResponse(Customer customer) {
this.email = customer.getEmail();
this.createdAt = customer.getCreatedAt();
this.name = customer.getName();
this.lastLoginAt = customer.getLastLoginAt();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(name).append(" ")
.append(email).append(" ")
.append(createdAt).append(" ")
.append(lastLoginAt).append(" ");
return sb.toString();
}
public String getName() {
return name;
}
}
디컴파일
before
after
//befor
}
}
// after
}
}
//before
@Mock
private KeyGenerator keyGenerator;
@Mock
private VoucherCreator voucherCreator;
//after
@Mock
private KeyGenerator keyGenerator;
@Mock
private VoucherCreator voucherCreator;
지금 당장은(지금 프로젝트 수준에서는 + 콘솔환경) 문제 없다 왜냐하면 NPE가 발생할 일이 없기 때문에
하지만 DTO에는 비즈니스 로직이 있어서는 안된다.
private void validateName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException(ErrorMessage.NULL_ARGUMENT.getMessage());
}
}
public void changeName(String name) {
validateName(name);
this.name = name;
}
@Component
public class WalletConverter {
private final KeyGenerator keyGenerator;
public WalletConverter(KeyGenerator keyGenerator) {
this.keyGenerator = keyGenerator;
}
public Wallet convertWallet(WalletRequest walletRequest) {
int id = keyGenerator.make();
int customerId = walletRequest.getCustomerId();
int voucherId = walletRequest.getVoucherId();
return new Wallet(id, customerId, voucherId);
}
}
public class Wallet {
private final int customerId;
private final int voucherId;
private final int walletId;
private boolean deleted = false;
public Wallet(int walletId, int customerId, int voucherId) {
this.walletId = walletId;
this.customerId = customerId;
this.voucherId = voucherId;
}
저의 경우 사용자가 데이터를 마음대로 삭제하는 건 위험하다고 생각하여 deleted라는 boolean 변수를 사용하여
삭제 여부를 표시했고
이로써 관리자가 삭제 여부를 판단하고 삭제할 수 있도록 관리자의 역할을 나두게 되었습니다.
하지만 멘토님께서
before
@Override
public void run(String... args) {
Power power = Power.ON;
while (power.isOn()) {
try {
Menu menu = guideStartVoucher();
Command command = commandCreator.createCommand(menu);
power = command.execute();
} catch (IllegalArgumentException e) {
logger.error("사용자의 잘못,된 입력이 발생하였습니다.");
output.write(e.getMessage());
}
}
}
after
@Override
public void run(String... args) {
Power power = Power.ON;
while (power.isOn()) {
try {
Menu menu = guideStartVoucher();
Command command = commandCreator.createCommand(menu);
power = command.execute();
} catch (IllegalArgumentException e) {
logger.error("사용자의 잘못,된 입력이 발생하였습니다. {0}", e);
output.write(e.getMessage());
}
}
}
before
@Override
public void run(String... args) {
Power power = Power.ON;
while (power.isOn()) {
try {
Menu menu = guideStartVoucher();
Command command = commandCreator.createCommand(menu);
power = command.execute();
} catch (IllegalArgumentException e) {
logger.error("사용자의 잘못,된 입력이 발생하였습니다. {0}", e);
output.write(e.getMessage());
}
}
}
after
@Override
public void run(String... args) {
Power power = Power.ON;
while (power.isOn()) {
try {
Menu menu = guideStartVoucher();
Command command = commandCreator.createCommand(menu);
power = command.execute();
} catch (IllegalArgumentException e) {
logger.error("사용자의 잘못,된 입력이 발생하였습니다. {0}", e);
output.write(e.getMessage());
} catch (Exception e) {
logger.error("알 수 없는 error 발생");
}
}
}
IllegalArgumentException인 언체크드 인셉션 외의 예외가 발생할 경우를 대비
NULL_ARGUMENT("null이 발생하였습니다."),
null이어서는 안됩니다와 같은 진짜 에러를 나타내는 메세지
외부인프라, 메세지큐의 경우 Mocking만으로 테스트가 가능하다
그러나 서비스처럼 핵심 비즈니스 로직은 목킹으로 테스트 하는게 의미가 있을까
@SpringBootTest를 이용해서 진짜로 되는지 확인해야 하는데 목킹이 의미가 있을까
생각해봐야 한다.
class WalletServiceTest {
private static final int CUSTOMER_ID = 10;
private static final int VOUCHER_ID = 10;
private static final int WALLET_ID = 10;
private static final String TEST_USER_NAME = "test-user";
private static final String TEST_USER_EMAIL = "test1-user@gmail.com";
private static final LocalDateTime CREATE_AT = LocalDateTime.now();
@Mock
private WalletRepository walletRepository;
@Mock
private VoucherConverter voucherConverter;
@Mock
private WalletConverter walletConverter;
@Mock
private CustomerConverter customerConverter;
@InjectMocks
private WalletService walletService;
private WalletRequest walletRequest;
private Wallet wallet;
@BeforeEach
void setup() {
MockitoAnnotations.openMocks(this);
walletRequest = new WalletRequest(CUSTOMER_ID, VOUCHER_ID);
wallet = new Wallet(WALLET_ID, CUSTOMER_ID, VOUCHER_ID);
}
@Test
@DisplayName("사용자가 바우처를 등록하는 지갑 정보와 지갑 정보를 등록해주는 서비스로 등록된 지갑정보는 같다.")
void giveVoucher_InsertWallet_Equals() {
//given
when(walletConverter.convertWallet(walletRequest)).thenReturn(wallet);
when(walletRepository.insert(wallet)).thenReturn(wallet);
BDDMockito.given().willReturn()
//when
Wallet result = walletService.giveVoucher(walletRequest);
//then
assertThat(result).isEqualTo(wallet);
verify(walletRepository, times(1)).insert(wallet);
}
@Test
@DisplayName("사용자가 삭제하고자 하는 지갑 정보와 바우처를 삭제헤주는 서비스를 통해 삭제된 지갑 정보는 같다.")
void takeVoucher_DeleteWallet_Equals() {
//given
when(walletRepository.deleteWithVoucherIdAndCustomerId(VOUCHER_ID, CUSTOMER_ID)).thenReturn(wallet);
//when
Wallet result = walletService.takeVoucher(walletRequest);
//then
assertThat(result).isEqualTo(result);
verify(walletRepository, times(1)).deleteWithVoucherIdAndCustomerId(VOUCHER_ID, CUSTOMER_ID);
}
@Test
@DisplayName("임의로 고객의 정보를 넣었을 때, 그 고객의 정보가 바우처 아이디를 통해 찾은 고객 정보 리스트에 포함되어 있다.")
void customerList_CustomerResponseList_Contains() {
//given
List<Customer> customers = new ArrayList<>();
Customer customer = new Customer(CUSTOMER_ID, TEST_USER_NAME, TEST_USER_EMAIL, CREATE_AT);
customers.add(customer);
List<CustomerResponse> customerResponses = List.of(new CustomerResponse(customer));
when(walletRepository.findAllCustomersByVoucher(VOUCHER_ID)).thenReturn(customers);
when(customerConverter.convertCustomerResponse(customers)).thenReturn(customerResponses);
//when
List<CustomerResponse> result = walletService.customerList(VOUCHER_ID);
//then
assertThat(result).containsExactlyElementsOf(customerResponses);
verify(walletRepository, times(1)).findAllCustomersByVoucher(VOUCHER_ID);
verify(customerConverter, times(1)).convertCustomerResponse(customers);
}
@Test
@DisplayName("임의로 바우처의 정보를 넣었을 때, 그 바우처의 정보가 고객 아이디를 통해 찾은 바우처 정보 리스트에 포함되어 있다.")
void voucherList_VoucherResponseList_Contains() {
//given
Voucher voucher = new FixedAmountVoucher(VOUCHER_ID, new FixedDiscount(20), VoucherType.FIXED_AMOUNT_VOUCHER);
List<Voucher> voucherList = List.of(voucher);
Vouchers vouchers = new Vouchers(voucherList);
List<VoucherResponse> voucherResponses = List.of(new VoucherResponse(voucher));
when(walletRepository.findAllVouchersByCustomer(CUSTOMER_ID)).thenReturn(vouchers);
when(voucherConverter.convertVoucherResponse(vouchers)).thenReturn(voucherResponses);
//when
List<VoucherResponse> result = walletService.voucherList(CUSTOMER_ID);
//then
assertThat(result).containsExactlyElementsOf(voucherResponses);
verify(walletRepository, times(1)).findAllVouchersByCustomer(CUSTOMER_ID);
verify(voucherConverter, times(1)).convertVoucherResponse(vouchers);
}
}
문제의 코드 죄다 목킹인데
껍데기에 리턴값 이거다 알려주고 확인하는 용도에서만 그친다
정말 좋은 테스트일까?
핵심 비즈니스로직이 있는 코드인데
public Voucher createVoucher(VoucherType voucherType, double discountAmount) {
int id = keyGenerator.make();
Discount discount = discountCreator.createDiscount(voucherType, discountAmount);
Voucher voucher = voucherCreator.createVoucher(id, voucherType, discount);
return voucherRepository.insert(voucher);
}
현재 Voucher가 있는데 사용자가에게 Voucher 안에 있는 중요 정보가 그대로 노출될 수 있는 환경이다.
final 카멜케이스
static final 상수펴기법
@Component
public class CommandCreator {
private final Map<Menu, Command> strategies;
private final WalletService walletService;
private final VoucherService voucherService;
private final Input input;
private final Output output;
public CommandCreator(WalletService walletService, VoucherService voucherService, Input input, Output output) {
this.input = input;
this.output = output;
strategies = new HashMap<>();
this.voucherService = voucherService;
this.walletService = walletService;
strategies.put(Menu.EXIT, new ExitCommand(this.output));
strategies.put(Menu.LIST, new ListCommand(this.output, voucherService));
strategies.put(Menu.CREATE, new CreateCommand(this.input, this.output, voucherService));
strategies.put(Menu.GIVE_VOUCHER, new GiveVoucherCommand(this.output, this.input, walletService));
strategies.put(Menu.TAKE_VOUCHER, new TakeVoucherCommand(this.output, this.input, walletService));
strategies.put(Menu.CUSTOMER_LIST, new CustomerListCommand(this.output, this.input, walletService));
strategies.put(Menu.VOUCHER_LIST, new VoucherListCommand(this.input, this.output, walletService));
}
public Command createCommand(Menu menu) {
return strategies.get(menu);
}
}
전략패턴을 사용했는데
이미 있는 빈을 생성한다.
사용자 요청마다 만드는 것...!!