TDD 실무 레거시 적용 - 단위 테스트

크리링·2024년 3월 12일
0

TDD 정리

목록 보기
4/5
post-thumbnail

이전의 TDD를 공부한 내용을 토대로 실무에서 레거시 코드에 TDD를 적용해보자!!!



어떻게?

김창준님의 함께 자라기를 읽었었는데 그 책에서 나오는 내용 중 개인적인 공부와 업무를 따로 두지말라는 말이 있었는데, 이 말이 상당히 인상깊었다.
그래서 실천하고자 이전에 배웠던 TDD 내용들을 실무에서 적용해보려 한다.



팀원과의 상의

내가 속해있는 회사는 10명이 안되는 규모이고, 개발팀은 나포함 3명이다. 그러기에 소수의 인원만 설득하면 실행할 수 있다는 장점이 있어서 이 점을 살려보려고 노력하고 있다.

먼저 팀원과의 회의에서 TDD의 필요성에 대한 내용은 이전에도 나온 적이 있었다. 업데이트하고 반영하다가 다른 곳에서 나온 이슈를 잡을 수 없어요., 모든 기능을 QA 진행한다는 것은 불가능에 가까워요., 일부라도 테스트를 조금씩 만들 필요성이 있어보여요. 라는 논의가 나왔었고, 실무와 레거시 코드에 관련된 글 들을 많이 찾아보았고 상의 끝에 우리만의 TDD를 레거시 코드에 적용하는 방식을 정리했다.

  1. 돌아가는 코드는 굳이 건들지마라
  2. 기능 개발이나 업데이트 진행시 테스트 코드를 추가
  3. 모든 작업 단위의 마무리에는 리팩토링 (커밋할 때 서비스 구현 및 업데이트와 리팩토링을 분리해서 하지 않는다.)

큰 원칙은 위에 3가지이다.

굳이 TDD를 만들겠다고 모든 코드에 단위테스트를 만드는 것보다 실제 코드를 손 볼 일이 있을 때 테스트 코드를 추가하고, 리팩토링을 무조건 적용하는 방식으로 단위 테스트를 조금씩 늘려나가는 전략을 세웠다.



그렇다면 본격적인 나의 실무에서 레거시 코드에 TDD를 적용하는 방식을 공유해본다.




적용

레거시 배경 구현

실무 환경과 비슷한 블로그 환경으로 프로젝트를 만들어보았습니다.

깃허브 코드 참고

초기 패키지는 아래와 같이 설정했습니다.

클래스 다이어그램은 아래와 같습니다.

간단하게 코드를 부가 설명하자면

  • 실무에서 다대다 매핑을 사용하지 않습니다.
    • PostCategory 사이에 따로 매핑 테이블을 만들고 각각 1:N 매핑을 통해서 다대다를 대체하였습니다.
  • 모든 매핑은 지연로딩을 사용합니다.
  • @Setter 어노테이션을 사용하지 않고, @Builder 패턴을 사용하므로 필요한 데이터만 설정하고, 유연성 확보, 변경 가능성 최소화, 높은 가독성을 추구합니다.

그렇다면 이제 실무 레거시 코드에 대해 알아보겠습니다.






실무 환경 기능 개발 및 테스트 코드

최대한 실무와 비슷한 코드를 만들려했지만 너무 많은 기능을 붙여야하기 때문에 요약했고
많은 각색이 들어간점 양해 부탁드립니다.

카테고리를 지정하고 댓글을 작성하면 카테고리공부 카테고리라면 장르 하위의 모든 게시글에 댓글이 달리고, 카테고리가 아니라면 모든 게시글 중 하나만 댓글이 달리는 기능이다.

간단하게 단위 테스트를 작성할 내용을 초기 체크리스트로 만들어보면

  • users 조회 안되는 경우
  • category 조회 안되는 경우
  • category가 공부인 경우
  • 카테고리가 공부가 아닌 경우

