Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
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
Tags more
Archives
Today
Total
관리 메뉴

요리사에서 IT개발자로

테스트 코드 작성? (단위테스트 JUnit) 본문

Spring

테스트 코드 작성? (단위테스트 JUnit)

H.S-Backend 2024. 7. 8. 20:22

테스트 코드가 비즈니스 로직의 구현체에 대한 피드백을 주는것과 같다.

 

기존 비즈니스로직이 클래스 또는 Repository 등등 의존성이 너무 심한 경우 

 기본적으로

테스트 코드 작성을 하기에 비교적으로 용이하지 않다.

 

 또한 @Mock 을 비교적으로 편리하게 관리하게 하기 위한 방법은 따로 존재하지 않는다.

 

 비즈니스로직은 언제든지 바뀔 수 있기에 테스트코드가 깨질 수 는 있다.

슬라이스 테스트를 하는 이유

한 구간에서 모든것을 풀 테스트를 하게된다면

@Mock에 대한 의존성이 높아지기 때문이다.


Mock 가짜객체

테스트할 때 필요한 실제 객체와 동일한 모의 객체를 만들어 테스트의 효용성을 높이기 위해서 사용한다.

 

사용 경우
  • 실제 객체를 만들기에 비용과 시간이 많이 소요되는 경우
  • 의존성이 길게 걸쳐져 있어서 테스트를 제대로 구현하기 여러울 때
  • 테스트 작성을 위한 환경 구축이 어려운 경우
종유 및 사용방법
  • @Mock :특정 클래스 위에 선언하면 해당 클래스를 가짜 객체로 만들어준다.
  • @InjectMocks : @Mock으로 생성된 객체자동으로 Di 해주는 어노테이션이다. 
@Mock
private PostLikeRepository postLikeRepository;

@Mock
private UserRepository userRepository;

@Mock
private PostRepository postRepository;

@InjectMocks
private PostLikeServiceImpl postLikeService;

 

Mock을 사용하기 위해서는 

@ExtendWith(MockitoExtension.class)
class PostLikeServiceImplTest {
클래스에
@ExtendWith(MockitoExtension.class)를 붙어주어야한다.

 

@MockBean은 통합테스트에서 쓰인다.


단위테스트를 구성하는 방법

 

AAA패턴 사용

준비(Arrange), 실행(Act), 검증(Assert) 

 

익숙해지면 모든 테스트를 쉽게 읽을 수 있고 이해할 수 있기에
유지 보수 비용이 줄어든다.

public class Calculator {
    public int sum(int firstNum, int secondNum) {
        return firstNum + secondNum;
    }
}
class CalculatorTest {

    @Test
    void sum_of_two_numbers() {  // 단위 테스트 이름
        // Arrange (준비 구절)
        int firstNum = 10;
        int secondNum = 20;
        Calculator calculator = new Calculator();

        // Act (실행 구절)
        int result = calculator.sum(firstNum, secondNum);

        // Assert (검증 구절)
        assertThat(result).isEqualTo(30);
    }
}
준비구절

테스트 대상 시스템(SUT : System Under Test)과 관련된 의존성을 원하는 상태로 만든다.

실행 구절

SUT에서 메서드를 호출하고 의존성을 전달하며 출력 값을 캡쳐한다.

검증 구절

결과를 검증한다.

결과는 반환 값이나 SUT와 협력자의 최종상태, SUT가 협력자에 호출한 메서드 등으로 표시 될 수 있다.


Given -When-Then패턴

AAA와 똑같다고 보면된다

Given - 준비 구절

When - 실행 구절

Then - 검증 구절

테스트 구성 측면에서 차이는 없으나
비개발자에게는 Given - When - Then 구조가 더 읽기 쉽다.
@Test
@DisplayName("게시글 좋아요 삭제 성공 테스트")
void deleteLikeFromPost() {
    //given
    given(postRepository.findById(1L)).willReturn(Optional.of(post));
    given(postLikeRepository.findByPostAndUser(post, userDetails.getUser())).willReturn(
        Optional.of(postLike));

    //when
    postLikeService.deleteLikeFromPost(1L, userDetails);

    //Then
    assertThat(post.getCount()).isEqualTo(0L);
}

 

테스트 코드 작성중의 피해야 될 상황

1. 여러개의 준비, 실행, 검증 구절 피하기(통합테스트)

2. 테스트 내 if문 피하기(테스트 내의 또 테스트가 진행한다?)


준비 구절과 실행 구절

AAA패턴을 적용했을 경우 각 구절의 크기와 구성

준비구절이 가장 크다.

 

그러므로 같은 테스트 클래스 내 공개 메서드 또는 별도의 팩토리 클래스로 도출하는것이 좋다.

준비 구절에서 코드 재사용이 도움이되는 두가지 패턴에는

Object Mother와 Test Data Builder가 있다.

public class TestUsers {

