본문 바로가기

개발

단위 테스트 적용하기

수정 내역

24.10.31 easy-random 등을 이용한 fixture 개선 시도에 대한 내용 추가

24.11.19 assertion의 extract 체이닝 케이스 추가

목차

- 서론

- 프로젝트 특징

- 테스트 코드 분리

- 단위 테스트 구성

- 객체 찍어내기

- easy-random, fixutre monkey 도입

- service, repository 관리

- 실제 테스트 만들기

- assertThat을 여러 개 써도 될지?

서론

테스트 코드를 작성한 이유는 지금 맡은 유지 보수 업무를 보다 효과적으로 수행하고 싶었다.

기존에 작성된 코드가 잘 사용되지 않는 패턴으로 구성되어 있어 이해하는 데 어려움이 있었고

주석은 달아놓지만 여기서 놓치는 부분들을 테스트로 보완하고 싶었다.

 

또한, 우리 팀에 TDD 프로세스를 소개하고 적용하고 싶은 마음도 있다.

비록 TDD에 대한 이해는 부족하지만, 이런 노력들이 쌓이면 팀원들에게 레퍼런스를 제공할 수 있을 것이라 생각한다.

이를 위해 생각을 정리하고 공유하면, 더 발전된 테스트를 만들어 나갈 수 있을 것이라 믿는다.

프로젝트 특징

내가 TDD를 적용할 프로젝트들은 다음 특징을 가지고 있다.

  • 도메인 중심의 서비스(용어가 복잡하여 한글 변수명 사용 중)
  • B2B 서비스로 제한된 사용자만 쓰고 있음
  • 어떤 프로젝트는 폐쇄망에서 실행됨
  • B2C도 있지만, 트래픽이 많지 않음

현재로서는 트래픽이 많은 프로젝트에 대한 테스트 경험이 부족하지만,

시간이 지나면서 이러한 부분도 업데이트할 수 있을 것이다.

테스트 코드 분리

로직 수정하는 과정에서 테스트 코드에서 발생하는 오류들을 불편해하는 팀원들이 있었다.

런타임에 문제는 없지만, 팀원들이 더 큰 책임을 지고 있기에

이런 이슈들로 TDD에 대한 부정적인 인식을 주게 할 필요는 없다고 생각했다.

fork? branch?

깃 트리 관점에서 본다면 fork가 더 낫지 않을까라 생각했다.

나 혼자 테스트를 만들기 때문에 브랜치 하나로 충분하지만

다른 팀원들도 TDD를 시작하거나 여러 브랜치 pull을 하다 보면 깃 트리가 지저분해질 거란 우려가 든다.

 

지금은 브랜치를 만들어 엔티티에 관한 간단한 테스트 케이스를 몇 개 남겨둔 뒤, fork로 분리했다.

테스트 첫 단계에서는 테스트 전용 IDE를 하나 더 띄우는 수고 없이 브랜치 체크아웃 딸칵으로 레퍼런스를 제공하는 것도 좋겠다 생각했다.

 

단위 테스트 구성

친구 프로젝트를 많이 참고했다.

https://github.com/woowacourse-teams/2023-fun-eat

 

GitHub - woowacourse-teams/2023-fun-eat: 궁금해? 맛있을걸? 먹어봐 - 펀잇 🥄

궁금해? 맛있을걸? 먹어봐 - 펀잇 🥄. Contribute to woowacourse-teams/2023-fun-eat development by creating an account on GitHub.

github.com

 

객체 찍어내기

Fixture란 패키지를 만들어, 각 상황에 맞는 객체를 메소드 호출로 해결할 수 있도록 했다.

이를 통해 객체 생성 의도를 명확히 하고, 객체 생성에 필요한 라인을 줄여 테스트에 집중할 수 있다.

@SuppressWarnings("NonAsciiCharacters")
public class Shape_Fixutre{
    public static Shape 직사각형_만들기(int width, int height) {
        return new Shape(width, height);
    }

    public static Shape 정사각형_만들기(int width) {
        return new Shape(width);
    }
}

해치웠나?

갠플할 땐 이렇게 하면 됐었는데?

소규모로 처음부터 시작하는 부트 캠프 시절이나 먹혔지, 오랫동안 운영해온 코드에선 이 방법이 쉽지 않았다.

복합키 EmbeddedId는 기본이고 칼럼만 30개가 넘는 엔티티도 있어 객체 생성만 하는 것도 무척 힘들었다.

또한, 빌더 패턴으로 객체를 생성하고 있어 들여쓰기 하는 것도 힘들었다.

 

그래서 정적 팩토리 메서드를 만들어 공간만 차지하는 로직은 분리했다.

네이밍 컨벤션을 of로 해야 하지만 testOf로 지정해 테스트 전용이란 것을 명확하게 했다.

// before - builder 패턴만 씀
public static A testOf(Long id1, Long id2, Long id3) {
    return A.builder()
        .id(
            A_ID.builder()
                .id(
                    A_ID_ID.builder()
                        .id1(id1)
                        .id2(id2)
                        .build()
                )
                .id3(id3)
                .build()
        )
        .칼럼1("아무튼 칼럼이 너무 많아")
        .칼럼2("화면 절반을 차지하는")
        .칼럼N("빌더 코드")
        .build();
}

// after - 정적 팩토리 메서드 사용
public static 아무튼_복잡한_객체 testOf(Long id1, Long id2, Long id3) {
    // 빌더 코드는 A 객체 안으로 넣어놨다.
    return A.testOf(id1, id2, id3);
}

easy-random, fixutre monkey 도입

한 짤 요약

글또에서 갓갓한 현직자 분의 피드백을 받아 객체를 랜덤 생성하는 기법에 대해 알게 됐다.

