서비스 레이어에 대한 테스트 코드를 작성중이었다
Mock 객체를 쓸 일이 많아 Mockito를 활용하여 테스트코드를 작성하고 있었다
@ExtendWith(MockitoExtension.class)
class AddressServiceTest {
@Mock
private MemberService memberService;
@Mock
private AddressRepository addressRepository;
@InjectMocks
private AddressService addressService;
Member member;
AddMemberAddressRequest addMemberAddressRequest;
@BeforeEach
void setUp(){
// 테스트용 멤버
member = Member.createNewMember(
Grade.create("테스트 등급",1,"테스트 등급"),
"테스트",
"testId",
"testPassword",
LocalDate.now(),
Member.Gender.M,
"test123@nhn.com",
"010-2222-2222",
Role.createRole("테스트 권한","테스트 권한")
);
try {
member.getClass().getDeclaredField("id").setAccessible(true);
ReflectionUtils.setField(member.getClass().getDeclaredField("id"),member,1L );
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
// 테스트용 AddMemberAddressRequest
addMemberAddressRequest = new AddMemberAddressRequest();
for(Field field : addMemberAddressRequest.getClass().getDeclaredFields()){
field.setAccessible(true);
}
try {
ReflectionUtils.setField(addMemberAddressRequest.getClass().getDeclaredField("name"),
addMemberAddressRequest,"테스트");
ReflectionUtils.setField(addMemberAddressRequest.getClass().getDeclaredField("phoneNumber"),
addMemberAddressRequest,"010-9999-9999");
ReflectionUtils.setField(addMemberAddressRequest.getClass().getDeclaredField("alias"),
addMemberAddressRequest,"테스트");
ReflectionUtils.setField(addMemberAddressRequest.getClass().getDeclaredField("requestedTerm"),
addMemberAddressRequest,"테스트용입니다");
ReflectionUtils.setField(addMemberAddressRequest.getClass().getDeclaredField("zipCode"),
addMemberAddressRequest,"99999");
ReflectionUtils.setField(addMemberAddressRequest.getClass().getDeclaredField("roadNameAddress"),
addMemberAddressRequest,"테스트");
ReflectionUtils.setField(addMemberAddressRequest.getClass().getDeclaredField("numberAddress"),
addMemberAddressRequest,"테스트");
ReflectionUtils.setField(addMemberAddressRequest.getClass().getDeclaredField("notes"),
addMemberAddressRequest,"(테스트)");
ReflectionUtils.setField(addMemberAddressRequest.getClass().getDeclaredField("detailAddress"),
addMemberAddressRequest,"테스트");
ReflectionUtils.setField(addMemberAddressRequest.getClass().getDeclaredField("defaultLocation"),
addMemberAddressRequest,true);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
};
}
@Test
@DisplayName("getMemberAddress 메서드 동작 테스트")
@Order(3)
void getMemberAddressesTest() {
MemberAddress memberAddress = MemberAddress.createMemberAddress(
member,
addMemberAddressRequest
);
try {
memberAddress.getClass().getDeclaredField("id").setAccessible(true);
ReflectionUtils.setField(memberAddress.getClass().getDeclaredField("id"),memberAddress,1L);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
Mockito.when(addressRepository.findMemberAddressByMember(member)).thenReturn(List.of(memberAddress));
addressService.getMemberAddresses(member.getId());
Mockito.verify(addressRepository,Mockito.times(1)).findMemberAddressByMember(member);
}
package com.nhnacademy.taskapi.address.service;
import com.nhnacademy.taskapi.address.domain.dto.req.AddMemberAddressRequest;
import com.nhnacademy.taskapi.address.domain.dto.req.DeleteMemberAddressRequest;
import com.nhnacademy.taskapi.address.domain.dto.req.UpdateMemberAddressRequest;
import com.nhnacademy.taskapi.address.domain.dto.resp.AddMemberAddressResponse;
import com.nhnacademy.taskapi.address.domain.dto.resp.DeleteMemberAddressResponse;
import com.nhnacademy.taskapi.address.domain.dto.resp.GetMemberAddressResponse;
import com.nhnacademy.taskapi.address.domain.dto.resp.UpdateMemberAddressResponse;
import com.nhnacademy.taskapi.address.domain.entity.MemberAddress;
import com.nhnacademy.taskapi.address.exception.InvalidMemberAddressException;
import com.nhnacademy.taskapi.address.exception.MemberAddressNotFoundException;
import com.nhnacademy.taskapi.address.repository.AddressRepository;
import com.nhnacademy.taskapi.member.domain.Member;
import com.nhnacademy.taskapi.member.service.MemberService;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class AddressService {
private final AddressRepository addressRepository;
private final MemberService memberService;
public AddMemberAddressResponse addMemberAddress(Long memberId, AddMemberAddressRequest memberAddressRequest){
// memberId null 체크, memberAddressRequest null 체크는 컨트롤러에서
Member member = memberService.getMemberById(memberId);
MemberAddress memberAddress = MemberAddress.createMemberAddress(member, memberAddressRequest);
addressRepository.save(memberAddress);
return AddMemberAddressResponse.changeEntityToDto(memberAddress);
}
public GetMemberAddressResponse getMemberAddress(Long memberId,Long addressId){
MemberAddress memberAddress = addressRepository.findById(addressId)
.orElseThrow(()-> new MemberAddressNotFoundException("해당하는 ID의 배송지가 존재하지 않습니다"));
if (memberAddress.getMember().getId() != memberId){
throw new InvalidMemberAddressException("해당 ID 배송지의 member ID와 요청한 member ID가 일치하지 않습니다.");
}
return GetMemberAddressResponse.changeEntityToDto(memberAddress);
}
public List<GetMemberAddressResponse> getMemberAddresses(Long memberId){
Member member = memberService.getMemberById(memberId);
List<GetMemberAddressResponse> resp = new ArrayList<>();
for(MemberAddress memberAddress : addressRepository.findMemberAddressByMember(member)){
resp.add(GetMemberAddressResponse.changeEntityToDto(memberAddress));
}
return resp;
}
@Transactional
public UpdateMemberAddressResponse updateMemberAddress(Long memberId, UpdateMemberAddressRequest updateMemberAddressRequest){
MemberAddress memberAddress = addressRepository.findById(updateMemberAddressRequest.getId())
.orElseThrow(()-> new MemberAddressNotFoundException("해당하는 ID의 배송지가 존재하지 않습니다"));
if (memberAddress.getMember().getId() != memberId){
throw new InvalidMemberAddressException("해당 ID 배송지의 member ID와 요청한 member ID가 일치하지 않습니다.");
}
memberAddress.updateMemberAddress(updateMemberAddressRequest);
return UpdateMemberAddressResponse.changeEntityToDto(memberAddress);
}
public DeleteMemberAddressResponse deleteMemberAddress(Long memberId , DeleteMemberAddressRequest deleteMemberAddressRequest){
MemberAddress memberAddress = addressRepository.findById(deleteMemberAddressRequest.getId())
.orElseThrow(()->new MemberAddressNotFoundException("해당하는 ID의 배송지가 존재하지 않습니다"));
if (memberAddress.getMember().getId() != memberId){
throw new InvalidMemberAddressException("해당 ID 배송지의 member ID와 요청한 member ID가 일치하지 않습니다.");
}
addressRepository.delete(memberAddress);
return DeleteMemberAddressResponse.changeEntityToDto(memberAddress.getId());
}
}
테스트 코드를 실행하자 다음과 같은 오류가 발생했다.
WARNING: A Java agent has been loaded dynamically (/Users/pangpange/.m2/repository/net/bytebuddy/byte-buddy-agent/1.14.16/byte-buddy-agent-1.14.16.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
org.mockito.exceptions.misusing.PotentialStubbingProblem:
Strict stubbing argument mismatch. Please check:
- this invocation of 'findMemberAddressByMember' method:
addressRepository.findMemberAddressByMember(
null
);
-> at com.nhnacademy.taskapi.address.service.AddressService.getMemberAddresses(AddressService.java:58)
- has following stubbing(s) with different arguments:
1. addressRepository.findMemberAddressByMember(
com.nhnacademy.taskapi.member.domain.Member@247667dd
);
-> at com.nhnacademy.taskapi.address.service.AddressServiceTest.getMemberAddressesTest(AddressServiceTest.java:153)
Typically, stubbing argument mismatch indicates user mistake when writing tests.
Mockito fails early so that you can debug potential problem easily.
However, there are legit scenarios when this exception generates false negative signal:
- stubbing the same method multiple times using 'given().will()' or 'when().then()' API
Please use 'will().given()' or 'doReturn().when()' API for stubbing.
- stubbed method is intentionally invoked with different arguments by code under test
Please use default or 'silent' JUnit Rule (equivalent of Strictness.LENIENT).
For more information see javadoc for PotentialStubbingProblem class.
at com.nhnacademy.taskapi.address.service.AddressService.getMemberAddresses(AddressService.java:58)
at com.nhnacademy.taskapi.address.service.AddressServiceTest.getMemberAddressesTest(AddressServiceTest.java:155)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Process finished with exit code 255
그리고 테스트코드에 노란줄이 떠있길래 봤더니
다음과 같은 문구가 나왔다

org.mockito.exceptions.misusing.PotentialStubbingProblem:
Strict stubbing argument mismatch. Please check:
- this invocation of 'findMemberAddressByMember' method:
addressRepository.findMemberAddressByMember(
null
);
-> at com.nhnacademy.taskapi.address.service.AddressService.getMemberAddresses(AddressService.java:58)- has following stubbing(s) with different arguments:
1. addressRepository.findMemberAddressByMember(
com.nhnacademy.taskapi.member.domain.Member@247667dd
);
읽어보니 핵심적인 부분은 다음과 같았다
나는 테스트 코드에서 when을 사용해 'findMemberAddressByMember'를
스터빙 해놓았는데 실제 테스트에서는 스터빙해놓은 메서드에 정의한것과 다른 argument를 넣었다는 것이었다.
좀 더 구체적으로 말해보자면 원인은
스터빙은
findMemberAddressByMember(Member@247677dd 객체).thenReturn...
라고 해놓고
실제 테스트에선 findMemberAddressByMember(null).thenReturn...
즉 스터빙을 해서 메서드가 어떻게 사용되며 어떤 결과값을 도출할지 정의해놓고선
그대로 사용을 안해서 생기는 문제였다
그래서 코드를 자세히 보기로 했다
서비스가 필드로
테스트코드에서 저 두개의 클래스는 Mock 객체로 사용하였다



addressService의 getMemberAdressess 메서드는 다음과 같은
큰 두개의 로직으로 구성된다
파라미터로 member의 Id를 매개변수로 받으면
memberService의 getMemberId() 메서드를 이용해서
해당하는 ID의 member를 찾아오고
찾아온 member를 addressRepository의findMemberAddressMember() 메서드의 매개변수로 넣어주면 해당 member가 가지고 있는 모든 주소가 List 형태로 리턴된다
문제가 드러났다 아뿔싸
위에서 말했듯이 테스트코드에서는
두개가 mock 객체였다
그런데 나는 test코드에서
memberService의 getMemberById는 스터빙하지 않았고
addressRepository의 findMemberAddressByMember만 스터빙하고 있었다.

따라서 테스트코드를 돌려보면
memberService의 getMemberById가 선행되고
그 이후에
addressRepository의 findMemberAddressByMember가 행해지는데
getMemberById가 스터빙되지 않았으니 반환하는 member가 따로 없으니
member가 null이되고
findMemberAddressByMember 메서드의 파라미터엔 null이 들어갈 수 밖에 없었다
즉 이래서 스터빙해놓은 메서드와, 실제 사용된 메서드의 형태가 달랐던것이었다.

Mockito.when(addressRepository.findMemberAddressByMember(member)).thenReturn(List.of(memberAddress));
memberService의 getMemberById도 스터빙해주니까
말끔하게 해결되었다.


