본문 바로가기
Spring Boot Project/Plming

[Plming] 게시글 기능 테스트

by slchoi 2022. 4. 2.
728x90
SMALL

지금까지 기능 개발을 진행하면서 테스트를 계속 진행하기는 했지만, 테스트 파일 이곳저곳에서 테스트를 진행해 각 기능의 테스트 코드가 어디 들어가 있는지 파악하기 힘들었다. 따라서 application을 매핑하기 전에 그동안 개발해온 기능들도 다시 한번 테스트하고, 테스트 코드도 정리할 겸 한 번 더 기능 테스트를 하기로 결정했다. 테스트는 JUnit을 사용할 예정이다.

진행해야 할 테스트는 아래와 같다.

BoardServiceTest

  • save( ) - 게시글 등록
  • update( ) - 게시글 수정
  • delete( ) - 게시글 삭제 (실제 DB에서 삭제)
  • deleteYn( ) - 게시글 삭제 (deleteYn의 값을 1로 변경)
  • findAll( ) - 게시글 리스트 조회
  • findAllByDeletYn( ) - 게시글 리스트 조회 (삭제 여부 기준)
  • findAllByUserI( ) - 게시글 리스트 조회 (사용자 Id 기준)
  • findById( ) - 게시글 상세 정보 조회

 

BoardTagServiceTest

  • save( ) - 게시글 태그 저장
  • findAllByBoardId - 게시글 태그 id 리스트 조회 (게시글 Id 기준)
  • findTagNameByBoardId( ) - 게시글 태그 이름 리스트 조회 (게시글 Id 기준)
  • deleteAllByBoardId( ) - 게시글 태그 삭제

 

1. BoardServiceTest

게시글 관련 테스트를 진행하기 전에 태그에 관련된 코드가 포함되면 안 되므로 잠시만 Board 클래스의 @Builder 코드를 아래와 같이 수정한다.

@Builder
public Board(User user, String category, String status, String period, String title, String content) {
    this.user = user;
    this.category = category;
    this.status = status;
    this.period = period;
    this.title = title;
    this.content = content;
    // this.boardTags = boardTags;
}

 

기존에 생성한 테스트 클래스를 모두 삭제하고 새로 만들 것이다. "test.plming" 패키지 내에 board 패키지를 생성한 뒤 BoardServiceTest 클래스를 생성하고, 더보기 코드를 작성한다.

더보기
package plming.board;

import org.assertj.core.api.Assertions;
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.data.domain.Sort;
import plming.board.dto.BoardResponseDto;
import plming.board.entity.Board;
import plming.board.entity.BoardRepository;
import plming.board.model.BoardService;
import plming.user.entity.User;
import plming.user.entity.UserRepository;

import java.util.List;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.data.domain.Sort.Direction.*;

@SpringBootTest
public class BoardServiceTest {

    @Autowired
    private BoardRepository boardRepository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BoardService boardService;

    private Board post1;
    private Board post2;
    private User user1;
    private User user2;

    @BeforeEach
    void beforeEach() {
        user1 = User.builder().email("email@email.com").github("github")
                .image("no image").introduce("introduce").nickname("nickname")
                .password("password").role("ROLE_USER").social(1)
                .build();
        user2 = User.builder().email("email2@email.com").github("github2")
                .image("no image").introduce("introduce2").nickname("nickname2")
                .password("password2").role("ROLE_ADMIN").social(1)
                .build();

        post1 = Board.builder().user(user1).content("사용자1의 첫 번째 게시글입니다.")
                .period("1개월").category("스터디").status("모집 중").title("사용자1의 게시글1")
                .build();
        post2 = Board.builder().user(user2).content("사용자2의 첫 번째 게시글입니다.")
                .period("1개월").category("프로젝트").status("모집 중").title("사용자2의 게시글 1")
                .build();
        userRepository.save(user1);
        userRepository.save(user2);
        boardRepository.save(post1);
        boardRepository.save(post2);
    }