easy-random 기준 단 2줄로 객체를 생성해 given을 간소화할 수 있었다.

개인 프로젝트 테스트 결과 매우 성공적이었지만, 실제 코드에선 실패했다. 이유는 다음과 같다.

 

1. 주소 데이터를 다룸

시도, 시군구 등 실제 데이터를 가지고 테스트를 진행해야 했기 때문에 랜덤 값이 들어오면 오히려 검증하기 어려웠다.

특히, 행정 구역 등 주소 데이터가 바뀌는 상황에 대한 테스트는 유의미한 성과는 없었다.

2. 도메인 특화

앞서 1번과 비슷한 원인이다. 사전에 정의된 값을 다뤄야 했기 때문에 데이터를 랜덤하게 넣는 것은 테스트 결과를 검증하는데 어려웠다.
좀 더 시도를 해봐야겠지만, 의미 있는 데이터 소수만 가지고 테스트를 만드는 것이 더 편했다.

service, repository 관리

테스트에 사용될 service, repository 같은 의존성들을 관리하는 클래스다.

여러 의존성이 필요할 때 테스트 클래스 상단이 길어지는 문제를 상속을 통해 해결했다.

@Transactional
@SpringBootTest
@SuppressWarnings("NonAsciiCharacters")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class UnitTest {

    @Autowired
    public Shape서비스 shape서비스;

    @Autowired
    public ShapeRepository shapeRepository;
}

단위 테스트가 필요한 테스트 클래스에 이 클래스를 상속한다.

@SuppressWarnings("NonAsciiCharacters")
public class ShapeTest extends UnitTest {

    @Test
    public void 아무튼_생성된_테스트 {
        // given
        Shape rectangle = 직사각형_만들기(1, 2);
        Shape squre = 정사각형_만들기(1);

        // when
        ...

        // then
        assertThat(...);
        ...
        assertThat(...);
    }

실제 테스트 만들기

???: 1:N이 출동하면 어떨까?

부트 캠프 때 해왔던 방식대로 하려니 제대로 되지 않았다.

사용자가 작업을 하기 위해선 여러 단계에 걸쳐 내용을 기입하는 구조여서 1:N 관계가 이어지는 경우들이 있었다.

그러다 보니 부모 객체 만드느라 각 테스트의 given에 20줄 이상씩 작성하는 것은 테스트 코드를 작성하는 의미가 없다 생각했다.

사진이 보이는 것보다 더 복잡하게 있음

따라서 메서드를 클래스로 변경하고, 입력에 필요한 데이터들을 @BeforeEach로 사전에 준비하기로 하였다.

게시글 수정 테스트를 예로 들자면, 여러 메서드를 클래스로 변환하여 구조를 개선하였다.

메서드 여러 개에서 클래스 여러 개로 변경

별것도 아니었잖아?

???: 그 테스트는 최약체였다.

기존에 테스트를 만들 때는 크게 세 가지로 구분하였다.

1. Service 테스트

2. Repository 테스트

3. 객체 내 비즈니스 로직 테스트

 

하지만, 2, 3번이 제대로 수행되지 않아 결국 구분 없이 통합시켰다.

모든 내용을 설명하기는 어렵지만 Java에서 Date 관련 객체가 여러 개 존재함에 따라 이들을 혼용하여 발생한 문제와

어떤 Repository는 프로시저 호출을 해 코드로 진행하기 어려웠다.

 

이 부분에서 테스트 코드 분리를 결심하게 된 이유이기도 한데, 특정 경우에서는 테스트 전용 메서드를 만들 필요가 있었다.
주석이나 메소드명을 통해 구분할 수 있더라도, 프로덕션 환경까지 올리는 것은 좋지 않다고 생각했다.

assertThat을 여러 개 써도 될지?

테스트당 하나의 assertThat을 쓰란 글이 있었지만, 여기에 얽매이지 않았다.

억지로 assertThat 하나씩 쓰면 테스트를 위한 테스트를 만드는 느낌이 들었다.

 

assertThat은 하나만 있어야 한다는 글과 이를 반박하는 글도 봤고 두 의견 모두 동의하지만,

테스트 제목 안에서 여러 내용을 유추할 수 있는 경우엔 assertThat을 여러 개 썼다.

여기서 글또분의 피드백을 통해 assertion의 extract 체이닝을 쓰면 assertThat 하나로 해결할 수 있단 것을 알게 됐다.

하나의 객체에서 여러 상태를 검증해야 할 때 유용하게 쓸 수 있다.

 

예를 들어 정사각형이 올바르게 생성됐는지 검증하는 부분이면

가로 세로 검증 테스트 클래스 2개 > 테스트 하나에 assertThat 1개

즉, 다음 형태가 더 직관적이다 생각했다.

    @Test
    public void 정사각형_생성테스트() {
        // given
        int len = 1;
        Shape squre = 정사각형_만들기(len);
        shape서비스.save(squre);

        // when
        Shape foundSquare = shapeRepository.find(square.getId());

        // then
        // 피드백 받은 코드
        assertThat(foundSquare)
            .extracting(Shape::getWidth, Shape::getHeight)
        	.containsExactly(len, len); // 넓이와 높이가 동일한지 확인
                       
        /* 피드백 받기 전 코드
        assertThat(squre.width).isEqualTo(len);
        assertThat(squre.height).isEqualTo(len);
        */
    }

 

'개발' 카테고리의 다른 글

파일 옮길 땐 tar를 쓰자  (0) 2024.11.19
ArrayBuffer, Blob  (0) 2024.11.09
[Java] p6spy 긴 바인딩 로그 치환하기  (0) 2024.09.04
[python] pandas csv 분할 중 .0 .1 문제  (0) 2024.08.29
proxyBeanMethods=false를 써야 하는지  (0) 2024.07.21