TIL_220731_강의용 게시판 프로젝트 5

창고·2022년 8월 1일
1

들어가기에 앞서
실제 프로젝트 진행했던 코드 내용 및 세부 내용은 일부만 업로드하였습니다.

8. 데이터베이스 접근 로직 테스트 정의

(3) Entity 생성 (이어서)

  • ArticleComment 클래스 수정 (Article과 동일 원리)
@Getter
//@Setter : 클래스 전체적으로 걸면 안됨 -> (id) 등을 setter로 변경해버릴 수 있음
//물론 case by case로 전체적으로 거는 경우도 있음
@ToString
// 본문을 index로 하기엔 너무 큼 (MySQL 자체 기능이나 ElasticSearch 등의 검색 엔진 사용)
@Table(indexes = {
        @Index(columnList = "content"),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy")
})
@Entity
public class ArticleComment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment
    private Long id;

    @Setter @ManyToOne(optional = false) private Article article;
    // 게시글 (ID) ManyToOne : 상관관계, optional = false : 해당 필드가 필수로 있어야 함 (casecading = none이 기본)
    @Setter @Column(nullable = false, length = 500) private String content; // 본문

    // meta data
    @CreatedDate @Column(nullable = false) private LocalDateTime createdAt; // 생성일시
    @CreatedBy @Column(nullable = false, length = 100) private String createdBy; // 생성자
    @LastModifiedDate @Column(nullable = false) private LocalDateTime modifiedAt; // 수정일시
    @LastModifiedBy @Column(nullable = false, length = 100) private String modifiedBy; // 수정자

    protected ArticleComment() {}

    public ArticleComment(Article article, String content) {
        this.article = article;
        this.content = content;
    } // @NoArgsConstructor로 대체 가능

    public static ArticleComment of(Article article, String content) {
        return new ArticleComment(article, content);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ArticleComment that)) return false;
        return id != null && id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

(4) 어플리케이션 구동 및 테스트

  • service 로 구동해보기
  • services > +(Add service) > Run Configuration > Spring Boot

  • Database refresh 시 table 생성되어 있음

(번외) JPA Buddy 특수 기능

  • 앞의 과정은 Java 클래스로 도메인, 엔티티를 생성한 후 Spring Boot로 DDL을 사용, DB에 테이블을 만드는 과정이었음
  • JPA Buddy를 통해 DB 설계 > Java 클래스 생성이 가능
    • 허나 해당 과정 강의 상에서는 무료였으나 지금은 유료 버전... ㅎ;
    • Entities from DB로 활용
  • DTO 생성 기능 (JPA Buddy)
    • DTO에 id는 잘 안 넣는 편 (제외)
    • Record 타입
  • 번외 : intention 기능
    • prefererences > intention 검색 > multiple lines 체크 되어 있는지 확인
  • 최소화 모드

(5) Entity 수정

  • 양방향 바인딩을 위한 article 수정
    (Article : one to many, ArticleComment : many to one)
  • 필드와 메타 데이터 사이, 즉 필드 마지막에 메소드 추가
    @OrderBy("id") // id 기준 정렬
    @OneToMany(mappedBy = "article", cascade = CascadeType.ALL)
    @ToString.Exclude // 위의 ToString alt+enter로 ToString.Exclude 활성화
    // 실무에서는 양방향 바인딩을 푸는 경우가 많음 (운영상 이슈 등)
    private final Set<ArticleComment> articleComments = new LinkedHashSet<>();
  • @ToString alt+enterToString.Exclude 활성화 (순환참조 문제 등 해결)
  • 번외 : 해당 오류 로그 관련 > 최적화 옵션 관련
  • Running의 어플리케이션 > EditConfiguration > Modify options > 'optimization' 검색 시 Disable launch optimization > 활성화 > Apply

(6) 간단한 CRUD 테스트

  • new > Spring Data Repository (JPA Buddy 기능)
  • 일반적으로 만드는 방법 (interface 타입 생성)
