확장성이 떨어지는 White Box Test 를 왜 하는걸까?

tony·2024년 4월 18일

Java

목록 보기
4/5

순수하게 제가 테스트 코드에 대해 바라보는 관점에서 적은 포스트이므로 수정사항이 있으시면 코멘트 부탁드립니다 🙏

Intro : Unit Test & WhiteBox Test 💭


Unit Test

  • 모듈이 의도된 대로 정확히 작동하는지 검증하는 테스트

White Box Test

  • 프로그램의 내부 구조와 동작을 검사
  • 모듈(메서드)에 대해서 참/거짓 테스트 케이스를 기반으로 분기문을 검증

Black Box Test

  • 프로그램의 외부 사용자의 요구사항 명세를 검증하는 기능 테스트
  • 유스케이스나 테스트케이스에 대한 기능수행여부를 검증

*각 테스트 타입에 따라 여러 종류가 있다. 해당 포스트의 핵심에서 벗어나므로 명세하지 않았다.

위 개념에 따라 아래와 같은 생각이 들었다.

"확장성을 고려한 구현 및 테스트라면 기능에 대한 검증을 처리해야하므로 화이트박스 테스트를 최소화해야하지 않을까?"

왜 유닛테스트에 대한 화이트박스 테스트가 필요한지 알아보자.

구현프로세스에 따른 테스트 코드 작성👇


TDD : Black Box First, White Box Later

가령 아래와 같은 기능을 만든다 치자.

회원가입 요청에 대해 새로운 사용자를 생성해주세요

사용자ID에 따른 사용자를 조회해주세요

그렇다면 요구사항 체크리스트는 아래와 같을 것이다.

# 기능 요구사항 체크리스트
## 기능 검증
1. **유효한 사용자 생성**
    - [ ] 유효한 `User` 객체(널이 아니고 비어 있지 않은 사용자 이름 포함)를 저장.
    - [ ] `UserRepository`를 사용하여 `User` 객체를 저장함.
    - [ ] 저장된 `User` 객체를 동일한 `id`와 `username`으로 반환함.
2. **유효한 사용자 검색**
    - [ ] `UserRepository`를 사용하여 `id`로 `User` 객체를 검색함.
    - [ ] 검색된 `User` 객체를 동일한 `id`와 `username`으로 반환함.
## 예외 검증
1. **유효하지 않은 사용자 생성**
    - [ ] 널 사용자 이름을 가진 `User` 객체가 전달될 때 `IllegalArgumentException`을 발생시킴.
    - [ ] `UserRepository`를 사용하여 유효하지 않은 `User` 객체를 저장하지 않음.
2. **유효하지 않은 사용자 검색**
    - [ ] 존재하지 않는 `id`로 `User` 객체가 전달될 때 `IllegalArgumentException`을 발생시킴.
    - [ ] `UserRepository`에서 어떤 `User` 객체도 검색하지 않음.
## 추가적인 검증:
- [ ] 사용자를 저장할 때 `UserRepository.save()`와 정확히 한 번 상호작용함.
- [ ] `id`로 사용자를 검색할 때 `UserRepository.findById()`와 정확히 한 번 상호작용함.
- [ ] 널 사용자 이름으로 사용자를 저장하려고 할 때 `UserRepository.save()`와 상호작용하지 않음.
- [ ] 존재하지 않는 사용자를 검색하려고 할 때 `UserRepository.findById()`와 정확히 한 번 상호작용함.

이에 따라 TDD 를 적용하여 기능 검증을 우선시하여 아래와 같이 테스트를 작성할 수 있다.

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

public class SimpleUserServiceTest {

    private SimpleUserService simpleUserService;