    public static User aRegularUser() {
        return new User("John Smith", "jsmith", "42xcc", "ROLE_USER");
    }

    public static User anAdmin() {
        return new User("James Smith", "jsmith", "42xcc", "ROLE_ADMIN");
    }

    // other factory methods
    // ...
}

테스트 코드를 작성하다보면 똑같은 객체를 계속해서 생성해주어야 하는 상황이 발생할 것이다.

이것을

Object Mother를 사용하여 테스트 사용될 객체를 재사용함으로

데이터의 생성과 관리를 일관성 있게 할 수 있다.


Test Data Builder패턴은 

Builder를 활용하여 테스트 데이터를 생성하는 패턴

public classUserStubGenerator {

public static UserStub.BuildergenerateUserStub() {
           return UserStub.builder()
               .loginId("tid1234")
               .password("password1234@");
       }

   }

 

위와 같이 데이터를 만들고

// 사용처 (loginId 특수문자가 있을 시 실패하는 케이스)
User testUser = UserStubGenerator.generateUserStub()
    .loginId("@#($*$(@)")
    .toEntity();

 

해당 빌더를 재사용하여 Entity를 생성할 수 있으나

테스트 객체의 상태를 확인하기 어렵다는 단점이 있다.


실행구절

보통 코드 한줄로 이루어져 있고 두 줄 이상인 경우에는 SUT구성에 문제가 있을 수 있다.

@Test
void purchase_succeeds_when_enough_inventory() {
    // Given (준비 구절)
    Store store = new Store();
    store.AddInventory(Product.Shampoo, 10);
    Customer customer = new Customer();

    // When (실행 구절)
    boolean success = customer.purchase(store, Product.Shampoo, 5);  // 상품 구매
    store.removeInventory(success, Product.shampoo, 5);    // 재고 감소

    // Then  (검증 구절)
    assertThat(success).isTrue();
    assertThat(store.GetInventory(Product.Shampoo)).isEqualTo(5);
}

위의 테스트 코드의 문제점

상품 구매와 재고 감소가 동시에 있다.

단일 작업(구매 프로세스)을 수행하는데 두개의 메서드 호출이 필요하단것.

 

테스트 자체는 문제가 없을 수 있으나

단위 테스트는 구매 프로세스 라는 단일한 동작 단위를 검증하는 것이다.

 

해결책은 코드 캡슐화를 항상 지키는 것이다.

 

이처럼 단위 테스트 작성은 비즈니스 로직의 리팩토링 신호를 파악할 수 있다는 장점이 있다.

 

검증 구절

 

단위 테스트의 단위는 동작의 단위이다. 코드의 단위가 아니다.

단위 동작은 여러 결과를 낼 수 있고 모든 결과를 평가하는 것이 좋다.

// Assert v1
assertThat(customer.getId()).isEqualTo(1L);
assertThat(customer.getName()).isEqualTo("스파르타");
assertThat(customer.getPhone()).isEqualTo("010-1234-1234");
assertThat(customer.getBudget()).isEqualTo(5000);
assertThat(customer.getInventorySize()).isEqualTo(5);

 

위와 같은 방법보단 

// Assert v2
assertThat(customer.equals(afterPurchase)).isTrue();

테스트의 크기가 너무 커져버리면 테스트를 한눈에 파악 하기 어려워지기 때문에

필드가 너무 많다면 적절하게 객체를 정의하는 방법도 있다


SUT를 구별하기 위해서는 다른 클래스 들과 구분하는 것이 중요하다.

테스트에 개입하는 클래스들이 많다면 테스트 대상을 찾는것에 시간이 많이 들일 수 있기에

내가 테스트하고싶은 대상(클래스)의

변수명을 sut로 사용하는것이 좋다.

@Test
    void sum_of_two_numbers() {
        // Given
        int first = 10;
        int second = 20;
        Calculator sut = new Calculator();

        // When
        int result = sut.sum(first, second);

        // Then
        assertThat(result).isEqualTo(30);
    }
}

 

마지막으로

SUT를 구별하는 것이 중요하듯 테스트 내에서 특정 부분이 어떤 구절속에 있는 지 파악하기에 시간을 많이 들이지 않도록

각 구절을 구분하는 것이 중요하다.

 

AAA 또는 given-when-then패턴이 익숙해졌다면

빈줄로 분리하는 것이 코드량이 적고 가독성도 좋다.

 @Test
    void sum_of_two_numbers() {
        int first = 10;
        int second = 20;
        Calculator sut = new Calculator();

        int result = sut.sum(first, second);

        assertThat(result).isEqualTo(30);
    }
}

 

 

반응형