import com.fastcampus.projectboard.domain.ArticleComment;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ArticleCommentRepository extends JpaRepository<ArticleComment, Long> {
}
  • 테스트 클래스 생성 (Ctrl + shift + t)
  • 슬라이스 테스트 진행을 위한 코드 작성
  • 테스트를 위한 생성자
    private final ArticleRepository articleRepository;
    private final ArticleCommentRepository articleCommentRepository;

    public JpaRepositoryTest(@Autowired ArticleRepository articleRepository,
                             @Autowired ArticleCommentRepository articleCommentRepository
    ) {
        this.articleRepository = articleRepository;
        this.articleCommentRepository = articleCommentRepository;
    }
  • select 테스트 메소드
    @DisplayName("select 테스트")
    @Test
    void givenTestData_whenSelecting_thenWorksFine() {
        // Given

        // When
        List<Article> articles = articleRepository.findAll();

        // Then
        assertThat(articles)
                .isNotNull()
                .hasSize(1000);

    }
  • 테스트 데이터 생성 (mockaroo)
    • 회원 가입 후 SCHEMAS 에서 새 스키마 생성
    • 초기에 datetime을 SQL datetime으로 안 하고 복사해서 테스트 시 오류 생겼음
  • resource 에 new file > data.sql 생성
  • dialect를 mysql로 설정한 후 위에서 만든 테스트 파일 복사
  • 테스트 실행 성공 (select)
  • insert 테스트 메소드
    @DisplayName("insert 테스트")
    @Test
    void givenTestData_whenInserting_thenWorksFine() {
        // Given
        long previousCount = articleRepository.count(); // 현재 repository의 aritcle 개수 카운트

        // When
        Article savedArticle = articleRepository.save(Article.of("new article", "new content", "#spring"));
        // repository에 새 article 추가
        
        // Then
        assertThat(articleRepository.count()).isEqualTo(previousCount + 1);

    }
  • insert 테스트를 위해 Article,ArticleComment에 @EntityListeners(AuditingEntityListener.class) 추가
@EntityListeners(AuditingEntityListener.class)
@Entity
public class ArticleComment {
  • update 테스트 메소드
    @DisplayName("update 테스트")
    @Test
    void givenTestData_whenUpdating_thenWorksFine() {
        // Given
        Article article = articleRepository.findById(1L).orElseThrow();
        // 현재 repository 내부에서 id가 1l인 article 불러오기, 없으면 exception throw
        String updatedHashTag = "#springboot";
        // 업데이트할 해시태그
        article.setHashtag(updatedHashTag);
        // 해시태그 수정

        // When
        Article savedArticle = articleRepository.saveAndFlush(article);
        // save로만 할 경우 update 쿼리가 관측이 안됨

        // Then
        assertThat(savedArticle).hasFieldOrPropertyWithValue("hashtag", updatedHashTag);

    }
  • delete 테스트 메소드
    @DisplayName("delete 테스트")
    @Test
    void givenTestData_whenDeleting_thenWorksFine() {
        // Given
        Article article = articleRepository.findById(1L).orElseThrow();
        // 현재 repository 내부에서 id가 1l인 article 불러오기, 없으면 exception throw
        long previousArticleCount = articleRepository.count();
        long previousArticleCommentCount = articleCommentRepository.count();
        // 연관관계가 지정되어 있어 article 삭제 시 articlecomment도 같이 지워지므로 둘 다 개수 카운트
        int deletedCommentsSize = article.getArticleComments().size();

        // When
        articleRepository.delete(article);

        // Then
        assertThat(articleRepository.count()).isEqualTo(previousArticleCount - 1);
        assertThat(articleCommentRepository.count()).isEqualTo(previousArticleCommentCount - deletedCommentsSize);

    }
  • Commit 및 push 정리
  • GitKraken에서의 PULL REQUEST
    • 좌측 'PULL REQUEST' 사용
    • 그러나 GitHub에서 추가해줘야 하는 것들이 있어 GitKraken에서는 사용하지 않는 것 권장
  • GitHub에서 EOF 빨간 표시를 보기 싫다면 코드 마지막 줄에 빈 한 줄을 추가해줘야 함
  • Pull Request 후 fetch, main branch로 와서 Pull fast foward

(7) Entity 구현 관련 개선

