티스토리 뷰

728x90

여태까지 공부하면서 테스트 코드가 가장 어려웠다.

테스트 자체가 어렵다기 보다는 명확하게 잘리지 않은 경계선을 명확하게 잘라내고 구분지어서 

'딱 여기서부터 여기까지만. 나머지는 전부 그렇다고 쳐'의 논리 과정을 갖는 것이 쉽지 않았다.

 

테스트를 이해하는 데에만 시간을 꽤 많이 쏟은 것같은데 오늘 드디어 어느 정도 이해한 느낌을 받아 내용을 정리하고자 한다.

 

본인이 말하고자 하는 테스트란 유닛 테스트에 한하여 말한다. 유닛 테스트만 할줄 알면 통합 테스트는 식은 죽 먹기라는 말을 본 적이 있는데 그 말에 매우 동의한다.

 

기본 개념

스프링 내에서 혼자 살아가는 객체는 어느 무엇도 없다.

모든 객체가 크고 작은 요구를 다른 객체에게 해가며 특정 인풋에 대해 특정 액션을 하여 특정 아웃풋을 낸다.

 

특히 IoC로 인해 개발자는 처음에 한번 만들어 Bean으로 등록 이후에 필요할 때마다 선언만 하고 주입해준 인스턴스를 사용하기만 된다.

 

이때 유닛 테스트를 작성한다는 것은 위의 모든 연관 관계를 끊어내고 IoC도 받지 않으며 마치

"내가 스프링 컨테이너가 된 것처럼" 코딩해야 한다.

이 개념이 가장 난해했고 사람마다 생각은 다르겠지만 본인은 위와 같이 이해하였다.

 

내가 어떻게 흘러가는지 모르면 테스트 코드를 아예 작성할 수가 없다는 말과 일맥상통하며 반대로 생각하면 테스트 코드만 잘 짜두면 나에게도 물론 남에게도 이해하기 좋은 코드가 된다는 것이다.

 

Mocking 이해하기

유닛 테스트를 하면 반드시 따라오는 말이 Mocking이다.

본인은 이 mocking을 처리하는 과정이 가장 와닿지 않았다.

 

mocking은 테스트에 있어 필수 조건은 아니며 POJO일수록 좋은 테스트이지만 POJO는 생산성이 떨어지고 통합 테스트는 성능이 떨어진다(라고 생각한다.).

mocking이 진정한 생산성과 성능 두 마리의 토끼를 잡아낸 방법이라고 생각한다.

이렇게 좋은 방법이었기에 여태까지 이해가 되지 않고 와닿지 않은게 아닌가 싶다. 

 

mocking의 진정한 핵심은 단 한가지이다.

"내가 궁금한건 이 객체 단 하나니까 나머지는 다 내 말대로 해"

여기서 핵심은 '내가 궁금한건 너 하나'가 아니라 '나머지는 다 내 말대로 행동해'였다.

 

사실 대부분은 유닛 테스트에서 '내가 궁금한건 너 하나'라는 개념은 너무 당연하고 직관적이므로 쉽게 이해할 것같다. 본인도 그랬었고.

본인에게 있어 가장 난해했던 문제는 '나머지는 다 내 말대로 행동해'였다.

도대체 뭘 내 말대로 행동하라는건지, 여기서 나는 누구인지, 그래서 그걸 어떻게 하는건지 등등..

 

여기서 mocking을 하는 것도 "나"이고 '내 말'도 "나"의 말이고 '나'도 "나"이며 이 모든걸 하는 것도 "나" 자신이다.

스프링이 주도하는 세상에서 내가 주도하는 세상으로 와버리는 것이다.

 

이 개념을 좀 더 빠르게 확실히 이해했다면 테스트를 이해하는데 좀 더 시간이 단축되지 않았을까 생각하는 아쉬운 생각이 든다.

 

 

테스트를 작성함에 있어 가장 중요했던 것은 어디서 어떤 테스트를 진행할 것인가? 였다.
테스트를 전혀 이해하지 못 했을 때에는 Validation을 Repository에서 진행하려고 했었다. 
validation은 DTO와 Controller의 역할이지 Repository의 역할이 아니다!

 

POJO와 Repository

POJO와 Repository는 연관 관계를 가지는 경우도 별로 없고 있다고 해도 연관 관계를 나눠서 테스트를 하는 것이 바람직하므로 mocking이라고 할만한 요소가 별로 없다.

 

