[Spring Boot] TDD : JUnit 테스트 코드 작성 + Assert 메서드 정리

2024. 3. 5. 13:31
반응형

 

 

요즘 프로그램을 작성할 때, 테스트 코드를 작성하는 것은 매우 필수적이다.

이번 포스팅에서는 TDD(테스트 주도 개발)과 단위 테스트의 개념에 대해서 살펴보고, JUnit을 통해 단위 테스트 코드를 작성하는 방법을 알아보려고 한다.

 

<다룰 내용>
1. 개발 방법론 : 전통적 방식의 한계와 TDD의 등장 및 개념
2. 테스트의 종류 : 단위 테스트, 통합 테스트, 부하 테스트
3. 테스트의 규칙과 JUnit 소개
4. 테스트 코드의 기본적인 작성 방법 및 구조, 실행 방법
5. 테스트 코드 작성 프로세스 : 요구사항에 따라 테스트 케이스를 정의한 후, 개발 -> 테스트 코드를 작성하는 방법
6. 다양한 Assert 함수 및 AssertJ 함수 살펴보기 : 값 비교, 대소 비교, 문자열 비교, Null 검증, 조건 검증, 컬렉션 조건 검증, 포함 여부 검증, 필드 추출, 객체 비교, 예외 검증 등

 

 


개발 방법론

 

전통적 방식의 한계

기존에는 프로젝트 전체에 대한 철저한 계획을 구상한 뒤, 장기간에 걸쳐 많은 비용을 투입하여 개발을 완수하였다. 이러한 계획주도 개발 방식은 소프트웨어의 규모가 커지고 복잡해짐에 따라서 한계에 봉착하게 되었다. 개발하는 인간은 언제나 완벽할 수 없었으며, 소프트웨어는 예측 불가능하며, 요구사항은 점차 유동적이고 복잡해져 갔다.

 

애자일 프로그래밍(Agile programming)과 익스트림 프로그래밍(eXtream Programming, XP)

소프트웨어 개발 방법론 중 하나로, 빠르게 변화하는 요구 사항에 대응하기 위해 유연하고 적응성 있는 접근 방식을 제공한다. 애자일 프로그래밍은 전통적인 방식의 소프트웨어 개발에서 발생하는 비효율성과 불필요한 문서 작성, 엄격한 계획에 대한 의존을 줄이려고 했다.

미래에 대한 예측 대신, 지속적으로 프로토타입을 완성하고, 이 때, 소단위 요구사항을 덧붙여서 점차 확장해나가는 수평적으로 개발하는 방법론이다.

이 때, 작은 실험과 피드백이라는 개념이 생겨났으며, 이는 작은 단위의 실험을 통해서 가설을 검증하고 피드백을 통해 프로토타입을 지속적으로 개선하는 방법이다.

 

익스트림 프로그래밍은 애자일 프로그래밍 개발 방법론 중 하나이다. 

주요 특징으로는 짧은 개발 주기, 간단한 설계, 테스트 주도 개발(TDD), 지속적 통합(CI) 등이 있을 수 있다.

 

 

TDD(Test-Driven Development, 테스트 주도 개발)

 

TDD는 익스트림 프로그래밍의 주요 특징 중 하나이다. 기존에는 개발이 이루어진 다음 테스트 케이스 작성 및 테스트를 작성했다.

반면 테스트 주도 방법은 테스트 케이스를 먼저 작성한 다음 테스트 케이스에 따라서 실제 개발을 수행하는 방법이다.

작은 단위의 기능을 테스트하고 작성하기 때문에 피드백 주기가 짧으며 코드가 변경되어도 테스트를 수행하여 기존 요구사항에 대한 영향을 파악하고, 오류를 방지할 수 있다. 따라서 코드의 품질이 향상되고, 유지보수성이 향상된다.

 

 


테스트의 종류

 