  • 공통 / 반복된 필드에 대한 개선 : 필드를 추출하는 방식
    • Embedded 방식
    • MappedSuperClass
  • MappedSuperClass 사용
    • meta data 필드 그대로 복붙
    • @EntityListeners(AuditingEntityListener.class) 를 Article, ArticleComment에서 빼서 MappedSuperClass에 사용
    • ISO 규격에 맞게 출력해주는 @DateTimeFormat
    • updatable = false로 수정이 되면 안되는 필드를 수정 불가능하게 변경
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    @CreatedDate
    @Column(nullable = false, updatable = false) private LocalDateTime createdAt; // 생성일시
  • 기존 Article, ArticleComment 클래스가 AuditingFields를 상속
  • Article
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;

@Getter
//@Setter : 클래스 전체적으로 걸면 안됨 -> (id) 등을 setter로 변경해버릴 수 있음
//물론 case by case로 전체적으로 거는 경우도 있음
@ToString // alt+enter로 ToString.Exclude 활성화
// 본문을 index로 하기엔 너무 큼 (MySQL 자체 기능이나 ElasticSearch 등의 검색 엔진 사용)
@Table(indexes = {
        @Index(columnList = "title"),
        @Index(columnList = "hashtag"),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy")
})
@Entity
public class Article extends AuditingFields {

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

    @Setter @Column(nullable = false) private String title; // 제목
    @Setter @Column(nullable = false, length = 10000) private String content; // 본문

    @Setter private String hashtag; // 해시태그
    // @Transient 언급이 없는 이상 @Column 적용된 것으로 인식 (nullable = true)

    @OrderBy("id") // id 기준 정렬
    @OneToMany(mappedBy = "article", cascade = CascadeType.ALL)
    @ToString.Exclude // 위의 ToString alt+enter로 ToString.Exclude 활성화
    // 실무에서는 양방향 바인딩을 푸는 경우가 많음 (운영상 이슈 등)
    private final Set<ArticleComment> articleComments = new LinkedHashSet<>();

    /* AuditingFields로 이전됨
    // meta data
    // JPA Auditing
    @CreatedDate @Column(nullable = false) private LocalDateTime createdAt; // 생성일시
    @CreatedBy @Column(nullable = false, length = 100) private String createdBy; // 생성자
    @LastModifiedDate @Column(nullable = false) private LocalDateTime modifiedAt; // 수정일시
    @LastModifiedBy @Column(nullable = false, length = 100) private String modifiedBy; // 수정자

    /* public void setArticleComments(Set<ArticleComment> articleComments) {
        this.articleComments = articleComments;
    } */ // 자동으로 생성된 것으로 삭제

    // Entity로서의 기본 기능 추가
    // Hibernate 구현체 기준으로 기본 생성자가 필요 (외부에서는 사용하지 못하도록 해야 함)
    protected Article() {} // private는 안됨

    private Article(String title, String content, String hashtag) {
        this.title = title;
        this.content = content;
        this.hashtag = hashtag;
    }

    public static Article of(String title, String content, String hashtag) {
        return new Article(title, content, hashtag);
    }
    // 도메인 Article을 생성하려면 위의 매개값이 필요하다는 가이드

    // list, collection 에 넣고 사용할 때 동일성 / 동등성 확인 필요
    // Lombok의 @EqualsAndHashCode 사용 x, 사용하게 되면 모든 필드가 동일해야 동일한 객체라고 판단하게 되는데 그럴 필요 없음

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Article article)) return false;
        return id != null && id.equals(article.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}
  • ArticleComment
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;
import java.util.Objects;

@Getter
//@Setter : 클래스 전체적으로 걸면 안됨 -> (id) 등을 setter로 변경해버릴 수 있음
//물론 case by case로 전체적으로 거는 경우도 있음
@ToString
// 본문을 index로 하기엔 너무 큼 (MySQL 자체 기능이나 ElasticSearch 등의 검색 엔진 사용)
@Table(indexes = {
        @Index(columnList = "content"),
        @Index(columnList = "createdAt"),
        @Index(columnList = "createdBy")
})
@Entity
public class ArticleComment extends AuditingFields {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment
    private Long id;

    @Setter @ManyToOne(optional = false) private Article article;
    // 게시글 (ID) ManyToOne : 상관관계, optional = false : 해당 필드가 필수로 있어야 함 (casecading = none이 기본)
    @Setter @Column(nullable = false, length = 500) private String content; // 본문

    /* AuditingFields로 이전됨
    // meta data
    @CreatedDate @Column(nullable = false) private LocalDateTime createdAt; // 생성일시
    @CreatedBy @Column(nullable = false, length = 100) private String createdBy; // 생성자
    @LastModifiedDate @Column(nullable = false) private LocalDateTime modifiedAt; // 수정일시
    @LastModifiedBy @Column(nullable = false, length = 100) private String modifiedBy; // 수정자
     */

