본문 바로가기
Study/Server 심화

2주차: TDD와 테스트 코드(2)

by jisu-jeong0 2024. 4. 9.

지난주에 이어 이번주에도 TDD와 테스트 코드에 대해 공부한다.

 

1주차: TDD와 테스트 코드(1)

테스트 코드? 단위 테스트 통합 테스트 좋은 테스트의 특징 TDD? TDD를 사용해야 하는 이유 TDD 작성 방법 TDD의 장점 1️⃣ 테스트 코드? : 소프트웨어의 기능과 동작을 테스트하는데 사용되는 코드

fluttering-girdle-e7f.tistory.com

 

TDD와 테스트 코드에 대한 이론적인 내용은 해당 글을 참고해주세요 :)


JUnit?

Mockito?

    1. Mock 객체를 생성하는 방법
    2. Mock이 어떻게 동작해야 하는지 관리하는 방법
    3. Mock의 행동을 검증하는 방법

테스트 어노테이션

테스트 클래스와 메소드

테스트 코드 구현

 

 

1️⃣ JUnit?

: 자바 프로젝트를 위한 단위 테스트 프레임워크

🌍 JUnit5의 규칙
JUnit5에서 테스트를 실행하기 위해서는 다음의 한 가지만 지켜주면 된다.
➡ 테스트 메소드는 @Test 어노테이션이 붙어있어야 함!
💡 JUnit5?
이전 JUnit 버전과 다르게, JUnit5는 세개의 서브 프로젝트로 이루어져 있다.
JUnit5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

 

JUnit5는 크게 3가지 모듈로 구성된다.

  • JUnit Platform : JVM에서 테스트 프레임워크를 실행하는데 기초를 제공한다. 또한 테스트를 발견하고 테스트 계획을 생성하는 TestEngine API를 제공해 테스트 프레임워크를 개발할 수 있다.  
  • JUnit Jupiter : JUnit5에 새로 추가된 테스트 코드용 API 로, JUnit Jupiter는 JUnit 5에서 테스트를 작성하고 확장을 하기 위한 새로운 프로그래밍 모델과 확장 모델의 조합이다. Jupiter 는 테스트 코드 작성에 필요한 junit-jupiter-api 모듈과 테스트 실행을 위한 junit-jupiter-engine 모듈로 분리되어 있다.
    • JUnit Jupiter = junit-jupiter-api + junit-jupiter-engine
  • JUnit Vintage : Unit Vintage는 하위 호환성을 위해 JUnit3과 JUnit4를 기반으로 돌아가는 플랫폼에 테스트 엔진을 제공해준다. 

 

 

2️⃣ Mockito?

 

: Mock 객체를 쉽게 만들고, 관리하고, 검증할 수 있는 방법을 제공하는 Java 오픈소스 테스트 프레임워크

💡 Mock: 진짜 객체와 비슷하게 동작하지만, 프로그래머가 직접 행동을 관리하는 객체
@Mock Mock 객체를 만들어 반환
@Spy  Stub하지 않은 메소드들을 원본 메소드 그대로 사용하는 어노테이션
@InjectMocks @Mock, @Spy로 생성된 가짜 객체를 자동으로 주입시켜주는 어노테이션
@MockBean 스프링 컨텍스트에 mock객체를 등록하고 스프링 컨텍스트에 의해 @Autowired가 동작할 때 등록된 mock 객체를 사용할 수 있도록 동작하게 해준다.


Stub : 특정 메서드 호출에 대해 미리 정의된 동작을 반환하는 객체

💡 @Mock@InjectMocks의 차이?
▪ @Mock와 같이 모의 객체를 생성한다는 것은 실제 객체와 동일한 메소드와 동작을 가지지만 실제 데이터나 외부 리소스와의 상호작용은 없다.
▪ @InjectMocks와 같이 모의 객체를 주입한다는 것은 테스트의 대상이 특정 모의 객체를 사용해야 할 때, 그 모의 객체를 자동으로 주입하여 테스트를 수행할 수 있도록 한다.
▪ 모의 객체를 주입하는 것은 @Mock로 생성한 모의 객체가 자동으로 주입되어 테스트가 진행된다.
Mock을 활용한 테스트를 작성 시 알아야 할 내용
1. Mock 객체를 생성하는 방법
2. Mock이 어떻게 동작해야 하는지 관리하는 방법

3. Mock의 행동을 검증하는 방법

 

1. Mock 객체를 생성하는 방법

모조 객체를 만들어 사용하고 싶은 클래스를 @Mock 어노테이션 혹은 @MockBean 어노테이션을 사용하여 필드에 주입한다. (어노테이션을 사용하려면 @ExtendWidth를 사용해야 한다)

💡 @Mock과 @MockBean의 차이?
둘 다 모조 객체를 주입한다는 점에서는 동일하나 @MockBean은 모조 객체를 Bean으로써 관리할 수 있도록 만들어준다.

2. Mock이 어떻게 동작해야 하는지 관리하는 방법 : Stub

모의 객체의 메서드 호출에 대한 ‘예상 동작’을 정의한다.

3. Mock의 행동을 검증하는 방법 : Verify

모의 객체에 대해 특정 메서드가 호출되고 예상된 인자와 함께 호출되었는지를 검증하는 메소드를 제공한다.

 

3️⃣ 테스트 어노테이션