    @AfterEach
    void afterEach() {
        boardRepository.deleteAll();
    }


    @Test
    @DisplayName("게시글 생성")
    public void save() {

        // given
        userRepository.save(user1);
        userRepository.save(user2);

        // when
        boardRepository.save(post1);
        boardRepository.save(post2);
    }

    @Test
    @DisplayName("게시글 수정")
    void update() {

        // given
        Board board = new Board(user1, "스터디", "모집 완료", "1주일", "1번 게시글 수정", "1번 게시글 수정합니다.");

        // when
        post1.update("1번 게시글 수정", "1번 게시글 수정합니다.", "스터디", "모집 완료", "1주일");

        // then
        assertEquals(board.getTitle(), post1.getTitle());
        assertEquals(board.getContent(), post1.getContent());
        assertEquals(board.getStatus(), post1.getStatus());
        assertEquals(board.getCategory(), post1.getCategory());
        assertEquals(board.getPeriod(), post1.getPeriod());
    }


    @Test
    @DisplayName("게시글 삭제 - 실제 DB에서 삭제)")
    void delete() {

        // when
        boardRepository.deleteById(post1.getId());

        // then
        assertEquals(1, boardRepository.count());
    }

    @Test
    @DisplayName("게시글 삭제 - deleteYn 값 변경")
    void deleteYn() {

        // when
        post1.delete();

        // then
        assertEquals('1', post1.getDeleteYn());
    }

    @Test
    @DisplayName("게시글 리스트 조회")
    void findAll() {

        // when
        List<Board> boardList = boardRepository.findAll();

        // then
        assertEquals(boardRepository.count(), boardList.size());
    }

    @Test
    @DisplayName("게시글 리스트 조회 - 삭제 여부 기준")
    public void findAllByDeleteYn() {

        // when
        boardService.delete(post1.getId());
        List<Board> boardList = boardRepository.findAllByDeleteYn('0', Sort.by(DESC, "id", "createDate"));

        // then
        assertEquals(1, boardList.size());
    }

    @Test
    @DisplayName("게시글 리스트 조회 - 사용자 id 기준")
    public void findAllByUserId() {

        // when
        List<Board> boardList = boardRepository.findAllByUserId(user1.getId(), Sort.by(DESC, "id", "createDate"));

        // then
        assertEquals(1, boardList.size());

    }

    @Test
    @DisplayName("게시글 상세 정보 조회")
    public void findById() {

        // when
        BoardResponseDto board1 = boardService.findById(post1.getId());
        BoardResponseDto board2 = boardService.findById(post2.getId());

        // then
        assertEquals(post1.getTitle(), board1.getTitle());
        assertEquals(post2.getContent(), board2.getContent());

        // then
        assertThat(board1.getTitle() == post2.getTitle()).isFalse();
        assertThat(board2.getContent() == post2.getContent()).isFalse();
    }
}

 

테스트 클래스를 실행시키면 모든 테스트 메서드가 오류 없이 성공한 것을 확인할 수 있다.

BoardService 테스트

테스트 메서드 코드를 살펴보면

애노테이션 및 메서드 설명
@BeforeEach @BeforeEach 애노테이션을 사용해 테스트 메서드를 실행시키기 전에 필요한 게시글과 사용자 정보를 미리 DB에 저장한다.

여기서는 두 명의 사용자와 두 개의 게시글을 저장한다.
@AfterEach 하나의 클래스 내에 여러 테스트 메서드가 있을 때, 다른 테스트 메서드에서 생성한 자원이 다른 테스트 메서드를 실행시킬 때 영향을 주지 않도록 테스트 메서드 하나가 끝날 때마다 @AfterEach를 실행시켜 DB를 초기화한다.
save( ) 게시글 생성을 테스트하는 메서드이다.

사실 @BeforeEach에서 오류가 발생하지 않았다면 DB에 게시글이 잘 저장된 것이므로, 이 메서드를 사용해 테스트할 필요는 없지만, 그래도 한 번 더 테스트해봤다.
update( ) 게시글 수정을 테스트하는 메서드이다.