    protected ArticleComment() {}

    public ArticleComment(Article article, String content) {
        this.article = article;
        this.content = content;
    } // @NoArgsConstructor로 대체 가능

    public static ArticleComment of(Article article, String content) {
        return new ArticleComment(article, content);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ArticleComment that)) return false;
        return id != null && id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}
  • AuditingFields
import lombok.Getter;
import lombok.ToString;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.format.annotation.DateTimeFormat;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@ToString
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class AuditingFields {
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    @CreatedDate
    @Column(nullable = false, updatable = false) private LocalDateTime createdAt; // 생성일시

    @CreatedBy
    @Column(nullable = false, length = 100, updatable = false) private String createdBy; // 생성자

    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    @LastModifiedDate
    @Column(nullable = false) private LocalDateTime modifiedAt; // 수정일시

    @LastModifiedBy
    @Column(nullable = false, length = 100) private String modifiedBy; // 수정자
}

9. API 구현 및 테스트 정의

(1) HAL Explorer 기본

  • Spring initializr > Rest Repository, Rest Repository HAL Explorer
  • application.yml 수정
  data.rest:
    base-path: /api
    detection-strategy: annotated
  • ANNOTATED : 어노테이션으로 명시한 Repository만 노출
  • Repository에 @RepositoryRestResource 추가
  • HAL Explorer : API 테스트를 위한 서비스
    • localhost:8080/api 진입 시
  • Test 폴더 내 controller package, DataRestTest 클래스 생성
  • 굳이 필요하지는 않지만 공부용으로 작성
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// @WebMvcTest
@DisplayName("Data REST - API 테스트") // 전체 테스트
@Transactional // spring Transactional
@AutoConfigureMockMvc
@SpringBootTest
public class DataRestTest {

    private final MockMvc mvc;

    public DataRestTest(@Autowired MockMvc mvc) {
        this.mvc = mvc;
    }

    @DisplayName("[API] 게시글 리스트 조회")
    @Test
    void givenNothing_whenRequestingArticles_thenReturnsArticleJsonResponse() throws Exception {
        // Given

        // When & Then
        mvc.perform(get("/api/articles")) // get 이후 ctrl+shift+space 두번, 이후 static 메소드로 호출
                .andExpect(status().isOk()) // status 이후 ctrl+shift 두번, MockMvc... status
                .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));// MediaType 중 hal+json이 없어 직접 valueOf로 호출
    }

    @DisplayName("[API] 게시글 단건 조회")
    @Test
    void givenNothing_whenRequestingArticle_thenReturnsArticleJsonResponse() throws Exception {
        // Given

        // When & Then
        mvc.perform(get("/api/articles/1")) // get 이후 ctrl+shift+space 두번, 이후 static 메소드로 호출
                .andExpect(status().isOk()) // status 이후 ctrl+shift 두번, MockMvc... status
                .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));// MediaType 중 hal+json이 없어 직접 valueOf로 호출
    }

    @DisplayName("[API] 게시글 -> 댓글 리스트 조회")
    @Test
    void givenNothing_whenRequestingArticleCommentsFromArticle_thenReturnsArticleCommentsJsonResponse() throws Exception {
        // Given

        // When & Then
        mvc.perform(get("/api/articles/1/articleComments")) // get 이후 ctrl+shift+space 두번, 이후 static 메소드로 호출
                .andExpect(status().isOk()) // status 이후 ctrl+shift 두번, MockMvc... status
                .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));// MediaType 중 hal+json이 없어 직접 valueOf로 호출
    }

    @DisplayName("[API] 댓글 리스트 조회")
    @Test
    void givenNothing_whenRequestingArticleComments_thenReturnsArticleCommentsJsonResponse() throws Exception {
        // Given

        // When & Then
        mvc.perform(get("/api/articleComments")) // get 이후 ctrl+shift+space 두번, 이후 static 메소드로 호출
                .andExpect(status().isOk()) // status 이후 ctrl+shift 두번, MockMvc... status
                .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));// MediaType 중 hal+json이 없어 직접 valueOf로 호출
    }

    @DisplayName("[API] 댓글 단건 조회")
    @Test
    void givenNothing_whenRequestingArticleComment_thenReturnsArticleCommentJsonResponse() throws Exception {
        // Given

        // When & Then
        mvc.perform(get("/api/articleComments/1")) // get 이후 ctrl+shift+space 두번, 이후 static 메소드로 호출
                .andExpect(status().isOk()) // status 이후 ctrl+shift 두번, MockMvc... status
                .andExpect(content().contentType(MediaType.valueOf("application/hal+json")));// MediaType 중 hal+json이 없어 직접 valueOf로 호출
    }
    
}
  • 위의 테스트를 통해 아래의 API 전부 구현 및 테스트가 완료되었음