단위 테스트(Unit Test)

  • 소프트웨어의 가장 작은 단위인 모듈, 함수, 클래스 등의 개별적인 기능을 테스트하는 것
  • 주로 프로그래머가 작성하며, 코드의 동작을 검증하고 예상대로 동작하는지 확인한다.
  • 코드 변경 시에 기존 코드의 충돌 및 오류를 발견 가능하며, 새로운 코드의 동작의 성공 여부를 확인할 수 있다. 
  • TDD에서의 테스트 케이스는 주로 단위 테스트 작성을 의미한다. 그리고 주로 자동화되어 사용된다(CI).
    • CI, Continuous Integration (지속적 통합): 개발자들이 작업한 코드를 지속적으로 통합하여 통합 문제를 최소화하고, 코드 변경이 일어날 때마다 자동으로 빌드 및 테스트가 실행되며, 코드베이스에 통합시키도록 한다.
    • CD, Continuous Deployment (지속적 배포) : CI를 통과한 이후, 자동화된 프로세스를 통해 테스트를 통과한 코드를 실제 환경으로 자동으로 프로덕션 환경에 배포하는 파이프라인 구축을 목적으로 한다.

 

통합 테스트(Integration Test)

  • 각각의 모듈이 제대로 상호작용하는지, 시스템의 구성 요소들이 올바르게 통합되어 동작하는지를 확인한다.
  • 즉 이름과 같이 독립적인 기능이 아닌, 모듈 간의 연계되어 동작하는 것을 확인하는 것이다.

 

부하 테스트(Stress Test)

  • 소프트웨어가 예상되는 최대 부하를 견딜 수 있는지를 검증한다.
  • 시스템에 과도한 부하를 가하여 응답 시간, 처리량, 자원 사용량 등을 측정하고, 성능 문제를 파악 및 최적화할 수 있다.

 


테스트 작성 규칙

 

좋은 테스트 코드는 FIRST라는 규칙을 따른다고 한다.

  1. Fast (빠르게): 빠르게 실행되어야 한다. 테스트가 느리면 개발자들이 자주 실행하지 않을 수 있다.
  2. Isolated (독립적으로): 한 테스트의 실패가 다른 테스트에 영향을 주어서는 안 된다.
  3. Repeatable (반복 가능하게): 동일한 환경에서 여러 번 실행해도 일관된 결과를 보여야 한다.
  4. Self-Validating (자가 검증적으로): 테스트는 성공 또는 실패를 명확하게 보여줘야 하며, 자동화되어야 한다.
  5. Timely (적시에): 테스트하려는 코드를 구현하기 이전에 구현해야 한다.

 

또한 좋은 테스트 코드는 1가지 테스트에 1가지의 개념만을 테스트하는 것이 좋다.

 

 

 


JUnit이란?

 

자바를 위한 단위 테스트 프레임워크이다.  메소드, 클래스 및 패키지에 대한 단위 테스트를 작성하는 데 자주 사용된다 이는 TDD 방법론을 따르는 개발 프로세스에서 특히 유용하게 사용된다.

 

  • 간단하고 사용하기 쉬운 API를 제공하며, 테스트 케이스를 작성하고 실행하는 데 필요한 다양한 어노테이션 및 메서드를 제공한다.
  • 테스트의 성공, 실패 또는 예외 처리와 같은 결과를 자동으로 보고한다.
  • 개발자는 코드를 작성하기 전에 해당 코드의 동작을 정의하는 테스트 케이스를 작성하고, 이 테스트 케이스를 사용하여 코드를 구현하고 테스트한다. 이를 통해 코드의 품질을 향상시키고 버그를 빠르게 발견할 수 있다.

 

 

 

 

 


테스트 코드 기본

 

 

Spring Boot에서 테스트 코드는 주로 프레임워크에서 test 코드를 작성하는 디렉터리로 src/test/java에 작성한다.

여기에 테스트 클래스를 하나 생성해주자.

 

 

 

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;



@SpringBootTest
@Transactional // 각각의 테스트 메서드에 대해 트랜잭션을 시작하고, 테스트가 종료되면 롤백
class TagServiceTest {


}

 

기본적인 테스트 코드의 작성 틀이다.

 

@SpringBootTest 어노테이션은 Spring Boot 애플리케이션을 테스트하기 위한 환경을 설정한다. 이는 스프링의 ApplicationContext를 로드해주어, 테스트 환경에서도 스프링 환경과 Bean들을 주입받아서 사용할 수 있게 된다.

 

