여러 자료들을 찿아보는 과정에서 정말 마음에 와닿는 글이 있었다. 🔗

스크린샷 2024-10-22 오후 2.13.28.png

무작정 모든 것에 대해서 테스트 하는 것보다는 좋은 테스트, 필요한 테스를 효율적으로 하는게 좋다고 생각했다.

<aside> 💡

결국 좋은 테스트라는 건


📌 그렇다면 테스트 코드는 어떻게 짜야할까?

<aside> 1️⃣

회귀를 방지하는 테스트

정확히는 새로운 기능을 추가했을 때, 의도한 대로 작동하지 않아 이미 완성된 코드를 수정하려고 돌아가는 것.

프로젝트가 커지면서 소프트웨어의 복잡도는 정말 빠르게 증가하는데, 이러한 회귀를 방지하고자 미리 테스트 해야합니다. 테스트를 통과했는데 기능은 고장나는 거짓음성(False Nagative)을 만들지 않도록, 회귀하지 않도록 가능한 모든 경우를 테스트로 검증하는 것이 중요합니다.

회귀하지 않도록 테스트로 검증하는 것이 중요한데, 회귀 오류가 생길일이 없는 코드를 테스트하지는 말자. 예를 들어 User.getName() 처럼 아무런 로직이 없는 코드를 테스트 할 필요는 없다는 말입니다.

</aside>

<aside> 2️⃣

리펙토링에 내성이 있는 테스트 코드

이건 실제로 이번 우테코 프로젝트를 하면서 겪은 일입니다 . 클래스를 없애고 리펙토링을 하는 과정에서 기존에 만든 테스트 코드를 지우는 일이 발생했습니다. 물론 아예 클래스를 지워버리고 새롭게 디자인하는 과정은 흔하지는 않겠지만…

새로운 기능을 추가했고 모든 것이 잘 동작하는데 기존 테스트가 깨지면 안됩니다. 그런데 테스트가 리팩토링에 내성이 있으려면 어떻게 코드를 작성해야할까요?

간단합니다. 구현 세부사항이 아닌, 최종 결과를 테스트 목표로 삼으면 된다.

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private PasswordEncoder passwordEncoder;  // 내부 세부 구현을 Mocking

    @InjectMocks
    private UserService userService;

    @Test
    void testPasswordEncoding() {
        // Stub: PasswordEncoder가 어떻게 동작하든, "encodedPassword"를 반환하도록 설정
        when(passwordEncoder.encode("rawPassword")).thenReturn("encodedPassword");

        // Service 메서드를 실행하고 결과를 검증
        String encodedPassword = userService.encodePassword("rawPassword");

        // 내부 알고리즘이 아닌 결과만 확인 (특정 값이 아니라 인코딩 결과가 유효한지 확인)
        assertTrue(encodedPassword.equals("encodedPassword"));  // 최종 결과 검증
    }
}

회귀방지에서는 가능한 모든 경우를 테스트하라고 했다. 그런데 여기선 하지말라고? 그게 아니다. 조금 더 생각해보자.

회원가입을 할 때 비밀번호의 PasswordEncoder 같은 내부 알고리즘이 바뀌었다고 테스트 결과에 영향이 있을까?

사실 테스트를 할 때 그 객체의 내부 동작까지 알 필요는 없다. 공개된 동작(api)을 실행하고, 그 결과만 확인하면 된다.

👉🏻 확장성이 있는 테스트를 작성하도록 하자.. !

</aside>

<aside> 3️⃣

빠른 피드벡과 유지 보수성

모든 의존성을 다 검사하는게 아닌, 그 대상만을 빠르게 검증하고 버그를 피드백함으로서 버그 수정비용을 크게 줄일 수 있습니다.

비슷한 맥락으로, 단위테스트는 유지보수성이 좋아야 하는데 (like 2번) 테스트를 이해하기 쉽고 외부 의존성없이 바로 실행하기 쉽게 만듦으로서 나중에 테스트를 추가하더라도 테스트의 유지보수가 쉬워지게 됩니다.

</aside>