(2) API 추가 구현 - Querydsl 활용

  • 위에서는 기본적인 기능만 구현되고 부가적인 필터 / 검색 기능은 아직 미구현 상태
    (작성자명 검색 등)
  • 위에서 작성한 테스트는 공부 목적으로 한 것이고 실제로는 통합 테스트라 무겁고 DB에 영향을 주기 때문에 @Disabled로 제외 처리
@Disabled("Spring Data REST 통합 테스트는 불필요하므로 제외시킴")
@DisplayName("Data REST - API 테스트") // 전체 테스트
@Transactional // spring Transactional
@AutoConfigureMockMvc
@SpringBootTest
public class DataRestTest {
  • querydsl 의존성 추가
	// queryDSL 설정
	implementation "com.querydsl:querydsl-jpa"
	implementation "com.querydsl:querydsl-core"
	implementation "com.querydsl:querydsl-collections"
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError 대응
	annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError 대응
  • querydsl을 위한 설정 추가 (build.gradle 내부)
// Querydsl 설정부
def generated = 'src/main/generated'

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
	options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// java source set에 querydsl QClass 위치 추가
sourceSets {
	main.java.srcDirs += [ generated ]
}

// gradle clean 시에 QClass 디렉토리 삭제
clean {
	delete file(generated)
}
  • gradle refresh 후, main의 'generated' 지운 후 tasks > build > build 진행
  • ArticleRepository, ArticleCommentRepository 수정
@RepositoryRestResource
public interface ArticleRepository extends
        JpaRepository<Article, Long>,
        QuerydslPredicateExecutor<Article> // Article 안의 모든 필드에 대한 기본 검색 기능 추가
        //QuerydslBinderCustomizer<QArticle>
{
}
  • 위의 기능으로 해당 엔티티의 모든 필드에 대한 기본 검색 기능이 추가됨
  • 다만 부가적인 커스텀 검색 기능 구현이 필요
  • 이를 위해 QuerydslBinderCustomizer를 활성화 시킨 후 Override > customize 재정의
    • bindings 활용
    • 검색 가능한 필드 제한
    • 일치검색에 대한 옵션 조정
import com.fastcampus.projectboard.domain.Article;
import com.fastcampus.projectboard.domain.QArticle;
import com.querydsl.core.types.dsl.DateTimeExpression;
import com.querydsl.core.types.dsl.StringExpression;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer;
import org.springframework.data.querydsl.binding.QuerydslBindings;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource
public interface ArticleRepository extends
        JpaRepository<Article, Long>,
        QuerydslPredicateExecutor<Article>, // Article 안의 모든 필드에 대한 기본 검색 기능 추가
        QuerydslBinderCustomizer<QArticle> { // 커스컴 검색 기능 추가를 위해 활성화

    @Override
    default void customize(QuerydslBindings bindings, QArticle root) {
        bindings.excludeUnlistedProperties(true); // true로 리스팅하지 않은 필드에 대해서는 검색 기능 열지 않게 함
        bindings.including(root.title, root.content, root.hashtag, root.createdAt, root.createdBy); // 해당 필드에 대해서는 검색 기능 오픈
        // bindings.bind(root.content).first(StringExpression::likeIgnoreCase); // like '${v}'
        // 대소문자까지 일치해야 검색되는 부분 해제
        bindings.bind(root.title).first(StringExpression::containsIgnoreCase); // like '%${v}%'
        bindings.bind(root.content).first(StringExpression::containsIgnoreCase); // like '%${v}%'
        bindings.bind(root.hashtag).first(StringExpression::containsIgnoreCase);
        bindings.bind(root.createdAt).first(DateTimeExpression::eq);
        bindings.bind(root.createdBy).first(StringExpression::containsIgnoreCase);
    }
}
  • QArticle, QArticleComment 에 대한 gitignore 추가
### Querydsl
/src/main/generated

10. 뷰 구축

(1) 목표 수립 및 Thymeleaf 의존성 추가, 테스트 클래스 생성