@Transactional 어노테이션은 각각의 테스트 메서드에 대해 트랜잭션을 시작하고, 테스트가 종료되면 롤백한다. 이렇게 하면 데이터베이스 등에 테스트 코드의 내용이 반영되지 않으며 각각의 테스트가 독립적으로 실행되고 서로의 상태에 영향을 주지 않게 된다.

 

 

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;



@SpringBootTest
@Transactional // 각각의 테스트 메서드에 대해 트랜잭션을 시작하고, 테스트가 종료되면 롤백
class TagServiceTest {

    @BeforeEach
    public void before(){
        System.out.println("Test Before");
    }

    @AfterEach
    public void after(){
        System.out.println("Test After");
    }


}

 

@BeforeEach@AfterEach 어노테이션을 사용하여 테스트 전후 처리를 할 수 있다.

각각의 테스트 메서드마다 적용되어, 실행되기 전과 후에 실행될 코드를 정의하여 필요한 초기화 작업이나 마무리 작업을 작성할 수 있다.

 

 

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Transactional // 각각의 테스트 메서드에 대해 트랜잭션을 시작하고, 테스트가 종료되면 롤백
class TagServiceTest {

    @Autowired
    private TestService testService;

    @BeforeEach
    public void before(){
        System.out.println("Test Before");
    }

    @AfterEach
    public void after(){
        System.out.println("Test After");
    }

    @Test
    @DisplayName("두 수가 일치해야 성공")
    public void test() throws Exception {
        // Given
        int number = 0;
        System.out.println("Given");

        // When
        int generateNumber = testService.generate();
        System.out.println("When");

        // Then
        assertThat(generateNumber).isEqualTo(number);
        System.out.println("Then");
    }

    @Test
    @DisplayName("두 수가 일치하지 않아야 성공")
    public void test2() throws Exception {
        // Given
        int number = 0;
        System.out.println("Given");

        // When
        int generateNumber = testService.generate();
        System.out.println("When");

        // Then
        assertThat(generateNumber).isNotEqualTo(number);
        System.out.println("Then");
    }

}
@Service
public class TestService {
    public int generate(){
        return 0;
    }
}

 

@Test 어노테이션을 사용하여 테스트 메서드를 정의한다. 각 테스트 메서드는 테스트 케이스를 나타내며, 테스트할 기능을 작성한다.

@DisplayName 어노테이션은 테스트 메서드의 이름을 지정할 수 있다.

 

테스트 메서드의 구성은 Given, When, Then으로 구성된다.

Given: 주어진 상황을 설정하는 부분. 

When: 테스트할 기능을 호출하고 실행하는 부분. 

Then: 기대한 결과를 검증하는 부분. 특정한 결과나 동작이 예상대로 수행되었는지 확인한다.

 

위의 테스트는 Given으로 주어진 0과, generate() 메서드로 생성된 정수가 일치하는 지의 여부를 확인하는 코드이다.

아래의 테스트는 반대로 일치하지 않는지의 여부를 확인하는 코드이다.

따라서 위의 테스트는 성공할 것이고, 아래의 테스트는 실패하여 통과되지 못할 것이다.

 

 


테스트 코드 실행

 

 

왼쪽 사진 : 테스트 클래스의 @Test를 전부 실행시키려면 클래스 단위로 Run 시키면 된다.

오른쪽 사진 : 테스트 클래스의 @Test를 하나만 실행시키려면 메서드를 우클릭한 후 Run 시키면 된다.

 

 

 

InteliJ에서 테스트 결과를 확인할 수 있다. 다음과 같이 테스트가 통과한 것과, 통과하지 못한 것들을 확인할 수 있다.


단위 테스트 코드 작성 프로세스 살펴보기

 

이제 실제 요구사항에 따른 단위 테스트 코드를 작성해보자.

 

 


테스트 케이스 작성

 

태그에 관련된 요구사항에 대해서 테스트 코드를 작성할 것이다. 요구사항은 다음과 같다.

1. 회원은 자기 소유의 태그 리스트를 가진다.

