[Junit] Service 테스트 코드 작성

chrkb1569·2023년 2월 14일
2

JUnit5

목록 보기
4/6

지난 시간에는 Repository에 대한 테스트 코드를 모두 마치고 오늘은 Service에 대한 테스트 코드를 작성하게 되었습니다.

그런데, TDD를 통하여 Service를 개발하려니 어떻게 시작해야할지 막막할 따름입니다...

그래서 구글링을 해보았으나, 다들 Service 메소드를 먼저 구현한 뒤에 테스트하는 방식을 사용하고 있었습니다.

계속 찾아보고는 있으나, 마땅한 글이 없더라구요...

그래서 생각해봤는데, 테스트 코드를 작성할 줄 아는 것이 우선이라고 생각해서 메소드를 먼저 만든 뒤, 이를 확인하는 형태의 테스트 코드를 작성하기로 했습니다.

TDD는 추후에 테스트 코드를 좀 더 작성해본 뒤, 감을 잡으면 그때 연습해가야 할 것 같습니다.

Service 본코드 작성

일단은 본코드를 먼저 작성하기로 하였기 때문에, Service를 간단하게 구현만 해주었습니다.

package com.example.JUnitTest.service;

import com.example.JUnitTest.domain.Book;
import com.example.JUnitTest.domain.BookRepository;
import com.example.JUnitTest.dto.BookEditDto;
import com.example.JUnitTest.dto.BookSaveDto;
import com.example.JUnitTest.web.dto.BookResponseDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class BookService {
    private final BookRepository bookRepository;

    // 책 전체 조회
    @Transactional(readOnly = true)
    public List<BookResponseDto> getBooks() {
        return bookRepository.findAll().stream()
                .map(s -> BookResponseDto.getInstance(s))
                .collect(Collectors.toList());
    }

    // 책 단일 조회
    @Transactional(readOnly = true)
    public BookResponseDto getBook(Long id) {
        Book findItem = bookRepository.findById(id).orElseThrow();

        return BookResponseDto.getInstance(findItem);
    }

    // 책 생성

    @Transactional
    public Long saveBook(BookSaveDto saveDto) {
        Book book = saveDto.toDto();

        Book savedBook = bookRepository.save(book);

        return savedBook.getId();
    }

    // 책 수정

    @Transactional
    public Long editBook(BookEditDto editDto, Long id) {
        Book findItem = bookRepository.findById(id).orElseThrow();

        findItem.setTitle(editDto.getTitle());
        findItem.setContent(editDto.getContent());

        return findItem.getId();
    }

    // 책 삭제
    @Transactional
    public void deleteBook(Long id) {
        bookRepository.deleteById(id);
    }
}

다음처럼 간단하게 DTO파일, 예외 처리가 필요한 부분만 만들어주고, 나머지는 그냥 정말 기능만 구현해주었습니다.

Service Test 코드 작성

Service 본 코드를 작성하였으니, 이제는 테스트 코드를 작성할 차례입니다.

일단은 테스트 코드에서 사용하게 될 Mockito에 대한 설정을 추가해주고,

testImplementation "org.mockito:mockito-core:3.3.3"
@ExtendWith(MockitoExtension.class)
public class BookServiceTest {

    @InjectMocks
    private BookService bookService;

    @Mock
    private BookRepository bookRepository;
}

다음처럼 Mockito를 사용하여 테스트 코드를 작성해보겠습니다.

책 저장 기능 테스트

일단 책 저장 기능의 경우, 사용자로부터 DTO를 받아서 처리하기때문에, 데이터를 만들어서 Service에 넘겨주었습니다.

    @Test
    @DisplayName("책 저장 테스트")
    public void saveBook() {
        // given
        BookSaveDto bookSaveDto = new BookSaveDto("테스트 제목 1", "테스트 내용 1", "테스트 작성자 1");
        Book saveItem = bookSaveDto.toDto();

        //stub
        BDDMockito.given(bookRepository.save(saveItem)).willReturn(bookSaveDto.toDto());

        //when
        Long savedId = bookService.saveBook(bookSaveDto);

        //then
        Assertions.assertThat(saveItem.getId()).isEqualTo(savedId);
    }