이 정도이다.
먼저, TDD의 기능 구현은 쉬운 로직에서 어려운 로직으로 가라했으니 쉬운 부분부터 구현한다.

코드에서 보면

빨간 테두리 부분인데 먼저 users가 조회 안되는 경우를 먼저 하려한다.
구현하기 전 좀 더 체크리스트를 구체적으로 생각해보면

  • users 조회 안되는 경우
    • userId가 null인 경우
    • userId가 없는 아이디인 경우

로 구체적으로 만들어주고, 단위 테스트를 만들어보자.

JUnit5로 테스트 파일을 먼저 만들고,

Mock으로 각 리포지토리를 reviewServiceImpl에 주입해주고

단위 테스트를 위와같이 만들어준다. (Assertionsorg.assertj.core.api.Assertions 라이브러리 사용)

실행해보면

초록불을 확인할 수 있다.

이미 코드에서 orElseThrow로 예외처리를 잘 해놔서 간단하게 초록불을 확인할 수 있다.

  • users 조회 안되는 경우
    • userId가 null인 경우
    • userId가 없는 아이디인 경우

이와 같이 userId가 없는 아이디인 경우도 만들어준다.

단위 테스트를 실행하면

단위 테스트 통과했으니 전체 실행해보면

모두 초록불을 확인

  • users 조회 안되는 경우
    • userId가 null인 경우
    • userId가 없는 아이디인 경우
  • category 조회 안되는 경우
  • category가 공부인 경우
  • 카테고리가 공부가 아닌 경우






한가지 큰 체크리스트를 끝내고, 위의 빨간 테두리 안에 포함되는 두번째 category가 조회 안되는 경우를 진행해본다.

세부적인 체크리스트를 만들어보면

  • category 조회 안되는 경우
    • category id가 null
    • category id가 없는 아이디인 경우

유저와 같이 두개 정도만 떠올라 체크리스트를 만들어주고, 추후 생각나는 경우는 그때그때 만들어주기로한다.

단위테스트를 작성하는데 Category 코드로 내려오기 위해서는 Users 조회 코드가 무사히 지나갈 수 있어야 한다. 그리고 Category 조회 코드에서 Exception이 발생하지만 후에 users가 필요하니 미리 MockData를 만들어주자.

@BeforeEach 어노테이션 통해 각 테스트 시작 전 MockData를 만들어주고자 한다.

만들어주는 과정에서 모든 entity의 아이디를 @GeneratedValue의 디폴트인 AUTO 전략을 가지고 자동으로 아이디를 자동으로 한개씩 증가하기 때문에 따로 @Builder 생성자 부분에 아이디를 넣지 않아 id를 지정할 수 가 없다.

이미 실무에서는 모든 엔티티에서 같은 전략으로 엔티티를 생성중이어서 전략을 바꿀 수 없고, 필요한 부분의 엔티티만 상속을 통해서 setId()를 할 수 있도록 만들어주었다.

부모 클래스를 만들어주고, 부모 클래스와 매핑은 하되, 상속을 통해 칼럼만 매핑정보로 제공받는 @MappedSuperclass 어노테이션을 사용한다.

그리고 기존 entity에 ID 어노테이션 칼럼은 지워주고 부모 클래스를 상속한다.

이렇게 기존 엔티티를 리팩토링해주고

카테고리 관련 단위 테스트를 만들어주면 초록불을 확인할 수 있다.

이제 초기 체크리스트 중 쉬운 부분을 테스트로 구현했고, 엔티티의 구조를 바꾸는 리팩토링도 진행했다. 다시 체크리스트를 보면

  • users 조회 안되는 경우
    • userId가 null인 경우
    • userId가 없는 아이디인 경우
  • category 조회 안되는 경우
    • category id가 null
    • category id가 없는 아이디인 경우
  • category가 공부인 경우
  • 카테고리가 공부가 아닌 경우