2. 회원은 태그 이름과 태그 내용을 지정하여 추가할 수 있다.

3. 다른 회원간은 상관 없지만, 같은 회원 소유의 동일한 이름의 태그를 추가할 수 없다.

4. 회원이 자신이 소유한 태그 리스트를 조회 가능하다.

@SpringBootTest
@Transactional 
class TagServiceTest {


    @BeforeEach
    public void before(){

    }

    @Test
    @DisplayName("태그 추가 성공")
    public void successTagInsert() throws Exception {

    }

    @Test
    @DisplayName("태그 추가 실패 : 존재하지 않는 회원")
    public void failTagInsertCaseNotExistMember() throws Exception {

    }

    @Test
    @DisplayName("태그 추가 실패 : 해당 회원에 중복된 태그이름이 존재")
    public void failTagInsertCaseDuplicateName() throws Exception {

    }

    @Test
    @DisplayName("태그 조회 : 해당 회원 소유의 태그가 1개일 때 조회 성공")
    public void successTagSelectByMemberV1() throws Exception {

    }
    
    @Test
    @DisplayName("태그들 조회 : 해당 회원 소유의 태그가 여러개일 경우 태그 리스트 조회 성공")
    public void successTagSelectByMemberV2() throws Exception {
        
    }


}

 

TDD 개발 방법론에 따라서, 실제 코드를 작성하기 전에 테스트 케이스를 먼저 작성하였다. 요구사항에 맞추어 테스트 메서드를 작성해주자.

 


실제 개발 수행

 

@Service
@AllArgsConstructor
@Transactional(readOnly = true)
public class TagServiceImpl implements TagService{

    private final TagRepository tagRepository;
    private final MemberRepository memberRepository;
    private final ModelMapper modelMapper;
    private final PasswordEncoder encoder;

    @Override
    @Transactional
    public Long addTag(TagRequestDto inputTag, Long memberId) {
        Member member = memberRepository.findMemberByMemberId(memberId);
        if(member == null){
            throw new NoSuchElementFoundException("존재하지 않는 회원입니다");
        }
        if(tagRepository.isExistByMemberOwnTagName(memberId, inputTag.getName())){
            throw new AlreadyExistElementException("이미 회원에게 존재하는 태그 이름입니다.");
        }
        Tag tag = modelMapper.map(inputTag, Tag.class);
        tag.setMember(member);
        Tag result = tagRepository.save(tag);
        return result.getTagId();
    }

    @Override
    public MemberWithTagDto getTagInfoByMember(Long memberId) {
        Member member = memberRepository.findAllLeftFetchJoin(memberId);
        MemberWithTagDto dto = modelMapper.map(member, MemberWithTagDto.class);
        return dto;
    }

}

 

테스트 케이스와 비즈니스 로직에 맞는 실제 개발을 수행하였다. 서비스에 태그를 Save하는 기능과, 멤버 ID로 소유한 태그들을 모두 Select하는 코드를 작성해주었다.

 

 


테스트 코드 작성

 

@SpringBootTest
@Transactional // 각각의 테스트 메서드에 대해 트랜잭션을 시작하고, 테스트가 종료되면 롤백
class TagServiceTest {

    @Autowired
    private TagService tagService;
    @Autowired
    private MemberService memberService;
    @Autowired
    private EntityManager em;

    private Long memberId1;
    private Long memberId2;

    @BeforeEach
    public void before(){
        MemberJoinRequestDto member = new MemberJoinRequestDto("hong", "홍길동", "1111");
        MemberJoinRequestDto member2 = new MemberJoinRequestDto("kim", "김길동", "1111");
        memberId1 = memberService.addMember(member);
        memberId2 = memberService.addMember(member2);
    }

    @Test
    @DisplayName("태그 추가 성공")
    public void successTagInsert() throws Exception {

    }

    @Test
    @DisplayName("태그 추가 실패 : 존재하지 않는 회원")
    public void failTagInsertCaseNotExistMember() throws Exception {

    }

    @Test
    @DisplayName("태그 추가 실패 : 해당 회원에 중복된 태그이름이 존재")
    public void failTagInsertCaseDuplicateName() throws Exception {

    }