전에 수행하였던 Repository 테스트와는 조금 다르게 stub이라는 것이 추가되었는데, 이는 Repository를 Mock으로 만들어서 주입하였기 때문에 Mock의 행위를 정의하는 과정입니다.

우리가 만들어준 Service는 Repository와 의존 관계를 맺고 있는 형태이기 때문에, Service를 새롭게 생성하기 위해서는 Repository를 가져와야 하지만, 이렇게 하나하나 다 가져온다면 테스트 코드의 규모가 너무 커지는 경향이 있습니다.

따라서, Repository의 형태만 띄고 있는 객체를 가져다가 일단 의존 관계에 사용해주는데, 이 대상이 바로 Mock입니다.

그런데 Mock의 경우에는 진짜로 형태만 띄고 있을 뿐, 아무런 기능이 없기 때문에, 우리가 이 행위를 할 경우 어떻게 동작해야한다는 것을 명시해주어야합니다.

따라서, given을 통하여 괄호 안의 행위를 할 경우, willreturn 내부에 있는 대상을 반환한다고 행위를 먼저 지정하고 테스트를 진행하게됩니다.

테스트는 정상적으로 잘 수행됨을 확인할 수 있었습니다.

책 전체 조회 테스트

다음은 책 전체 조회 테스트입니다.

    @Test
    @DisplayName(value = "책 전체 조회 테스트")
    public void getBooksTest() {
        //given
        List<Book> bookList = new LinkedList<>();
        bookList.add(new Book(1L, "Test Title1", "Test Content1", "Test1"));
        bookList.add(new Book(2L, "Test Title2", "Test Content2", "Test2"));
        bookList.add(new Book(3L, "Test Title3", "Test Content3", "Test3"));

        List<Book> savedList = bookList;
        
        //stub
        given(bookRepository.findAll()).willReturn(savedList);

        //when
        List<BookResponseDto> resultList = bookService.getBooks();

        //then
        Assertions.assertThat(resultList.size()).isEqualTo(3);
    }

Book Repository의 행위를 정의한 뒤, 서비스를 통하여 책 목록을 확인하도록 설정하였습니다.

테스트는 정상적으로 동작하는 것을 확인하였으며,

    @Test
    @DisplayName(value = "책 전체 조회 테스트")
    public void getBooksTest() {
        //given
        List<Book> bookList = new LinkedList<>();
        bookList.add(new Book(1L, "Test Title1", "Test Content1", "Test1"));
        bookList.add(new Book(2L, "Test Title2", "Test Content2", "Test2"));
        bookList.add(new Book(3L, "Test Title3", "Test Content3", "Test3"));

        List<Book> savedList = bookList;

        //stub
        given(bookRepository.findAll()).willReturn(savedList);

        //when
        List<BookResponseDto> resultList = bookService.getBooks();

        //then
        Assertions.assertThat(resultList.size()).isEqualTo(1);
    }

다음처럼 마지막 증명 과정에서 리스트의 크기를 변화시키는 경우, 오류가 발생하는 것을 볼 수 있으며,

    @Test
    @DisplayName(value = "책 전체 조회 테스트")
    public void getBooksTest() {
        //given
        List<Book> bookList = new LinkedList<>();
        bookList.add(new Book(1L, "Test Title1", "Test Content1", "Test1"));
        bookList.add(new Book(2L, "Test Title2", "Test Content2", "Test2"));
        bookList.add(new Book(3L, "Test Title3", "Test Content3", "Test3"));

        List<Book> savedList = bookList;

        //stub
        given(bookRepository.findAll()).willReturn(savedList);

        //when
        List<BookResponseDto> resultList = bookService.getBooks();

        //then
        Assertions.assertThat(resultList.get(1).getContent()).isEqualTo(bookList.get(1).getContent());
    }

다음처럼 테스트 하는 대상을 바꾸어도 잘 동작하는 것을 볼 수 있습니다.

책 단일 조회 테스트