이제 다음으로 category가 공부인 경우를 단위 테스트 작성해보자


코드에서는 빨간 네모 부분이다.

assert 부분은 reviewCountreviewList로 확인하고,
세부적인 체크리스트를 만들어보면

  • category가 공부인 경우
    • mapCategoryAndPostList가 없는 경우
    • mapCategoryAndPost가 있는 경우

mapCategoryAndPostList가 없는 경우 먼저 만들어보면
처음에 category를 통해 MapCategoryAndPostRepository에서 findById를 사용하므로 카테고리를 먼저 만드는 메소드를 만들어주고 Mock을 해주었다.

mapCategoryAndPostList 만 비어있으면 되므로 코드의 오류가 발생하지 않아 보여 이렇게 작성하고, 실행시키면

따로 예외처리 하지 않아도 정상 작동되는 레거시였기 때문에 그냥 바로 초록불을 확인할 수 있다.

간단하게 아래 then 부분만 리팩토링해보면

이렇게 만들고 전체 테스트에서 초록불을 확인한 후 다음 체크리스트로 넘어간다.

  • 카테고리가 공부이고 mapCategoryAndPost가 있는 경우

일단 Mock 데이터를 만들어보면

Post -> Review 생성 -> ReviewList 생성 
-> Post에 ReviewList 업데이트 -> Category와 Post Map 생성 
-> Category와 Post Map List 생성

단위 테스트 코드를 작성하면

Post 두개니까 Review 두개 생성되게 Assert문을 작성하고 실행하면

전체 테스트 모두 초록불을 확인하고 편안해진다.

  • users 조회 안되는 경우
    • userId가 null인 경우
    • userId가 없는 아이디인 경우
  • category 조회 안되는 경우
    • category id가 null
    • category id가 없는 아이디인 경우
  • category가 공부인 경우
    • mapCategoryAndPostList가 없는 경우
    • mapCategoryAndPost가 있는 경우
  • 카테고리가 공부가 아닌 경우






마지막으로 카테고리가 공부가 아닌 경우를 테스트 해보자

체크리스트를 좀 구체화해보면 category가 공부인 경우와 동일해 보인다.

  • 카테고리가 공부가 아닌 경우
    • mapCategoryAndPostList가 없는 경우
    • mapCategoryAndPost가 있는 경우

먼저 두개의 체크 사항이 카테고리가 공부인 경우와 비슷하니
일단 복사하고, categoryId와 예상되는 Assert 결과를 바꿔준다.

둘 다 실행해보면

mapCategoryAndPostList가 없는 경우는 초록불

mapCategoryAndPost가 있는 경우는 초록불을 확인할 수 있다.

원인을 찾아보면

카테고리 아이디가 1L이어서 else를 타지 못하는 것으로 여겨져
테스트 코드를 리팩토링해준다.

categoryId1L이 아닌 다른 아이디로 설정해주어서 다시 실행해보면

초록불을 확인했고
전체 테스트 실행해보면

모두 초록불을 확인할 수 있다.

체크리스트를 다시 확인해보면

  • users 조회 안되는 경우
    • userId가 null인 경우
    • userId가 없는 아이디인 경우
  • category 조회 안되는 경우
    • category id가 null
    • category id가 없는 아이디인 경우
  • category가 공부인 경우
  • 카테고리가 공부가 아닌 경우

모든 체크리스트를 완수했다.

이제 서비스 로직의 리팩토링을 해보자.






분량 조절 실패로 리팩토링은 다음 글에 남기겠습니다.



참고 : 개발일상 - 레거시에 통합 테스트 붙이기,
레거시 코드에 테스트코드 추가와 리팩토링하기,
Testing and Refactoring Legacy Code
[Java] 빌더 패턴(Builder Pattern)을 사용해야 하는 이유
@ManyToMany를 사용하면 안 되는 이유

0개의 댓글