    @Test
    @DisplayName("태그 조회 : 해당 회원 소유의 태그가 1개일 때 조회 성공")
    public void successTagSelectByMemberV1() throws Exception {

    }

    @Test
    @DisplayName("태그들 조회 : 해당 회원 소유의 태그가 여러개일 경우 태그 리스트 조회 성공")
    public void successTagSelectByMemberV2() throws Exception {

    }


}

 

테스트를 위해서 사전에 필요한 작업들을 수행해주자. 

1. TagService 등의 필요한 모듈들을 @Autowired를 통해서 주입시켜 주었다.

2. 모든 테스트 코드에 대해서 @BeforeEach를 통해서 사전에 Member 데이터를 준비해 두었다.

 

 


테스트 코드 : 태그 추가 성공 여부 확인

@Test
@DisplayName("태그 추가 성공")
public void successTagInsert() throws Exception {
    // Given
    TagRequestDto request = new TagRequestDto("태그 이름", "태그 컨텐츠");

    // When
    Long insertTagId = tagService.addTag(request, memberId1);

    // Then
    assertThat(insertTagId).isNotNull();
}

 

Service의 태그 추가 기능을 테스트하는 코드를 작성하였다.

assertThat(actual).isNull(expect) : 기대값(actual)이 Null이 아닌 경우 테스트를 통과시키는 기능이다.

따라서 태그가 정상적으로 추가되었다면, Null이 아닐 것이므로 해당 테스트를 통과할 것이다.

 

 


테스트 코드 : 태그 추가 실패 : 존재하지 않는 회원 케이스

@Test
@DisplayName("태그 추가 실패 : 존재하지 않는 회원")
public void failTagInsertCaseNotExistMember() throws Exception {
    // Given
    TagRequestDto request = new TagRequestDto("태그 이름", "태그 컨텐츠");

    // When & Then
    assertThrows(NoSuchElementFoundException.class, () -> {
        Long insertTagId = tagService.addTag(request, Long.MAX_VALUE);
    });
}

 

이번에는 Service의 태그 추가 기능 시, 태그를 소유한 회원 ID에 대한 파라미터가 잘못 들어왔을 경우를 테스트해본다.

assertThrows(SomeException.class, () -> code) : 해당 코드에서 임의의 SomeException이 발생 시 테스트를 통과하게 된다.

유저가 존재하지 않는다면 Service는 NoSuchElementFoundException을 throw하므로, 테스트가 통과할 것이다.

 

 


테스트 코드 : 태그 추가 실패 : 중복된 이름의 태그 존재

@Test
@DisplayName("태그 추가 실패 : 해당 회원에 중복된 태그이름이 존재")
public void failTagInsertCaseDuplicateName() throws Exception {
    // Given
    TagRequestDto exist = new TagRequestDto("중복 태그", "태그 컨텐츠");
    Long existTagId = tagService.addTag(exist, memberId1);
    TagRequestDto request = new TagRequestDto("중복 태그", "태그 컨텐츠");

    // When & Then
    assertThrows(AlreadyExistElementException.class, () -> {
        Long insertTagId = tagService.addTag(request, memberId1);
    });
}

 

마찬가지로 중복된 태그이름에 대해서도 예외가 발생하는 경우에 대한 케이스를 작성해두고, 예외가 발생 시 통과시키는 단위 테스트 코드를 작성시켜주었다.

 

 

 


테스트 코드 : 태그 조회 : 해당 회원 소유의 태그가 단일일 경우 조회 성공