    @Mock
    private UserRepository userRepository;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.openMocks(this);
        simpleUserService = new SimpleUserService(userRepository);
    }

    @Test
    public void testCreateUser() {
        User user = new User(1L, "john_doe");
        when(userRepository.save(any(User.class))).thenReturn(user);

        User savedUser = simpleUserService.createUser(user);

        assertNotNull(savedUser);
        assertEquals(user.getId(), savedUser.getId());
        assertEquals(user.getUsername(), savedUser.getUsername());
    }

    @Test
    public void testGetUserById() {
        User user = new User(1L, "john_doe");
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        User retrievedUser = simpleUserService.getUserById(1L);

        assertNotNull(retrievedUser);
        assertEquals(user.getId(), retrievedUser.getId());
        assertEquals(user.getUsername(), retrievedUser.getUsername());
    }

    @Test
    public void testCreateUserWithNullUsername() {
        User user = new User(1L, null);

        assertThrows(IllegalArgumentException.class, () -> simpleUserService.createUser(user));
    }

    @Test
    public void testGetUserByIdNotFound() {
        when(userRepository.findById(1L)).thenReturn(Optional.empty());

        assertThrows(IllegalArgumentException.class, () -> simpleUserService.getUserById(1L));
    }
}

이렇게 작성한 테스트 코드를 통해 서비스 코드를 차근차근 만들어나가도록 한다.

이 과정이 바로 TDD 의 근간이다. — 적어도 내가 알고 있는 한에서는,,

위 테스트는 “기능에 대한 검증” 에 가까우므로 블랙 박스 테스트에 가깝다.

반대로 기능검증보다 빠른 구현을 처리해야한다면

  • 서비스 코드를 먼저 만들고
  • 화이트박스 테스트에 가까운 유닛테스트를 만들어 기능동작을 검증하고
  • 통합테스트를 통해 요구사항에 부합하는지 확인한다.

즉, 어떤 프로세스를 통해 기능구현을 하냐에 따라 작성되는 테스트코드의 순서가 바뀐다.

그렇다면 어떤 테스트코드가 우선시되어야할까?

아니 그래서 유닛에 대해 화이트박스 테스트가 필요하냐구 🤷‍♂️


화이트박스는 구현의 내부동작 그 자체를 검증한다.

이 말인 즉슨, 깨지기 쉬운 테스트라는 것이다.

하지만 “깨지기 쉽다” 는 오히려 기능동작검증의 안전성을 높인다.

구현체의 내용이 변경되면 이전의 화이트박스 테스트가 깨지게되고,

이에 따라 기능검증을 다시 처리하여 기능동작검증을 업데이트한다.

즉, 구현체가 성장하고 확장됨에 따라 화이트박스방법의 유닛테스트도 같이 자라나게 된다.

이러한 과정을 통해 구현의 안정성을 챙길 수 있다.

If all of those methods are public, i.e. callable by the outside world, I'd definitely test all of them with a full set of tests. One good reason is that white-box tests are more brittle than black-box tests; if the implementation changes the public contract might change for some of those methods.

should unit tests be black box tests or white box tests?

여러 의견들 🗨️


“자주 변경되어야 하는 구현체라면 화이트박스보다 블랙박스가 낫지 않냐”는 의견에 많은 의견이 갈렸다.

아래는 그 중 하나이다.

확실히 블랙박스을 통해서 테스트 구현하면 쉽고 유지보수가 쉬운 건 사실인데

통합테스트로만 테스트를 관리해야하는 단점 역시 존재해서

테스트를 빠르게 실행하는 것은 힘든 부분이 있을 거라서

상황에 따라서 달라질 수 있다고 생각해요

너무 잦은 변경으로 테스트가 생산성을 저해한다면

블랙박스위주로 통합테스트만 작성하고

변경이 잦지 않은 안정적인 서비스에서는

단위 테스트 위주로 관리하는 것이 좋지 않을까? 합니다.

솔직히 “모든 구현에 대해 화이트건 블랙이건 둘 다 빈틈없이 짜임새 있게 작성되어야한다”고 생각한다.

하지만 “어느 것에 비중을 둘 것이냐”가 문제이다.

하나의 기능에 대해 여러 케이스가 있으니 말이다.

확장성과 개발생산성을 고려한다면 위 의견도 굉장히 공감가는 의견이라고 생각한다.

Reference 📔


Unit testing, Black-box testing and white box testing

should unit tests be black box tests or white box tests?

Does "Unit Testing" falls under white box or black box testing?

bliki: Unit Test

profile
내 코드로 세상이 더 나은 방향으로 나아갈 수 있기를

0개의 댓글