새로운 게시글 객체를 생성해 board에 저장하고, post1의 정보를 board의 정보와 일치하게 post1의 정보를 수정한다.

JUnit에서 제공하는 Assertions.assertEquals를 사용해 board와 post1의 제목, 내용, 모집 상태, 기간, 카테고리를 비교한다. 
delete( ) 게시글 삭제를 테스트하는 메서드이다. 이 메서드에서는 실제 DB에서 데이터를 삭제하는 것을 테스트해볼 것이다.

boardRepository의 deleteById 메서드를 사용해 post1을 DB에서 삭제한다.

boardRepository의 count 메서드를 사용해 현재 DB의 게시글 개수를 받아온다. @BeforeEach에서 게시글을 2개 생성했는데 그 중 1개를 삭제했으므로 boardRepository.count()의 값이 1이면 테스트가 성공한다.
deleteYn( ) 게시글 삭제를 테스트하는 메서드이다. 이 메서드에서는 실제 DB에서 데이터를 삭제하는 것이 아니라 deleteYn의 값을 1로 변경해주는 Board의 delete( ) 메서드를 테스트해볼 것이다.

post1에 delete( ) 메서드를 적용한 다음 post1의 deleteYn의 값을 확인했을 때 '1'이면 테스트에 성공한다.
findAll( ) 게시글 전체 리스트 조회를 테스트하는 메서드이다.

boardRepository의 findAll( ) 메서드를 사용해 게시글 리스트를 모두 받아와 boardList에 저장하고, 현재 DB의 게시글의 개수를 반환하는 boardRepository.count( ) 메서드와 boardList의 크기가 일치하면 테스트에 성공한다.
findAllByDeleteYn( ) 삭제되지 않은 게시글(deleteYn='0') 리스트 조회를 테스트하는 메서드이다.

먼저 boardService의 delete( ) 메서드를 사용해 post1을 삭제한다. 그 다음 boardRepository의 findAllByDeleteYn 메서드를 사용해 삭제되지 않은 게시글만 불러와 boardList에 저장한다.

@BeforeEach에서 두 개의 게시글을 생성했는데 그 중 하나가 삭제되었으므로 boardList의 길이가 1이면 테스트에 성공한다.
findAllByUserId( ) 사용자 id를 기준으로 게시글 리스트 조회를 테스트하는 메서드이다.

boardRepository의 findAllByUserId( ) 메서드를 사용해 1번 사용자가 작성한 게시글만 불러와 boardList에 저장한다.

@BeforeEach에서 1번 게시글은 1번 사용자가 작성했고, 2번 게시글은 2번 사용자가 작성했으므로, boardList의 길이가 1이면 테스트에 성공한다.
findById( ) 게시글의 상세 정보를 조회하는 메서드이다.

boardService에서 post1의 id에 해당하는 정보를 가져와 board1에 저장하고 post2의 id에 해당하는 정보를 가져와 board2에 저장한다.

post1의 제목과 board1의 제목이 일치하고, post2의 내용과 board2의 내용이 일치하면 첫 번째 테스트에 성공한다.

board1의 제목과 post2의 제목이 불일치하고, board2의 내용과 post1의 제목이 불일치하면 두 번째 테스트에 성공한다.

 

2. BoardTagServiceTest

BoardTagServiceTest 코드를 작성하기 전에 Board의 @Builder 부분의 코드를 원상 복구한다.

@Builder
public Board(User user, String category, String status, String period, String title, String content, List<BoardTag> boardTags) {
    this.user = user;
    this.category = category;
    this.status = status;
    this.period = period;
    this.title = title;
    this.content = content;
    this.boardTags = boardTags;
}

 

다음으로 "test.plming.board" 패키지 내에 BoardTagServiceTest 클래스를 생성하고, 더보기 코드를 작성한다.

더보기
package plming.board;