@Test
@DisplayName("태그 조회 : 해당 회원에 해당하는 태그 조회 성공")
public void successTagSelectByMemberV1() throws Exception {
    // Given
    TagRequestDto tag1 = new TagRequestDto("태그1", "태그 컨텐츠");
    TagRequestDto tag2 = new TagRequestDto("태그2", "태그 컨텐츠");
    Long tag1Id = tagService.addTag(tag1, memberId1);
    Long tag2Id = tagService.addTag(tag2, memberId2);


    // 엔티티를 추가한 후 영속성 컨텍스트를 비우고 변경사항을 즉시 데이터베이스에 반영 ->
    // when에서 영속성 컨텍스트에 존재하지 않는 엔티티를 사용하지 않고, 실제로 데이터베이스에 존재하는 엔티티를 조회
    em.clear();
    em.flush();

    // When
    MemberWithTagDto result = tagService.getTagInfoByMember(memberId1);

    // Then
    assertThat(result.getTags().size()).isEqualTo(1);
    assertThat(result.getTags().get(0).getTagId()).isEqualTo(tag1Id);
    assertThat(result.getTags().get(0).getTagId()).isNotEqualTo(tag2Id);

}

 

회원 ID로 서비스를 호출 했을 때 회원 소유의 태그를 성공적으로 Read하는지를 테스트하는 코드이다.

해당 코드는 주어진 상황(Given)은, Tag1은 Member1의 소유이고, Tag2는 Member2의 소유이다.

이 때(When) Member1이 소유한 태그를 조회 시 아래의 내용들을 검증한다.

 

1. Member1가 소유한 태그의 개수가 1개라면 통과

2. Member1의 소유한 1개의 태그가 Tag1이면 통과

3. Member1이 소유한 1개의 태그가 Tag2가 아니면 통과

 

여기서 Given 시 중요한 점이 있다. em.clear()와 flush()를 도중에 사용한 것이 보일 것이다.

 

이는 엔티티를 추가한 후 영속성 컨텍스트를 비우고 변경사항을 즉시 데이터베이스에 반영하기 위해서 작성한 것이다.

기본적으로 트랜잭션을 커밋할 때에는 자동으로 Flush 되어 해당 코드를 작성하지 않아도 문제가 없었다. 그렇지만 해당 코드는 같은 컨텍스트 안에서 persist(save) 이후, find()를 호출하는 경우이다.

따라서 이를 작성하지 않으면 when의 getTagInfoByMember()은 데이터베이스를 조회하는 것이 아닌, 영속성 컨텍스트에 있는 데이터를 가져오기 때문에 Given의 내용을 조회할 수 없다.


em.clear(), em.flush()를 호출하게 되면 DB에 데이터를 지우고, 영속성 컨텍스트를 지우게 된다. 따라서 영속성 컨텍스트에 존재하지 않는 엔티티를 사용하지 않고, 실제로 데이터베이스에 존재하는 엔티티를 조회하게 된다.

영속성 컨텍스트에 대한 내용은 아래의 글을 참고하면 좋을 것 같다.

https://sjh9708.tistory.com/65

 

[Spring Boot/JPA] 영속성 컨텍스트와 준영속 컨텍스트

영속성 컨텍스트 JPA에서 영속성 컨텍스트는 엔티티를 관리하는 논리적인 개념 영속성 컨텍스트는 어플리케이션과 DB사이에서 객체를 보관하는 가상의 DB같은 역할을 한다 엔티티를 메모리에 저

sjh9708.tistory.com

 


테스트 코드 : 태그 조회 : 해당 회원 소유의 태그가 여러개일 경우 리스트 조회 성공

 

@Test
@DisplayName("태그들 조회 : 해당 회원에 해당하는 태그들 조회 성공")
public void successTagSelectByMemberV2() throws Exception {
    // Given
    TagRequestDto tag1 = new TagRequestDto("태그1", "태그 컨텐츠");
    Long tag1Id = tagService.addTag(tag1, memberId1);
    TagRequestDto tag2 = new TagRequestDto("태그2", "태그 컨텐츠");
    Long tag2Id = tagService.addTag(tag2, memberId1);

    em.clear();
    em.flush();

    // When
    MemberWithTagDto result = tagService.getTagInfoByMember(memberId1);

    // Then
    assertThat(result.getTags().get(0).getTagId()).isEqualTo(tag1Id);
    assertThat(result.getTags().get(1).getTagId()).isEqualTo(tag2Id);

}

 

마찬가지로 회원 ID로 서비스를 호출 했을 때 회원 소유의 복수의 태그들을 성공적으로 Read하는지를 테스트하는 코드이다.