단순히 어떤 input에 대해서만 mocking을 하고 그에 대한 output만 잘 정의를 내리면 된다.

 

Repository의 예시

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    void setUp() {
        final User user = EntityBuilder.getUser();
        
        userRepository.save(user);
    }

    @Test
    void userShouldBeSaved() {
        final List<User> all = userRepository.findAll();

        assertThat(all).isNotEmpty();
    }

 

BeforeEach로 mock 객체를 매 테스트마다 초기화 될 수 있도록 만들어주고 모든 테스트마다 사용할 수 있도록 하면 된다.

 

하지만 위의 예시에서 userShouldBeSaved 테스트는 틀렸다.

JPA에서 기본으로 제공해주는 메서드를 테스트하고 있기 때문이다.

save, findById, findAll 등 날로 먹을 수 있는 것들은 날로 먹어주는 것이 상책이다.

 

 

@Test
void userShouldBeReturned_FindByEmail() {
    final String EMAIL = "qwe@qwe";
    final User user = userRepository.findByEmail(EMAIL)
            .orElseThrow(() -> new UserNotFoundException(" "));

    assertThat(user.getEmail()).isEqualTo(EMAIL);
}

@Test
void returnOptionalEmpty_WrongEmail_FindByEmail() {
    final String EMAIL = "111@111";
    final Optional<User> user = userRepository.findByEmail(EMAIL);

    assertThat(user).isEqualTo(Optional.empty());
}

 

위와 같이 본인이 직접 작성한 메서드들에 대해서 테스트를 진행해야 한다.

테스트는 중복되지 않는 선에서 최대한 많은 경우의 수를 생각하여 작성하는 것이 좋다.

 

 

POJO의 예시

본인이 작성한 코드 중에서 POJO는 그렇게 많지 않았다.

대부분 Embedded로 들어가는 model의 형태였으며 담고있는 로직도 매우 단순하여 테스트 또한 매우 간단하였다.

 

코드는 다음과 같다.

class NameTest {

    Name name = Name.builder().build();

    @BeforeEach
    void setUp() {
        name = Name.builder()
                .last("홍")
                .first("길동")
                .build();
    }
    @Test
    void getFullName() {
        final String fullName = name.getFullName();

        assertThat(fullName).isEqualTo("홍길동");
        assertThat(fullName).isNotEqualTo("길동홍");

        assertThat(fullName).contains("홍");
        assertThat(fullName).contains("길동");

        assertThat(fullName).doesNotContain("김");
    }
}

 

Service

본인이 가장 고전했던 layer였다!

그 놈의 mocking을 해야 되는 곳인데 그 놈의 mocking이 전혀 이해가 되지 않는 무간지옥이었다.

 

mocking에 대한 기본 개념은 상술했으므로 여기서는 간단히 흐름을 설명하고자 한다.

기본적으로 '내가 궁금한건 너 하나'가 여기서 @InjectMocks로 선언된다.

본인은 이 어노테이션의 명칭이  @InjectMocks가 아닌 @InjectedMock이어야 하지 않았나 생각한다..

 

아무튼 Injection을 받아야 할 곳을 선언 후에

Injection이 될 객체들을 @Mock으로 선언한다.

여기서 이 @Mock들이 '나머지는 다 내 말대로 행동해'의 나머지에 속하는 녀석들이다.

 

@ExtendWith(MockitoExtension.class)
class CartServiceTest {

    @InjectMocks
    CartService cartService;