다음은 책 단일 조회 테스트입니다.

    @Test
    @Transactional(readOnly = true)
    @DisplayName(value = "책 단일 조회 테스트")
    public void getBook() {
        //given
        Long bookId = 2L;

        Book savedBook = new Book(2L, "Test Title2", "Test Content2", "Test2");

        //stub

        given(bookRepository.findById(bookId)).willReturn(Optional.of(savedBook));

        //when
        BookResponseDto findBook = bookService.getBook(2L);

        //then
        Assertions.assertThat(findBook.getTitle()).isEqualTo(savedBook.getTitle());
        Assertions.assertThat(findBook.getContent()).isEqualTo(savedBook.getContent());
        Assertions.assertThat(findBook.getAuthor()).isEqualTo(savedBook.getAuthor());
    }

다음처럼 테스트 코드를 작성하였으며, 특정 Id값을 통하여 반환되는 객체의 정보를 마지막에 대조하는 식으로 코드를 작성하였습니다.

다음처럼 테스트 코드가 잘 동작하는 것을 확인하였으며, 코드를 바꾸어봄으로써 테스트를 실패해보겠습니다.

    @Test
    @Transactional(readOnly = true)
    @DisplayName(value = "책 단일 조회 테스트")
    public void getBook() {
        //given
        Long bookId = 2L;

        Book savedBook = new Book(bookId, "Test Title2", "Test Content2", "Test2");

        //stub
        given(bookRepository.findById(bookId)).willReturn(Optional.of(savedBook));

        //when
        BookResponseDto findBook = bookService.getBook(1L);

        //then
        Assertions.assertThat(findBook.getTitle()).isEqualTo(savedBook.getTitle());
        Assertions.assertThat(findBook.getContent()).isEqualTo(savedBook.getContent());
        Assertions.assertThat(findBook.getAuthor()).isEqualTo(savedBook.getAuthor());
    }

어.. 다음처럼 조회하는 아이디를 바꾸어서 테스트 해보았는데, 동작이 안됩니다.

다음과 같은 오류가 뜨면서 테스트 코드가 동작이 안되는데, stub 과정에서 설정해 둔 이외의 행동을 Mock에게 시킬 경우 이러한 결과가 나오는 것 같습니다.

이건 좀 나중에 공부를 해봐야 할 것 같습니다.

책 수정 테스트

    @Test
    @Transactional
    @DisplayName(value = "책 수정 테스트")
    public void editBook() {
        //given
        Long bookId = 1L;

        Book savedBook = Book.builder()
                .id(bookId)
                .title("Origin Title")
                .content("Origin Content")
                .author("Origin Author")
                .build();

        BookEditDto editDto = BookEditDto.builder()
                .title("Edit Title")
                .content("Edit Content")
                .build();

        //stub
        given(bookRepository.findById(bookId)).willReturn(Optional.of(savedBook));

        //when
        bookService.editBook(editDto, bookId);
        BookResponseDto findBook = bookService.getBook(bookId);

        //then
        Assertions.assertThat(findBook.getTitle()).isEqualTo(editDto.getTitle());
        Assertions.assertThat(findBook.getContent()).isEqualTo(editDto.getContent());
    }

다음처럼 조회 기능을 stub 과정을 통하여 선언해준 뒤, 책을 수정하도록 테스트 코드를 작성하였습니다.

책 삭제 테스트

마지막으로는 책 삭제 테스트를 진행하였는데, 아직 구현한 기능이 단순한 삭제밖에 없기 때문에 단순하게 실행이 되었는지, 안되었는지 확인하는 코드만 작성하였습니다.

또한, given을 통한 stub 구문을 공백으로 놔둔 이유는 사용하지 않는 행위를 선언할 경우, 오류가 발생하기 때문에 비워줬습니다.

물론 어노테이션을 통한 설정을 하면 오류가 발생하는 것을 막을 순 있습니다만.. 굳이?라는 느낌이 들어서 그냥 지워줬습니다.

    @Test
    @DisplayName(value = "책 삭제 테스트")
    @Transactional
    public void deleteBook() {
        //given
        Long bookId = 1L;

        //stub

        //when
        bookService.deleteBook(bookId);

        //then
        verify(bookRepository).deleteById(bookId);
    }

일단은 이렇게해서 Service 테스트 코드 작성까지 완료하였습니다.

하.... stub 과정을 통해서 Mock 행위 하나하나 지정해주려니까 좀 복잡하고 힘들다는 생각이 들었습니다ㅠㅠ

또, 아직 어떻게 지정해야할지 모르는 것도 많기 때문에 공부좀 해야할 것 같습니다..

0개의 댓글