@DisplayName 테스트 클래스나 테스트 메소드에 이름을 붙여줄때 사용
@DisplayNameGeneration 클래스에 해당 어노테이션을 붙이면 @Test 메소드 이름에 언더바(_)로 표시한 모든 부분은 공백으로 처리된다.
@BeforeEach 각각 테스트 메소드가 실행되기전에 실행되어야 하는 메소드를 명시해준다. @Test , @RepeatedTest , @ParameterizedTest , @TestFactory 가 붙은 테스트 메소드가 실행하기 전에 실행된다. 
@AfterEach @Test , @RepeatedTest , @ParameterizedTest , @TestFactory 가 붙은 테스트 메소드가 실행되고 난 후 실행된다. 
@BeforeAll 테스트가 시작하기 전 딱 한 번만 실행 된다.
@AfterAll 테스트가 완전히 끝난 후 딱 한 번만 실행 된다.
@Nested  테스트 클래스안에 Nested 테스트 클래스를 작성할 때 사용되며, static이 아닌 중첩된 클래스, 즉 inner 클래스여야만 한다. 
@Tag 테스트를 필터링할 때 사용한다. 클래스또는 메소드레벨에 사용한다.
@Disabled 테스트 클래스나, 메소드의 테스트를 비활성화 한다.
@Timeout 주어진 시간안에 테스트가 끝나지 않으면 실패한다.
@ExtendWith extension을 등록한다. 
@RegisterExtension 필드를 통해 extension을 등록한다. 
@TempDir 필드 주입이나 파라미터 주입을 통해 임시적인 디렉토리를 제공할 때 사용한다
💡 @BeforeEach VS @BeforeAll 
@BeforeEach는 각 테스트 메소드마다 실행되지만 @BeforeAll는 테스트가 시작하기 전 딱 한 번만 실행 된다.
@AfterEach과 @AfterAll의 차이도 동일하다.

 

4️⃣ 테스트 클래스와 메소드

(1) 테스트 클래스 : 적어도 한개의 @Test 어노테이션이 달린 테스트 메소드가 포함되어있는 클래스를 말한다. 

(2) 테스트 메소드 : @Test ,@RepeatedTest ,@ParamterizedTest,@TestFactory ,@TestTemplate 같은 메타 어노테이션이 붙여진 메소드를 말한다.

(3) 라이프사이클 메소드 : @BeforeAll , @AfterAll , @BeforeEach , @AfterEach 같은 메타 어노테이션이 붙여진 메소드를 말한다.

 

 

 

5️⃣ 테스트 코드 구현

사용자 조회, 추가, 삭제 메소드를 구현하여 테스트를 진행했다.

 

User

@Getter
@Setter
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;

    private String name;
}

 

사용자 테이블은 간단하게 id, email, name으로 구성했다.

 

UserService

@Service
@AllArgsConstructor
public class UserService {

    private UserRepository userRepository;

    // 사용자 추가 메소드
    public User registerUser(User user) {
        return userRepository.save(user);
    }

    // 사용자 조회 메소드
    public User getUserById(Long id) {
        Optional<User> user = userRepository.findById(id);
        return user.orElse(null);
    }

    // 사용자 삭제 메소드
    public void deleteUser(User user) { userRepository.delete(user); }
}

 

사용자 추가, 조회, 삭제 메소드를 각각 구현했다.

 

UserServiceTest

1. @ExtendWith(MockitoExtension.class)
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

 

@Mock을 사용하기 위해 @ExtendWith(MockitoExtension.class)를 사용했다.

 

2. @Mock과 @InjectMocks
    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    private User testUser;

    @BeforeEach
    public void setUp() {
        testUser = new User();
        testUser.setId(1L);
        testUser.setName("user");
        testUser.setEmail("user01@example.com");
    }

 

테스트 클래스에서

(1) 실제 객체와 동일한 메소드와 동작을 가지지만 실제 데이터나 외부 리소스와의 상호작용은 없는 UserRepository에는 @Mock을

(2) 테스트가 특정 모의 객체를 사용해야 할 때, 모의 객체를 자동으로 주입하여 테스트를 수행할 수 있도록 하기 위해 UserService에 @InjectMocks을 사용했다.

 

@BeforeEach를 사용해 테스트 메소드 실행 전에 매번 User를 설정해준다.

 

3. 테스트 메소드
    @DisplayName("사용자 추가 테스트")
    @Test
    public void testCreateUser() {
        // Given
        when(userRepository.save(testUser)).thenReturn(testUser);
        // When
        User addedUser = userService.registerUser(testUser);
        // Then
        assertEquals(testUser.getName(), addedUser.getName());
        assertEquals(testUser.getEmail(), addedUser.getEmail());
    }

    @DisplayName("사용자 조회 테스트")
    @Test
    public void testGetUserById() {
        // Given
        when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
        // When
        User retrievedUser = userService.getUserById(1L);
        // Then
        assertEquals(testUser.getId(), retrievedUser.getId());
        assertEquals(testUser.getName(), retrievedUser.getName());
        assertEquals(testUser.getEmail(), retrievedUser.getEmail());
    }

    @DisplayName("사용자 삭제 테스트")
    @Test
    public void testDeleteUser() {
        // Given
        // When
        userService.deleteUser(testUser);
        // Then
        verify(userRepository, times(1)).delete(testUser);
    }

 

@Test 어노테이션으로 지정한 테스트 메소드들을 실행한다. 각 코드는 given-when-then 패턴에 맞춰 작성했다.

 

4. 테스트 실행

 

테스트를 실행하면 @DisplayName으로 설정한대로 보이는 것을 알 수 있다.

 

 

참고

 

 

 

 

 

'Study > Server 심화' 카테고리의 다른 글

6주차: 동시성 처리(2)  (0) 2024.05.28
5주차: 동시성 처리(1)  (1) 2024.05.21
4주차: 스프링 시큐리티 + JWT(2)  (1) 2024.05.14
3주차: 스프링 시큐리티 + JWT(1)  (0) 2024.05.07
1주차: TDD와 테스트 코드(1)  (2) 2024.04.02