    @Mock
    CartRepository cartRepository;
    @Mock
    ItemOptionRepository itemOptionRepository;
    @Mock
    ItemRepository itemRepository;

 


Service에서 가장 중요한 핵심은 @InjectMocks이든 @Mock이든 전부 선언만으로는 어떠한 동작도 하지 않는 깡통들이라는 것이다!

 

@InjectMocks와 @Mock을 선언하는 것은 마치 아래의 코드와 같다.

@Service
public class MyService {
//내부 코드 없음
}

 

정확히 위 코드랑 같다.

한 마디로 아무런 기능이 들어가 있지 않으며 Mockito라는 프레임워크에게 존재를 알려주는 것뿐이다.

 

스프링에서도 위와 같이 객체를 @Service로 선언하고 내부에 아무런 값도 넣어주지 않는다면 스프링에서 해당 Bean에 대해서는 인지하고 있겠지만 어떠한 내부적 행동도 취해주지 않는 것과 같다.

 

핵심

이제 여기서 진짜 핵심이 나온다.

mocking을 함에 있어 2가지의 모킹이 있다고 생각하는데 하나는 값에 대한 모킹이며

다른 하나가 '행동에 대한 모킹'이다.

 

값에 대한 모킹은 POJO와 Repository에서도 나왔듯이 여기서 이런 값을 넣을거야.라고 명시하는 것과 같다.

행동에 대한 모킹은 "여기서 너는 이렇게 행동해야해"라고 명시하는 것과 같다.

 

이 부분을 받아들이기가 가장 힘들었다. 내가 어떻게 너가 이렇게 행동한다는걸 안다는 것인가??

Repository에 대한 테스트를 먼저 진행한 이유가 이것이었다.

나는 이미 Repository에 대한 충분한 검증을 거쳤으므로 여기서 이 값을 넣으면 '이렇게 행동한다는 것을 인지'하고 있다.

그 인지를 기반으로 행동을 정의하는 것이다.

 

 

    @Test
    void addItemOptionsToCart() {
        List<ItemOptionResponse> list = new ArrayList<>();
        ItemOptionResponse itemOptionResponse = ItemOptionResponse.builder()
                .itemQuantity(5)
                .itemId(item.getId())
                .itemOptionId(itemOption.getId())
                .build();
        list.add(itemOptionResponse);

        when(itemRepository.findById(itemOptionResponse.getItemId())).thenReturn(Optional.of(item));
        when(itemOptionRepository.findById(itemOptionResponse.getItemOptionId())).thenReturn(Optional.of(itemOption));

        cartService.addOptionItemsToCart(list, user);

        verify(cartRepository, times(1)).save(any());
    }

 

위 코드에서 when -> thenReturn이 그 행동을 정의하는 과정이다.

위 코드에서 findById는 JPA에 의해 기본적으로 주어지는 메서드이지만 위에서도 말했듯 

이 기본적으로 주어지는 메서드조차 내가 직접 정의를 내려야한다.

 

나는 ItemRepository에서 findById를 하면 Optional<Item>이 나올 것을 이미 인지하고 있다.

findByEmail, findByName과 같이 본인이 직접 정의한 메서드이었더라도 본인이 내린 정의와 그에 맞게 잘 검증된 메서드들이라면 나는 그것을 인지하고 있는 것이다.

 

이 부분이 '너는 여기서 이렇게 행동해'의 '너'와 '여기서'를 작성한 것이다.

이제 '이렇게 행동해'를 정의해야 한다. 

위 예시에서는 Optional.of(item)을 반환해.라고 정의한 것을 알 수 있다.

 

위와 같이 정의를 내리면 저 repository의 저 method는 자바가 사라지는 날까지 똑같은 아웃풋을 내놓을 것이다.

다시 말하지만 이렇게 정의를 내릴 수 있는 근거는 내가 이미 검증 마쳤기 때문이다.

 

마지막으로 본인이 검증하고자 한 메서드의 return 값이 void인 경우가 발생했다.

마지막에 repository에 save하고 끝나는 메서드와 같은 경우엔 verify를 사용할 수 있다.

 

verify는 mocking과는 좀 다른 검증 방법으로 

내 검증 대상을 검증할 때 해당 method의 호출 여부나 호출 정도를 알 수 있다.

 

cartRepository에 save라는 메서드가 한 번 호출된 적이 있는지 확인해줘.라고 요청을 보내는 것과 같다.

이때 우리는 save한 이후에 그 save의 return 값을 받아 무언가 더 로직을 진행하는 과정이 없기 때문에 

will -> then을 통해 행동에 대한 mocking을 정의해주지 않은 것을 알 수 있다.

 

또한 verify의 호출 여부를 확인하는 로직의 경우 

if (cartRepository.findByUserAndItemAndItemOption(user, item, itemOption).isPresent()) {
    throw new AlreadyExistsException("해당 상품이 장바구니에 존재합니다");
}

 

 

위와 같이 if를 통해 boolean을 체크해서 로직을 진행하는 경우라도

verify(cartRepository, times(1)).findByUserAndItemAndItemOption(user, item, itemOption);

위의 결과는 true가 나온다는 것을 명심해야 한다.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함