import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import plming.board.entity.Board;
import plming.board.entity.BoardRepository;
import plming.board.entity.BoardTag;
import plming.board.entity.BoardTagRepository;
import plming.tag.entity.Tag;
import plming.user.entity.User;
import plming.user.entity.UserRepository;

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

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
public class BoardTagServiceTest {

    @Autowired
    private BoardRepository boardRepository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BoardTagRepository boardTagRepository;

    @Autowired
    private plming.board.model.BoardTagService boardTagService;

    private User user1;
    private User user2;
    private Board post1;
    private Board post2;
    private Long[] post1TagIds = {10L, 20L};
    private Long[] post2TagIds = {40L, 50L, 60L};

    @BeforeEach
    void beforeEach() {
        user1 = User.builder().email("email@email.com").github("github")
                .image("no image").introduce("introduce").nickname("nickname")
                .password("password").role("ROLE_USER").social(1)
                .build();
        user2 = User.builder().email("email2@email.com").github("github2")
                .image("no image").introduce("introduce2").nickname("nickname2")
                .password("password2").role("ROLE_ADMIN").social(1)
                .build();
        post1 = Board.builder().user(user1).content("사용자1의 첫 번째 게시글입니다.")
                .period("1개월").category("스터디").status("모집 중").title("사용자1의 게시글1")
                .build();
        post2 = Board.builder().user(user2).content("사용자2의 첫 번째 게시글입니다.")
                .period("1개월").category("프로젝트").status("모집 중").title("사용자2의 게시글 1")
                .build();

        userRepository.save(user1);
        userRepository.save(user2);
        boardRepository.save(post1);
        boardRepository.save(post2);
        boardTagService.save(List.of(post1TagIds), post1);
        boardTagService.save(List.of(post2TagIds), post2);

    }

    @AfterEach
    void afterEach() {
        boardTagRepository.deleteAll();
        boardRepository.deleteAll();
        userRepository.deleteAll();
    }

    @Test
    @DisplayName("태그 저장")
    void save() {

        // when
        boardTagService.save(List.of(post1TagIds), post1);
        boardTagService.save(List.of(post2TagIds), post2);
    }

    @Test
    @DisplayName("태그 ID 리스트 조회 - 게시글 ID 기준")
    void findAllByBoardId() {

        // given
        Long post1Id = post1.getId();
        Long post2Id = post2.getId();

        // when
        List<BoardTag> post1Tags = boardTagRepository.findAllByBoardId(post1Id);
        List<BoardTag> post2Tags = boardTagRepository.findAllByBoardId(post2Id);

        // then
        List<Long> post1TagIdCp = post1Tags.stream().map(BoardTag::getTag).map(Tag::getId).collect(Collectors.toList());
        List<Long> post2TagIdCp = post2Tags.stream().map(BoardTag::getTag).map(Tag::getId).collect(Collectors.toList());

        assertEquals(List.of(post1TagIds), post1TagIdCp);
        assertEquals(List.of(post2TagIds), post2TagIdCp);
    }

    @Test
    @DisplayName("태그 이름 리스트 조회 - 게시글 ID 기준")
    void findTagNameByBoardId() {

        // when
        List<String> post1TagNames = boardTagService.findTagNameByBoardId(post1.getId());
        List<String> post2TagNames = boardTagService.findTagNameByBoardId(post2.getId());

        // then
        assertEquals(post1TagIds.length, post1TagNames.size());
        assertEquals(post2TagIds.length, post2TagNames.size());
    }

    @Test
    @DisplayName("게시글 태그 삭제")
    void deleteAllByBoardId() {

        // when
        boardTagRepository.deleteAllByBoardId(post1.getId());
        boardTagRepository.deleteAllByBoardId(post2.getId());

        // then
        assertEquals(0, boardTagRepository.findAllByBoardId(post1.getId()).size());
        assertEquals(0, boardTagRepository.findAllByBoardId(post2.getId()).size());

    }

}

 