해당 코드는 주어진 상황(Given)은, Tag1, Tag2가 Member1의 소유이다.

이 때(When) Member1이 소유한 태그를 조회 시 (Then) 두 개의 태그가 각각 Tag1, Tag2와 일치하는 지 검증한다.

 

 

 

// Then
// assertThat(result.getTags().get(0).getTagId()).isEqualTo(tag1Id);
// assertThat(result.getTags().get(1).getTagId()).isEqualTo(tag2Id);

assertThat(result.getTags())
        .extracting(e -> e.getTagId())
        .contains(tag1Id, tag2Id);

 

위의 assert 코드를 Equals가 아닌 포함 여부를 검증하는 코드로 다음과 같이 작성할 수도 있다. 

다음 코드는 getTags()로 불러 온 Tag List를 extracting() 메서드를 통해서 특정 필드를 추출한 후, 해당 필드의 리스트에 비교 대상들이 포함되었는지를 contains()를 통해 검증한다.

 

 

// Then
// assertThat(result.getTags().get(0).getTagId()).isEqualTo(tag1Id);
// assertThat(result.getTags().get(1).getTagId()).isEqualTo(tag2Id);

// assertThat(result.getTags())
//        .extracting(e -> e.getTagId())
//        .contains(tag1Id, tag2Id);

assertThat(result.getTags())
        .extracting(TagDto::getTagId)
        .contains(tag1Id, tag2Id);

 

위의 코드를 메서드 참조 연산 문법을 사용해서 다르게 표현할 수도 있다. 이는 람다식을 간결하게 표현하기 위한 방법 중 하나이다.

TagDto :: getTagId는 TagDto 클래스의 getTagID() 메서드를 Element에 대해서 수행하는 연산이다.

 

 


자주 사용되는 Assert 메서드

Assert는 테스트 코드에서 특정 조건이 참인지를 검증하는 데 사용되는 메서드들의 모음이었다.

자주 사용되는 Assert 메서드들에 대해서 정리해보겠다.

 

 

Assert 메서드

assert 메서드들은 JUnit과 같은 테스트 프레임워크에서 제공된다.

주로 단언문(assertion)을 통해 테스트 결과를 확인하는 데 사용된다.

 

값 비교 : assertEquals

int expected = 5;
int actual = someMethodThatReturnsFive();
assertEquals(expected, actual);

 

예상 값(expected)와 실제 값(actual)이 같으면 통과

 

 

객체 비교 : assertSame

String expected = "hello";
String actual = "hello";
assertSame(expected, actual);

 

예상 값(expected)와 실제 값(actual)이 동일한 객체를 참조하면 통과

 

 

조건 검증 : assertTrue

boolean condition = someCondition();
assertTrue(condition);

 

주어진 조건이 참이면 통과

 

 

Null 검증 : assertNotNull

Object obj = someMethodThatReturnsNonNullObject();
assertNotNull(obj);

 

인스턴스가 Null이 아니면 통과

 

 

예외 검증 : assertThrows

assertThrows(ArithmeticException.class, () -> divide(5, 0));

 

내부의 익명 함수(람다식)에서 해당 Excpetion이 발생하면 통과

 

 

 


 

AssertThat 

 

assertThat 메서드는 AssertJ와 같은 라이브러리에서 제공된다.

다양한 Matcher들과 함께 사용된다. Matcher들은 예상되는 조건을 지정하여 실제 값과 비교하고 검증하는 데 사용하며  좀 더 풍부하고 가독성이 높은 검증 메서드를 작성할 수 있다.

 

 

다양한 AssertJ 메서드

 

값 비교

assertThat(actual).isEqualTo(expected);

 

isEqualTo() : Actual과 Expeted의 값이 동일한 지 비교

 

수치 대소 비교

assertThat(actual).isGreaterThan(5); // 5보다 큰지 여부
assertThat(actual).isBetween(1, 10); // 1과 10 사이에 있는지 여부

 

 

문자열 검증

String actualString = "Hello World";
assertThat(actualString).startsWith("Hello"); // 접두사 검증
assertThat(actualString).endsWith("World"); // 접미사 검증
assertThat(actualString).contains("Wo"); // 부분 문자열 포함 여부 검증

 

 

