본문 바로가기
Server/Spring Boot

SpringBoot #3 TestCode, JPA, h2, API

by HaningYa 2020. 4. 25.
728x90

오늘의 배울것

  • TestCode 작성
  • JPA
  • h2 database
  • 등록 수정 삭제 API
  • Postman 으로 요청보내서 json 돌려받기

[참고한책]

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스

가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링 시큐리티를 활용한 소셜 로그인 등으로 애플리케이션을 개발하고, 뒤이어 AWS 인프라의 기본 사용법과 AWS EC2와 R...

m.yes24.com

[책저자분블로그-기억보단기록을]

 

기억보단 기록을

Java 백엔드, AWS 기술을 익히고 공유합니다.

jojoldu.tistory.com

[내 스프링 관련 이전글]

 

SpringBoot #2 Hello World 삽질

[이전글] SpringBoot #1 개발환경 세팅(Mac) [참고한 튜토리얼] Intellij IDEA CE(무료버전) 스프링부트(Java) 프로젝트 생성 [BY 정원] 2020.03.28 기준 모든 내용을 업데이트했습니다. 네이버 포스트 제 계정의..

haningya.tistory.com

 

오케이 진도 나가자.


HelloController에 대한 테스트 코드를 작성하고 테스트

아래와 같은 디렉토리에 HelloControllerTest 테스트 파일을 만든다.

HelloControlerTest.java


테스트 코드를 작성한다.

package com.example.dbmasterspringboot;

import com.example.dbmasterspringboot.Controller.HelloController;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import org.springframework.test.context.junit4.SpringRunner;
//import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
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;
//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void hello가_리턴된다() throws Exception {
        String hello = "helloworld";
        mvc.perform(get("/helloworld/string"))
                .andExpect(status().isOk())
                .andExpect(content().string(hello));
    }
}

 

 

 


#삽질

코드에서 @RunWith(SpringRunner.class) 에서 RunWith 를 cannot Resolve 한단다.

 


Junit이 없나보다 해서 다운받아줬더니 import 된다.

 

혹시 몰라서 gradle 에도 코드를 추가했다.

 

    testImplementation(group: 'junit', name: 'junit', version: '4.13')

테스트 코드 옆 재생버튼을 클릭하면 테스트가 시작되고

 

테스트 통과~


JPA, h2 디비 라이브러리 추가

gradle에 추가한다.

 

 //스프링 부트용 jpa 추상화 라이브러리
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    //인메모리 관계형 데이터베이스 , 별도 설치 없어도됨 // 앱 재시작마다 초기회
    compile('com.h2database:h2')

디비에 Post 하는 코드 작성한다.

package com.example.dbmasterspringboot.domain;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;

import javax.persistence.*;

@Getter //Getter 메소드 자동 생성
@NoArgsConstructor //기본 생성자 자동 추가
@Entity //테이블과 링크될 클래스를 나타냄
public class Posts {
    //실제 DB 테이블과 매칭될 클래스
    @Id //테이블의 PK 필드를 나타냄
    @GeneratedValue(strategy = GenerationType.IDENTITY) //PK 생성 규칙
    private Long id;

    @Column(length = 500, nullable = false)
    private String title;

    @Column(length = 500, nullable = false)
    private String content;

    private String author;

    @Builder //빌더 패턴 클래스 생성
    public Posts(String title, String content, String author){
        this.title = title;
        this.content = content;
        this.author = author;
    }
}

인터페이스 추가

package com.example.dbmasterspringboot.domain;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PostsRepository extends JpaRepository<Posts,Long>{
    //기본적인 CRUD 메소드 자동 생성

}

Posts 에 대한 테스트 코드 작성

package com.example.dbmasterspringboot.domain.posts;


import com.example.dbmasterspringboot.domain.Posts;
import com.example.dbmasterspringboot.domain.PostsRepository;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;

@SpringBootTest
@RunWith(SpringRunner.class)
public class PostsRepositoryTest {
    @Autowired
    PostsRepository postsRepository;

    @After
    public void cleanup(){
        postsRepository.deleteAll();
    }

    @Test
    public void 게시글저장_불러오기(){
        String title = "테스트 게시글";
        String content = "테스트 본문";

        postsRepository.save(Posts.builder()
        .title(title)
        .content(content)
        .author("uuzaza@naver.com")
        .build());

        //when
        List<Posts> postsList = postsRepository.findAll();

        //then
        Posts posts = postsList.get(0);
        assertThat(posts.getTitle()).isEqualTo(title);
        assertThat(posts.getContent()).isEqualTo(content);
    }

}

테스트 실패

실패

FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':test'.
> No tests found for given includes: [com.example.dbmasterspringboot.domain.posts.PostsRepositoryTest.게시글저장_불러오기](filter.includeTestsMatching)

테스트를 찾을 수 없다고 한다.

 

https://stackoverflow.com/questions/55405441/intelij-2019-1-update-breaks-junit-tests

 

Intelij 2019.1 update breaks JUnit tests

After 2019.1 update broke all tests with error: no tests found for given includes xxxx.someThingTest

stackoverflow.com

 

인텔리제이 문제라고 한다.

 

preference --> gradle --> Run tests using 을 IntelliJ IDEA로 바꾼다.

 

 


오류의 홍수이다


의미있는 오류만 보자면

 

java.lang.IllegalStateException: Failed to load ApplicationContext


Caused by: org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'entityManagerFactory' defined in class path resource 
[org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: 
Invocation of init method failed; nested exception is org.hibernate.AnnotationException:
No identifier specified for entity: com.example.dbmasterspringboot.domain.Posts


Caused by: org.hibernate.AnnotationException: 
No identifier specified for entity: com.example.dbmasterspringboot.domain.Posts

 

요렇게 3개인데 테스트 오류가 아니라 진짜 이 오류때문에 테스트가 실패했다는 건지 헷갈린다.

 

일단 마지막 번째 오류는

https://dkfkslsksh.tistory.com/49

 

No identifier specified for entity 에러 수정 방법

원인 @Data @Entity //@Table(name="t_wts_naverSid") public class WebtoonEntity { @Column(name="SID") private String sid; 위의 상태에서 컴파일 시작 org.hibernate.AnnotationException: No identifier spe..

dkfkslsksh.tistory.com

그치만,,, 난 @Id를 추가했는걸...

 

내가 경험해본 바로는 "난 추가하고 코드도 똑같은데 오류가 난다." 

상황일 때 나는 대부분 문제가  "이름은 같은데 다른 라이브러리를 import 했다." 였다.

import 할 때 자세히 안보고 alt-enter 하니,, 다시보면

 

잡았다 요놈!

 

javax 로 바꿔주면 된다.

 

테스트 성공

성공


동작된 쿼리문을 보고싶으면 application properties 에

 

spring.jpa.show_sql = true

 

를 추가해 주면 된다.

 

실행된 쿼리문 로그


등록 API 구현

@RestController
@RequiredArgsConstructor
public class PostsApiController {
    private final PostsService postsService;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto){
        return postsService.save(requestDto);
    }

}
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto){
        return postsRepository.save(requestDto.toEntitiy()).getId();
    }
}
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
    private String title;
    private String content;
    private String author;
    @Builder
    public PostsSaveRequestDto(String title, String content, String author){
        this.title = title;
        this.content = content;
        this.author = author;
    }

    public Posts toEntitiy(){
        return Posts.builder()
                .title(title)
                .content(content)
                .author(author)
                .build();
    }

}

테스트 코드 구현

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @After
    public void tearDown() throws Exception {
        postsRepository.deleteAll();
    }

    @Test
    public void Posts_등록된다() throws Exception {
        String title = "title";
        String content = "content";
        PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
                .title(title)
                .content(content)
                .author("author")
                .build();
        String url = "http://localhost:"+port+"/api/v1/posts";

        //when
        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url,requestDto,Long.class);
        //then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);

    }
}

테스트 코드 통과

통과


수정, 조회 API 구현

    @PostMapping("/api/v1/posts/{id}")
    public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto){
        return postsService.update(id,requestDto);
    }

    @GetMapping("/api/v1/posts/{id}")
    public PostsResponseDto findById (@PathVariable Long id){
        return postsService.findById(id);
    }
@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
    private String title;
    private String content;

    @Builder
    public PostsUpdateRequestDto(String title, String content){
        this.title = title;
        this.content = content;
    }
}
@Getter
public class PostsResponseDto {
    private Long id;
    private String title;
    private String content;
    private String author;

    public PostsResponseDto(Posts entity){
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.author = entity.getAuthor();
    }
}

테스트 코드 작성

    @Test
    public void Posts_수정된다() throws Exception {
        Posts savedPosts = postsRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());
        
        Long updateId = savedPosts.getId();
        String expectedTitle = "title2";
        String expectedContent = "content2";

        PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
                .title(expectedTitle)
                .content(expectedContent)
                .build();
        
        String url = "http://localhost:" + port + "/api/v1/posts/"+updateId;
        HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);
        
        //when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT,requestEntity,Long.class);
        
        //then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);
        
        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(expectedTitle);
        assertThat(all.get(0).getTitle()).isEqualTo(expectedContent);
        
    }