테스트 메서드를 하나씩 실행해보는데 deleteAllByBoardId( ) 테스트 메서드에서 아래와 같은 오류가 발생했다.

게시글 태그 삭제 테스트 오류 발생

org.springframework.dao.InvalidDataAccessApiUsageException: No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call 오류는 transaction과 관련된 오류인데, 기존의 BoardTagRepository 코드를 살펴보면

package plming.board.entity;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

public interface BoardTagRepository extends JpaRepository<BoardTag, Long> {
    /**
     * 게시글 ID 기준 태그 id 리스트 조회
     */
    List<BoardTag> findAllByBoardId(final Long boardId);

    /**
     * 게시글 ID 기준 태그 id 삭제
     */
    void deleteAllByBoardId(final Long boardId);
}

deleteAllByBoardId( ) 메서드에 @Transactional 애노테이션이 붙어있지 않아서 발생한 오류이다. DB에 접근해 값을 생성하거나 수정, 삭제하는 메서드에는 @Transactional 애노테이션을 붙여야 위와 같은 오류가 발생하지 않는다. 코드를 아래와 같이 수정하고 다시 테스트 메서드를 돌려보자.

package plming.board.entity;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

public interface BoardTagRepository extends JpaRepository<BoardTag, Long> {
    /**
     * 게시글 ID 기준 태그 id 리스트 조회
     */
    List<BoardTag> findAllByBoardId(final Long boardId);

    /**
     * 게시글 ID 기준 태그 id 삭제
     */
    @Transactional
    void deleteAllByBoardId(final Long boardId);

}

 

BoardTagServiceTest 클래스를 실행해보면 모든 메서드에서 오류 없이 잘 실행되는 것을 확인할 수 있다.

BoardTagServiceTest 실행

테스트 코드를 살펴보면

애노테이션 및 메서드 설명
@beforeEach BoardServiceTest와 같이 테스트 매서드를 실행하는데 필요한 데이터를 미리 저장한다.
@afterEach BoardServiceTest와 같이 하나의 테스트 메서드를 실행한 뒤 DB의 모든 데이터를 삭제한다.

여기서 삭제하는 순서가 중요하다. DB가 연관 관계로 매핑되어 있기 때문에 반드시 board_tag, board, user 테이블 순서로 삭제해야 한다. 순서가 바뀌면 오류가 발생한다.
save( ) 태그 저장 메서드 테스트 메서드이다.
findAllByBoardId( ) 게시글 Id 기준 태그 Id 리스트 조회 테스트 메서드이다.

boardTagRepository의 findAllByBoardId 메서드를 사용해 post1, post2의 BoardTag 객체를 찾아오고 리스트화해 각각 post1Tags, post2Tags에 저장한다.

post1Tags, post2Tags에서 태그 정보를 가져오고, 태그 정보 중 태그 id 값만 가져와 리스트화한 뒤 각각 post1TagIdCp, post2TagIdCp에 저장한다.

클래스 레벨에 선언한 post1TagIds, post2TagIds와 post1TagIdCp, post2TagIdCp 값을 비교해 일치하면 테스트에 성공한다.
findTagNameByBoardId( ) 게시글 Id 기준 태그 이름 리스트 조회 테스트 메서드이다.

boardTagService의 findTagNameByBoardId 메서드를 사용해 post1과 post2의 태그 이름 리스트를 가져온다.

클래스 레벨에 선언한 post1TagIds, post2TagId의 길이와 post1TagNames, post2TagNames의 길이를 비교해 일치하면 테스트에 성공한다.
deleteAllByBoardId( ) 게시글 태그 삭제 테스트 메서드이다.

boardTagRepository의 deleteAllByBoardId 메서드를 사용해 post1의 태그와 post2의 태그를 삭제한다.

boardTagRepository의 findAllByBoardId를 사용해 post1의 태그 정보와 post2의 태그 정보를 가져온다.

post1, post2의 태그는 전부 삭제되었으므로, 불러온 정보의 길이가 0이면 테스트에 성공한다.
728x90
LIST

댓글