  • 목표
  • Spring Initializr > Thymeleaf 사용, 의존성 추가
  • controller package > ArticleController 생성
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@RequestMapping("/articles")
@Controller
public class ArticleController {
}
  • 테스트 클래스 생성
   @Disabled("쉿! 구현 중")
    @DisplayName("[VIEW][GET] 게시글 리스트 (게시판 페이지 - 정상 호출")
    @Test
    public void givenNothing_whenRequestingArticlesView_thenReturnsArticlesView() throws Exception {
        // Given

        // When & Then
        mvc.perform(get("/articles"))
                .andExpect(status().isOk()) // 정상 호출인지
                .andExpect(content().contentType(MediaType.TEXT_HTML)) // HTML 파일의 컨텐츠인지
                .andExpect(view().name("articles/index")) // 뷰 이름 검사
                .andExpect(model().attributeExists("articles")); // 내부에 값이 있는지 (이름을 articles로 지정)
    }

    @Disabled("쉿! 구현 중")
    @DisplayName("[VIEW][GET] 게시글 상세 페이지 - 정상 호출")
    @Test
    public void givenNothing_whenRequestingArticleView_thenReturnsArticleView() throws Exception {
        // Given

        // When & Then
        mvc.perform(get("/articles/1"))
                .andExpect(status().isOk()) // 정상 호출인지
                .andExpect(content().contentType(MediaType.TEXT_HTML)) // HTML 파일의 컨텐츠인지
                .andExpect(view().name("articles/detail")) // 뷰 이름 검사
                .andExpect(model().attributeExists("article")) // 내부에 값이 있는지 (이름을 articles로 지정)
                .andExpect(model().attributeExists("articleComments")); // 댓글 리스트에도 값이 있어야 함
    }

    @Disabled("쉿! 구현 중")
    @DisplayName("[VIEW][GET] 게시글 검색 전용 페이지 - 정상 호출")
    @Test
    public void givenNothing_whenRequestingArticleSearchView_thenReturnsArticleSearchView() throws Exception {
        // Given

        // When & Then
        mvc.perform(get("/articles/search"))
                .andExpect(status().isOk()) // 정상 호출인지
                .andExpect(content().contentType(MediaType.TEXT_HTML)) // HTML 파일의 컨텐츠인지
                .andExpect(model().attributeExists("articles/search"));
    }

    @Disabled("쉿! 구현 중")
    @DisplayName("[VIEW][GET] 게시글 해시태그 검색 페이지 - 정상 호출")
    @Test
    public void givenNothing_whenRequestingArticleHashtagSearchView_thenReturnsArticleHashtagSearchView() throws Exception {
        // Given

        // When & Then
        mvc.perform(get("/articles/search-hashtag"))
                .andExpect(status().isOk()) // 정상 호출인지
                .andExpect(content().contentType(MediaType.TEXT_HTML)) // HTML 파일의 컨텐츠인지
                .andExpect(model().attributeExists("articles/search_hashtag"));
    }
  • 번외 : 테스트가 통과되어야 build 시 문제가 발생하지 않음
  • 따라서 일정 기능이 구현되지 않아 테스트가 이뤄지지 않는 경우 테스트 클래스나 메소드에 @Disabled를 넣어 build 시 무시하게끔 해야 함
    @Disabled("쉿! 구현 중")
    @DisplayName("[VIEW][GET] 게시글 리스트 (게시판 페이지 - 정상 호출")
    @Test
    public void givenNothing_whenRequestingArticlesView_thenReturnsArticlesView() throws Exception {
        // Given

        // When & Then
        mvc.perform(get("/articles"))
                .andExpect(status().isOk()) // 정상 호출인지
                .andExpect(content().contentType(MediaType.TEXT_HTML)) // HTML 파일의 컨텐츠인지
                .andExpect(model().attributeExists("articles")); // 내부에 값이 있는지 (이름을 articles로 지정)
    }
profile
공부했던 내용들을 모아둔 창고입니다.

0개의 댓글