테스트 불통

핏빛으로 물든 로그창

org.springframework.web.client.RestClientException: Error while extracting response for type [class java.lang.Long] and content type [application/json]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `java.lang.Long` out of START_OBJECT token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.Long` out of START_OBJECT token
 at [Source: (PushbackInputStream); line: 1, column: 1]

 

문제라고 가르쳐준 코드를 보니 딱히 문제될 건 없는데 put으로 예제가 되있다.

 

Post 만 쓴것 같아서 POST 로 수정했다.

 

        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.POST,requestEntity,Long.class);

 

다시 테스트 하면

 

 

이건 밑에 content 에서 getContent 해야 되는데 getTitle로 자동완성 시켜서 오류났다.

 

통과

 

통과~


웹브라우저로 직접 확인해 보자 히힣

application.properties에 다음 코드를 추가한다.

spring.h2.console.enabled=true

 

이제 서버를 켜고 localhost:자기포트/h2-console 로 접속한다.

접속한 모습

 

*JDBC URL 은 'jdbc:h2:mem:testdb' 로 설정해야 한다.

 

접속한 모습

좌측 POSTS 테이블에 ID Author, Content, Title을 볼 수 가 있다.


먼저 테이블을 조회해 보면 데이터가 아직 없는 것을 알 수 있다.

 

 

조회를 위한 데이터를 쿼리문으로 넣어준다.

INSERT INTO posts (author, content, title) VALUES ('author','content','title');

잘 들어갔다.


json 리턴 잘된다.

 

POSTMAN 으로 테스트 해보고 싶은데

암욜맨 그대여~ 

{
    "timestamp": "2020-04-25T11:53:27.943+0000",
    "status": 400,
    "error": "Bad Request",
    "message": "Required request body is missing: public java.lang.Long com.example.dbmasterspringboot.web.PostsApiController.update(java.lang.Long,com.example.dbmasterspringboot.web.dto.PostsUpdateRequestDto)",
    "path": "/api/v1/posts/1"
}

 

body가 없다고 나온다. 필요한 body가 있나,,?

 

근데 이상한게 나는 post 보는거면 findById를 호출해야되는데 로그를 보면

 

2020-04-25 21:04:07.604  WARN 91161 --- [nio-8081-exec-3] 
.w.s.m.s.DefaultHandlerExceptionResolver : 
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: 
Required request body is missing:
public java.lang.Long com.example.dbmasterspringboot.web.PostsApiController
.update(java.lang.Long,com.example.dbmasterspringboot.web.dto.PostsUpdateRequestDto)]

update를 호출한다.

 

업데이트 코드는

@PostMapping("/api/v1/posts/{id}")
    public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto) {
        return postsService.update(id, requestDto);
    }

바디 리퀘스트 하고있으니까 당연히 바디 없다고 에러뜨지 멍청한 나님아

 

일단 update의 PostMapping을 "/api/v1/posts/update/{id}"로 바꿨다.

 

{
    "timestamp": "2020-04-25T12:08:18.966+0000",
    "status": 405,
    "error": "Method Not Allowed",
    "message": "Request method 'POST' not supported",
    "path": "/api/v1/posts/1"
}

 

POST 안된단다

 

그치만 난 꼭 POSTMAN 으로 결과를 보고싶은걸,,,

 

ApiController 에 다음 코드를 추가했다.

 

그리고 Body에 파라미터로 id 에 value 1 로 해서 POST 보냈다.

 

@RequestMapping("/api/v1/posts/read")
    public PostsResponseDto getThisPostToMe(@RequestBody Long id){
        return postsService.findById(id);
    }

 

응 안되 돌아가

 

{
    "timestamp": "2020-04-25T12:14:42.533+0000",
    "status": 415,
    "error": "Unsupported Media Type",
    "message": "Content type 'multipart/form-data;boundary=--------------------------226831863656390902840129;charset=UTF-8' not supported",
    "path": "/api/v1/posts/read"
}

 

보니까 Body에 보낼려면 @RequestBody 가 아니라 @RequestParam 을 써야한다.

 

수정했고 좋았어 한번더

 

조금 나아진것 같다.

게시글이 없다. 이말은 적어도 api 코드는 작동한다는 의미인거 아닐까?

 

아아 멍청한 h2는 서버 끄면 디비 날라가는데 멍청아 어휴

 

다시 웹에서 INSERT 해주고 요청해본다.

INSERT INTO posts (author, content, title) VALUES ('author','content','title');

 

json 두둥 등장

감격

 

잘 받아온다. 

 

오늘은 여기까지

 

728x90

댓글