Null 검증

assertThat(actual).isNull(); // null 여부 검증
assertThat(actual).isNotNull(); // null이 아님을 검증

 

 

특정 조건을 만족하는 지 검증

assertThat(actual).matches(element -> element > 10);

 

matches() : 조건에 맞는 지를 검증한다.

 


컬렉션에서 조건에 맞는 Element 여부 검증

assertThat(actualList).anyMatch(element -> element > 10);

 

anyMatch() : 컬렉션에서 조건에 맞는 요소가 하나 이상 존재하는지 여부를 검증

 

 

컬렉션에서 포함 여부 검증

String[] actualList = {"재휘", "철수", "영희"};
assertThat(actualList).contains("철수", "영희");

 

contains() : 해당 컬렉션에 특정 요소가 포함되어 있는지의 여부를 검증

 

 

컬렉션 비교

assertThat(actualList).containsExactly(expectedArray); 
assertThat(actualList).containsAnyOf(expectedArray);

 

containsExactly() : 순서까지 정확히 일치
containsAnyOf() : 요소 중 일부 포함

 

 

비어있는 컬렉션 검증

assertThat(actualList).isEmpty(); // 비어있는지 여부 검증

 

 

필드 및 속성 추출

List<User> users = Arrays.asList(
    new User("재휘", 25),
    new User("철수", 30),
    new User("영희", 30)
);

assertThat(users)
    .extracting("name")
    .contains("영희", "철수");

 

extracting() : 객체나 컬렉션에서 특정 필드나 속성 값을 추출하여 검증할 때 사용.

 


객체 비교

assertThat(actual).isSameAs(expected); 
assertThat(actual).isEqualToComparingFieldByField(expected);

 

isSameAs() : 참조값 비교

EqualToComparingFieldByField() : 필드 값 비교

 

객체의 타입 검증

assertThat(actual).isInstanceOf(ExpectedClass.class);

 

해당 Actual이 예상한 클래스의 인스턴스인지를 확인한다.

 

 

예외 검증

assertThatThrownBy(() -> someMethod()).isInstanceOf(SomeException.class);

 

assertThatThrownBy() : 내부의 익명 함수(람다식)에서 해당 Excpetion이 발생하면 통과

 

 

 

 


SpringBootTest와 Mockito

 

SpringBootTest는 단위 테스트에서 사용할 때 극복해야 할 면들이 있다.

해당 면모들을 보완하기 위해서 단위 테스트에서 JUnit을 확장시킨 테스트 라이브러리인 Mockito를 자주 사용한다.

 

@SpringBootTest의 장점

1. 실제 애플리케이션과 유사한 환경에서 테스트를 수행하여 실제 동작을 더 정확하게 확인할 수 있다.

2. 실제 빈을 사용하므로 통합 테스트를 수행하기에 적절하다.

 

@SpringBootTest의 단점

1. 실행 속도가 느리다

2. 외부 의존성에 의해 테스트가 명확하지 않다.

 

자세한 내용들은 아래 포스팅에서 SpringBootTest와 Mockito를 비교한 내용을 살펴보도록 하자.

 

 

<함께 보기> Mockito를 사용한 단위 테스트

https://sjh9708.tistory.com/219

 

[Spring Boot] TDD : Mockito를 사용한 단위 테스트

이전 포스팅에서 TDD(테스트 주도 개발)과 단위 테스트를 @SpringBootTest를 사용해서 작성해보았다.이번 포스팅에서는 테스트 코드를 작성할 때 많이 사용되는 Mockito를 사용해서 단위 테스트 코드를

sjh9708.tistory.com

 

 

 


References

 

https://mangkyu.tistory.com/143

 

[TDD] 단위 테스트(Unit Test) 작성의 필요성 (1/3)

1. 단위 테스트 vs 통합 테스트 차이 [ 단위 테스트(Unit Test) ] 단위 테스트(Unit Test)는 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트이다. 여기서 모듈은 애플리케이션에

mangkyu.tistory.com

 

반응형

BELATED ARTICLES

more