티스토리 뷰

 

지난 글에서는 Mockito로 mock 객체를 만들고 JUnit과 함께 간단한 테스트를 작성하는 방법과 여러 Mockito 어노테이션에 대해 알아보았다. 이번 글에서는 Mockito를 사용하여 Spring Boot에서 Service 계층을 어떻게 테스트하면 좋을지 여러 테스트 케이스들을 살펴볼 것이다. Mockito에 대한 기본 개념이 궁금하신 분들은 이전 글을 먼저 참고하시길.

 

 

 

스프링 부트에서는 Mockito, JUnit 라이브러리가 자동으로 포함되기 때문에 별도의 설정없이도 테스트 코드를 작성할 수 있다. 그럼 테스트를 작성하기 위해 간단하면서도 의미있는 어플리케이션을 먼저 개발해보자. 이 어플리케이션은 Member, Team 엔티티로만 구성되며, 각 Member는 하나의 Team에 소속된다. (즉 Member - Team은 N:1 매핑) 여러 비즈니스 로직에 대한 테스트 케이스를 살펴보기 위해 Member 엔티티에 대한 CRUD가 발생하는 로직의 성공, 실패 테스트 코드를 모두 작성해볼 것이다. Member, Team 엔티티는 다음과 같다. 

@Builder
@Entity
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id", nullable = false)
    private Long id;

    @Column(nullable = false)
    private String name;
}
@Getter @Builder
@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id", nullable = false)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id", nullable = false)
    private Team team;

    //멤버 정보 수정 로직에 사용
    public void update(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

 

 

이번 글에서는 Service 계층만을 테스트할 것이므로 Controller를 제외한 Service, Repository만을 개발하였다. 일단 각각의 TeamRepository, MemberRepository는 스프링 데이터 JPA를 사용하여 별도의 메소드를 정의하지 않고 JpaRepository 인터페이스만을 상속한 기본적인 형태이다.

public interface TeamRepository extends JpaRepository<Team, Long> {

}
public interface MemberRepository extends JpaRepository<Member, Long> {

}

 

 

 

Member 엔티티에 대한 비즈니스 로직

먼저 MemberService는 비즈니스 로직을 수행하기 위해 TeamService, MemberRepository에 대한 의존관계를 가지고 있다. 따라서 MemberServiceTest 클래스 내에서는 실제 테스트 대상인 MemberService 필드에는 @InjectMocks 어노테이션을 선언하여 실제 객체가 주입되도록 하고 TeamService, MemberRepository 필드에는 @Mock 어노테이션을 선언하여 mock 객체를 생성하고 이를 memberService 객체에 주입하도록 하였다. 당연히 이 Mockito 어노테이션들을 사용하기 위해서는 테스트 클래스에 @ExtendWith(MockitoExtension.class) 어노테이션도 선언해주어야 한다. 

@RequiredArgsConstructor
@Service
public class MemberService {

    private final TeamService teamService;
    private final MemberRepository memberRepository;
    
    //비즈니스 로직...
 }
@ExtendWith(MockitoExtension.class)
class MemberServiceTest {

    @InjectMocks
    MemberService memberService; //실제 객체로 생성

    @Mock
    TeamService teamService; //MemberService에서 의존 → mock 객체로 생성

    @Mock
    MemberRepository memberRepository; //MemberService에서 의존 → mock 객체로 생성
}

참고로 테스트 대상이 되는 실제 객체를 생성하고 이 실제 객체에 mock 객체를 주입하는 방법에는 여러가지 방법이 존재한다. 위 코드는 Best Practice로 다른 방법이 궁금하신 분들은 링크 참고

 

이제 MemberService의 비즈니스 로직을 하나씩 개발하고 이에 대한 테스트 코드를 작성해보자. 각 로직에서는 Member에 대한 생성, 조회, 수정, 삭제가 발생한다. 테스트는 로직이 성공하는 것 뿐만 아니라 실패하는 것 또한 검증해야 올바른 테스트이므로 성공, 실패 테스트 코드를 모두 작성할 것이다. 

(가독성을 위해 테스트 코드에서 Mockito 클래스는 static import 하였음)

 

 

 

새로운 멤버 생성 

@Transactional
public Member saveMember(Long teamId, MemberDto memberDto) {
    Team team = teamService.getTeam(teamId);
    if (team == null) throw new IllegalArgumentException();
    Member member = Member.builder()
            .name(memberDto.getName())
            .age(memberDto.getAge())
            .team(team).build();
    return memberRepository.save(member);
}

saveMember 메소드에서는 해당 팀에 소속된 새로운 멤버를 생성하고 이를 저장하는 로직이 수행된다. 먼저 파라미터로 넘어온 teamId에 해당하는 팀이 정말 존재하는지 조회하기 위해 teamService 객체에 처리를 위임하였다. 만약 teamServicegetTeam 메소드에서 해당하는 팀이 없을 때 예외가 발생하면 saveMember 메소드에서는 이를 처리하지 않고 그대로 예외가 전파되도록 하였으며, 만약 getTeam 메소드가 null 값을 반환하면 IllegalArgumentException을 throw하도록 처리하였다. 반면 만약 해당하는 팀이 존재한다면, 파라미터로 넘어온 MemberDto로 해당 팀에 속한 member 객체를 생성하고 이를 DB에 저장하도록 memberRepository 객체에 처리를 위임하였다. 

 

 

@RequiredArgsConstructor
@Service
public class TeamService {

    private final TeamRepository teamRepository;

    public Team getTeam(Long teamId) {
        return teamRepository.findById(teamId).orElseThrow(IllegalArgumentException::new);
    }
}

위 코드는 MemberServicesaveMember 메소드에서 호출되는 TeamServicegetTeam 메소드이다. 만약 해당하는 팀이 존재하지 않을 경우에는 IllegalArgumentException 예외를 발생시킨다. 즉 해당 팀이 존재하지 않더라도 null 값을 반환하지는 않지만 MemberServicesaveMember 메소드에서 getTeam 메소드의 반환값에 대한 null 체크를 해준 이유는 MemberServiceTeamService의 구현 내용을 알지 못하기 때문이다. 즉 모든 가능성에 대한 처리를 해준 것이다.

 

 

@ExtendWith(MockitoExtension.class)
class MemberServiceTest {

    @InjectMocks
    MemberService memberService;

    @Mock
    TeamService teamService;

    @Mock
    MemberRepository memberRepository;

    @Captor
    ArgumentCaptor<Member> memberCaptor; //Captor 추가

    @DisplayName("새로운 멤버 생성 성공 테스트")
    @Test
    void saveMember() {
        //given
        Team team = Team.builder().id(1L).name("teamA").build();
        Member member = Member.builder().id(1L).name("memberA").age(20).team(team).build();
        when(teamService.getTeam(anyLong())).thenReturn(team);
        when(memberRepository.save(any(Member.class))).thenReturn(member);

        //when
        Member savedMember = memberService.saveMember(1L, new MemberDto("memberA", 20));

        //then
        verify(teamService, times(1)).getTeam(anyLong());
        verify(memberRepository, times(1)).save(memberCaptor.capture());
        assertThat(memberCaptor.getValue().getTeam()).isEqualTo(team);
        assertThat(savedMember).isEqualTo(member);
    }
}

가장 먼저 테스트할 케이스는 새로운 멤버를 생성하는 것에 성공한 경우이다. 먼저 테스트의 전체적인 구조는 given - when - then으로 구성되는데, given에서는 mock 객체의 Stub을 지정하며 when에서는 실제 객체 즉 테스트 대상이 되는 객체의 메소드를 호출한다. 다음으로 then에서는 메소드 호출 결과를 검증한다.

 

실제로 위 테스트 메소드에서는 MemberServicesaveMember 메소드 내부에서 처리를 위임하고 있는 TeamServiceMemberRepository의 메소드 호출 시 반환값을 when(...).thenReturn(...)으로 지정한 후 실제 saveMember 메소드를 호출하며 그 아래에서는 반환값과 mock 객체의 메소드 호출 여부 등을 검증하고 있다. 

 

 

그럼 검증하는 then 부분을 자세히 살펴보자. 일반적으로 mock 객체와 관련된 내용을 검증하기 위해서는 verify(...) 메소드가, 그 외의 것들을 검증하기 위해서는 Assertions가 사용된다. (Assertions에는 JUnit의 Assertions, AssertJ의 Assertions가 있다.)

 

먼저 로직 성공 시에는 saveMember 메소드 내에서 TeamServicegetTeam 메소드는 1번, MemberRepositorysave 메소드도 1번 호출되므로 이를 verify(...)로 검증한다. 다음으로 서비스 로직상 MemberRepositorysave 메소드의 인자로 전달되는 member 객체는 이전에 TeamService에서 조회된 team 객체를 가지고 있어야 하므로 이를 검증한다. 이때 mock 객체인 MemberRepositorysave 메소드의 인자로 전달된 member 객체를 획득하기 위해 ArgumentCaptor<Member> 타입의 memberCaptor를 사용하였다. 마지막으로 saveMember 메소드는 TeamRepositorysave 메소드의 반환값을 그대로 반환하므로 이를 검증해주었다. 

(이어지는 테스트 코드부터는 테스트 클래스를 제외한 테스트 메소드만을 작성할 것이다.)

 

 

@DisplayName("새로운 멤버 생성 실패 테스트 - 해당 팀 존재하지 않아 예외 발생")
@Test
void saveMemberFailWithException() {
    //given
    when(teamService.getTeam(anyLong())).thenThrow(new IllegalArgumentException());

    //when, then
    assertThatThrownBy(() -> memberService.saveMember(1L, new MemberDto("memberA", 20)))
            .isInstanceOf(IllegalArgumentException.class);

    verify(teamService, times(1)).getTeam(anyLong());
    verify(memberRepository, times(0)).save(any(Member.class));
}

위는 실패 테스트로 TeamService에서 해당 팀이 존재하지 않아 예외가 발생한 경우를 테스트하고 있다. 이 경우 saveMember 메소드에서는 발생한 예외를 그대로 전파하므로 밖으로 IllegalArgumentException 예외가 던져지고 MemberRepositorysave 메소드는 호출되지 않을 것이다.  

 

 

@DisplayName("새로운 멤버 생성 실패 테스트 - 해당 팀 존재하지 않아 null 반환")
@Test
void saveMemberFailWithNull() {
    //given
    when(teamService.getTeam(anyLong())).thenReturn(null);

    //when, then
    assertThatThrownBy(() -> memberService.saveMember(1L, new MemberDto("memberA", 20)))
            .isInstanceOf(IllegalArgumentException.class);

    verify(teamService, times(1)).getTeam(anyLong());
    verify(memberRepository, times(0)).save(any(Member.class));
}

이 또한 실패 테스트이지만 TeamService에서 해당 팀이 존재하지 않아 null을 반환한 경우를 테스트하고 있다. 이 경우 saveMember 메소드에서는 IllegalArgumentException을 발생시키므로 마찬가지로 MemberRepositorysave 메소드는 호출되지 않을 것이다. 

 

 

 

특정 멤버 조회 

//특정 멤버 조회
@Transactional(readOnly = true)
public Member getMember(Long memberId) {
    return memberRepository.findById(memberId).orElseThrow(IllegalArgumentException::new);
}

다음으로 파라미터로 전달된 memberId 값을 가진 특정 멤버를 조회하는 로직이다. 만약 해당 멤버가 없다면 IllegalArgumentException 예외를 발생시키고, 있다면 해당 Member 객체를 반환하도록 하였다. 즉 여기서는 특정 멤버가 MemberRepository에서 정상적으로 조회되는 경우와 조회되지 않아 예외가 발생하는 로직을 테스트하면 된다. 

 

 

@DisplayName("특정 멤버 조회 성공 테스트")
@Test
void getMember() {
    //given
    Team team = Team.builder().id(1L).name("teamA").build();
    Member member = Member.builder().id(1L).name("memberA").age(20).team(team).build();
    when(memberRepository.findById(anyLong())).thenReturn(Optional.of(member));

    //when
    Member findedMember = memberService.getMember(1L);

    //then
    verify(memberRepository, times(1)).findById(anyLong());
    assertThat(findedMember).isEqualTo(member);
}

먼저 MemberRepositoryfindById 메소드 호출 시 특정 Optional<Member> 타입의 객체가 조회되도록 Stub을 지정하였다. 따라서 MemberServicegetMember 메소드가 실제로 호출되었을 때 MemberRepository는 Stub에서 지정한 값을 반환할 것이다. 다음의 then 부분에서는 getMember 메소드에서 MemberRepositoryfindById 메소드를 정확히 1번 호출하였는지, 그리고 이 findById 메소드가 반환한 Optional 타입의 값을 예외 없이 Member 타입으로 잘 변환하여 반환하는지를 테스트하고 있다. 

 

 

@DisplayName("특정 멤버 조회 실패 테스트 - 해당 멤버 존재하지 않아 Optional.empty() 반환")
@Test
void getMemberFailWithOptionalEmpty() {
    //given
    when(memberRepository.findById(anyLong())).thenReturn(Optional.empty());

    //when, then
    assertThatThrownBy(() -> memberService.getMember(1L))
            .isInstanceOf(IllegalArgumentException.class);
    verify(memberRepository, times(1)).findById(anyLong());
}

MemberRepositoryfindById 메소드가 Optional.empty()를 반환하게 되면 MemberServicegetMember 메소드에서는 IllegalArgumentException 예외를 발생시키도록 하였다. 위 코드에서는 해당 멤버가 존재하지 않는 경우 해당 예외가 잘 발생하는지를 테스트하고 있다. 

 

 

 

성인인 멤버 리스트 조회

//성인인 멤버 리스트 조회
@Transactional(readOnly = true)
public List<Member> getMemberListOnlyAdult() {
    List<Member> memberList = memberRepository.findAll();
    return memberList.stream()
            .filter(member -> member.getAge() >= 20)
            .collect(toList());
}

위 서비스 로직은 MemberRepository로부터 현재 존재하는 모든 멤버 리스트를 조회한 후 나이가 20살 이상인 멤버만을 필터링하고 있다. 사실 이를 Service 단이 아닌 Repository에서 애초에 필터링해서 가져와도 되지만 다양한 테스트 코드를 작성해보기 위해 Service에서 이를 수행하도록 하였다. 

 

 

아래 테스트들은 모두 성인인 멤버 리스트 조회에 대한 성공 테스트로, 각각은 현재 멤버가 여러 명이면서 성인인 멤버가 조회된 경우, 현재 멤버가 여러 명이면서 성인인 멤버가 조회되지 않은 경우, 현재 멤버가 0명인 경우를 테스트한 것이다.  

@DisplayName("성인인 멤버 리스트 조회 성공 테스트 - 1명 이상")
@Test
void getMemberListOnlyAdult() {
    //given
    Team teamA = Team.builder().id(1L).name("teamA").build();
    Team teamB = Team.builder().id(2L).name("teamB").build();
    Member memberA = Member.builder().id(1L).name("memberA").age(20).team(teamA).build();
    Member memberB = Member.builder().id(2L).name("memberB").age(18).team(teamA).build();
    Member memberC = Member.builder().id(3L).name("memberC").age(27).team(teamB).build();
    when(memberRepository.findAll()).thenReturn(List.of(memberA, memberB, memberC));

    //when
    List<Member> memberListOnlyAdult = memberService.getMemberListOnlyAdult();

    //then
    verify(memberRepository, times(1)).findAll();
    assertThat(memberListOnlyAdult).hasSize(2);
    assertThat(memberListOnlyAdult).containsExactly(memberA, memberC);
    //assertThat(memberListOnlyAdult).containsExactly(memberA); //테스트 실패!!!
}

먼저 성인인 멤버 리스트를 조회하기 위해 현재 존재하는 모든 멤버를 조회하는 MemberRepositoryfindAll 메서드가 1번 호출됨을 검증하고 있다. 주어진 현재 멤버들 중 성인인 멤버는 memberA, memberC 뿐이므로 조회된 결과 memberListOnlyAdult 리스트의 사이즈는 2일 것이다. 그 아래에서는 이를 Assertions로 검증하고 있다.

 

 

@DisplayName("성인인 멤버 리스트 조회 성공 테스트 - 0명")
@Test
void getMemberListOnlyAdultWithAllLess20() {
    //given
    Team teamA = Team.builder().id(1L).name("teamA").build();
    Team teamB = Team.builder().id(2L).name("teamB").build();
    Member memberA = Member.builder().id(1L).name("memberA").age(15).team(teamA).build();
    Member memberB = Member.builder().id(2L).name("memberB").age(18).team(teamA).build();
    Member memberC = Member.builder().id(3L).name("memberC").age(17).team(teamB).build();
    when(memberRepository.findAll()).thenReturn(List.of(memberA, memberB, memberC));

    //when
    List<Member> memberListOnlyAdult = memberService.getMemberListOnlyAdult();

    //then
    verify(memberRepository, times(1)).findAll();
    assertThat(memberListOnlyAdult).isEmpty();
}

이 경우 현재 멤버들은 존재하나 성인인 멤버는 존재하지 않으므로 조회된 결과 memberListOnlyAdult 리스트의 사이즈는 0, 즉 비어있다. 

 

 

@DisplayName("성인인 멤버 리스트 조회 성공 테스트 - 멤버가 0명")
@Test
void getMemberListOnlyAdultWithNobody() {
    //given
    when(memberRepository.findAll()).thenReturn(List.of());

    //when
    List<Member> memberListOnlyAdult = memberService.getMemberListOnlyAdult();

    //then
    verify(memberRepository, times(1)).findAll();
    assertThat(memberListOnlyAdult).isEmpty();
}

이 경우 애초에 현재 멤버가 없으므로 마찬가지로 조회된 결과 memberListOnlyAdult 리스트의 사이즈는 0, 즉 비어있다. 

 

 

 

특정 멤버 정보 수정

//특정 멤버 정보 수정
@Transactional
public Member updateMember(Long memberId, MemberDto memberDto) {
    Member member = getMember(memberId);
    member.update(memberDto.getName(), memberDto.getAge());
    return member;
}

특정 멤버 정보를 수정하기 위해 먼저 파라미터로 전달된 memberId에 해당하는 멤버를 조회한다. 만약 존재하지 않는다면 앞서 살펴본대로 IllegalArgumentException 예외가 발생하며 updateMember 메소드에서는 이 예외를 그대로 전파한다. 반면 해당 멤버가 존재한다면 전달된 MemberDto의 값으로 해당 멤버의 정보를 수정하고 이 수정된 member 객체를 반환한다. 

 

 

@DisplayName("특정 멤버 정보 수정 성공 테스트")
@Test
void updateMember() {
    //given
    Team team = Team.builder().id(1L).name("teamA").build();
    Member member = Member.builder().id(1L).name("memberA").age(20).team(team).build();
    when(memberRepository.findById(anyLong())).thenReturn(Optional.of(member));

    //when
    MemberDto updateDto = new MemberDto("memberB", 25);
    Member updatedMember = memberService.updateMember(1L, updateDto);

    //then
    verify(memberRepository, times(1)).findById(anyLong());
    assertThat(updatedMember.getName()).isEqualTo(updateDto.getName());
    assertThat(updatedMember.getAge()).isEqualTo(updateDto.getAge());
}

먼저 정보를 수정하려는 특정 멤버를 조회하기 위해 memberRepositoryfindById 메소드가 1번 호출됨을 검증하고 있다. 이후에는 updateMember 메소드가 반환한 updatedMember 객체의 정보가 원하는대로 잘 수정되었는지를 검증한다. 

 

 

@DisplayName("특정 멤버 정보 수정 실패 테스트 - 해당 멤버 존재하지 않아 Optional.empty() 반환")
@Test
void updateMemberFailWithOptionalEmpty() {
    //given
    when(memberRepository.findById(anyLong())).thenReturn(Optional.empty());

    //when, then
    assertThatThrownBy(() -> memberService.updateMember(1L, new MemberDto("memberB", 25)))
            .isInstanceOf(IllegalArgumentException.class);
    verify(memberRepository, times(1)).findById(anyLong());
}

실패 테스트로, 수정하려는 특정 멤버가 조회되지 않아 IllegalArgumentException 예외가 발생함을 검증하고 있다.

 

 

 

특정 멤버 삭제 

//특정 멤버 삭제
@Transactional
public void deleteMember(Long memberId) {
    Member member = getMember(memberId);
    memberRepository.delete(member);
}

특정 멤버를 수정하는 로직과 마찬가지로 파라미터로 전달된 memberId 값을 가진 멤버를 먼저 조회하고 MemberRepositorydelete 메소드를 통해 해당 멤버 정보를 DB에서 삭제하도록 처리를 위임한다. 

 

 

@DisplayName("특정 멤버 삭제 성공 테스트")
@Test
void deleteMember() {
    //given
    Team team = Team.builder().id(1L).name("teamA").build();
    Member member = Member.builder().id(1L).name("memberA").age(20).team(team).build();
    when(memberRepository.findById(anyLong())).thenReturn(Optional.of(member));

    //when
    memberService.deleteMember(1L);

    //then
    verify(memberRepository, times(1)).findById(anyLong());
    verify(memberRepository, times(1)).delete(member);
}

성공 테스트로, MemberServicedeleteMember 실행 시, MemberRepositoryfindById 메소드로 특정 멤버를 조회하고 다시 delete 메소드로 조회된 멤버를 삭제한다는 것을 verify 메소드로 검증하고 있다. deleteMember 메소드는 반환값이 없으므로 별도의 Assertions 검증은 수행하지 않는다.  

 

 

@DisplayName("특정 멤버 삭제 실패 테스트 - 해당 멤버 삭제 시 예외 발생")
@Test
void deleteMemberFailWithException() {
    //given
    Team team = Team.builder().id(1L).name("teamA").build();
    Member member = Member.builder().id(1L).name("memberA").age(20).team(team).build();
    when(memberRepository.findById(anyLong())).thenReturn(Optional.of(member));
    doThrow(IllegalStateException.class).when(memberRepository).delete(any(Member.class)); //어떤 이유로 예외 발생

    //when, then
    assertThatThrownBy(() -> memberService.deleteMember(1L))
            .isInstanceOf(IllegalStateException.class);
    verify(memberRepository, times(1)).findById(anyLong());
    verify(memberRepository, times(1)).delete(member);
}

MemberServicedeleteMember 메소드 내에서 예외가 발생하면 별도의 처리 없이 해당 예외를 그대로 밖으로 전파한다. 위 코드는 이 로직을 테스트하는 것으로, 사실 MemberRepositoryfindById 메소드에서 특정 멤버가 조회되지 않을 때 IllegalArgumentException 예외가 발생함을 테스트하는 것이 더 옳지만, MemberRepositorydelete 메소드처럼 반환형이 void인 경우에는 어떻게 Stub을 지정하는지를 보여주기 위해 위와 같이 작성해보았다. 반환형이 void이므로 당연히 thenReturn(...)을 통한 반환값 지정은 불가능하고 발생되는 예외만을 지정할 수 있다. Stub 지정 방법은 먼저 doThrow(발생시킬 예외 클래스)를 호출하고 이후 메소드 체인으로 어떤 mock 객체의 어떤 메소드를 호출했을 때 해당 예외를 발생시킬 것인가를 작성해주면 된다.

 

 

 

MemberServiceTest 실행 결과

 

MemberServiceTest의 전체 테스트를 실행한 결과 모두 통과한 것을 확인할 수 있다.

 

 

 

//테스트를 위한 Team, Member 객체 생성 코드
public class TestFactory {
    
    public static Team aTeam() {
        return Team.builder()
                .id(1L)
                .name("teamA").build();
    }
    
    public static Member aMember() {
        return Member.builder()
                .id(1L)
                .name("memberA")
                .age(20)
                .team(aTeam()).build();
    }
}

위에서 작성한 테스트 코드에는 테스트를 위한 Team, Member 객체들을 직접 생성하는 코드들도 포함되어 있어 코드가 지저분해지고 이로 인해 가독성도 떨어지게 되었다. 따라서 테스트를 위한 Team, Member 객체를 생성해주는 역할만을 수행하는 별도의 클래스를 정의하고, 기존 테스트 코드에서 지저분한 객체 생성 코드들을 들어내면 더 깔끔한 테스트 코드 작성이 가능해진다. 앞으로는 더 효율적이고 깔끔하게 테스트 코드를 작성하는 방법에 대해 공부한 후 정리해볼 생각이다. 

 

 

 

마지막으로 스프링 부트에서 Service 계층을 단위 테스트하는 방법을 단계적으로 정리해보자.

 

- init: 테스트할 클래스의 객체에는 @InjectMocks 어노테이션을, 해당 클래스가 의존하고 있는 클래스의 객체에는 @Mock 어노테이션을 선언하고 이 Mockito 어노테이션을 활성화하기 위해 테스트 클래스 상단에 @ExtendWith(MockitoExtension.class) 어노테이션도 선언해준다. 

- given: when(...).thenReturn(...) 메소드 등을 통해 mock 객체의 동작을 정의한다. (즉 Stub 생성)

- when: 테스트 대상인 실제 객체의 메소드를 호출한다.

- then: verify(...) 메소드로 mock 객체의 동작을 검증하고 Assertions로 추가 검증을 수행한다.

 

 

 

자 Service 계층을 테스트했다면 다음으로는 Controller 계층 또한 테스트해줘야 한다. Controller를 테스트하기 위해서는 HTTP 스펙에 맞는 웹 요청을 보내야 하는데, 다행히도 스프링은 테스트를 위한 HTTP 요청을 만들어내는 MockMvc 클래스와 @WebMvcTest 어노테이션 등을 제공한다. 다음 글에서는 스프링 부트에서 Controller 계층을 어떻게 테스트하는지에 대해 알아보자.

 

 

 

끝.

 

 

 

참고

https://howtodoinjava.com/spring-boot2/testing/spring-boot-mockito-junit-example/

 

'자바 > 테스트' 카테고리의 다른 글

[Mockito] Mocking Framework with JUnit5  (1) 2023.10.07
[JUnit] Parameterized Test  (0) 2023.06.24
[JUnit] AssertJ Assertions  (0) 2023.06.23
[JUnit] JUnit5 Assertions  (0) 2023.06.20
[JUnit] 테스트 Tagging과 Filtering  (0) 2023.06.02
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
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
글 보관함