오늘의 배울것
- TestCode 작성
- JPA
- h2 database
- 등록 수정 삭제 API
- Postman 으로 요청보내서 json 돌려받기
오케이 진도 나가자.
HelloController에 대한 테스트 코드를 작성하고 테스트
아래와 같은 디렉토리에 HelloControllerTest 테스트 파일을 만든다.
테스트 코드를 작성한다.
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
인텔리제이 문제라고 한다.
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
그치만,,, 난 @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' 로 설정해야 한다.
접속한 모습
먼저 테이블을 조회해 보면 데이터가 아직 없는 것을 알 수 있다.
조회를 위한 데이터를 쿼리문으로 넣어준다.
INSERT INTO posts (author, content, title) VALUES ('author','content','title');
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 두둥 등장
잘 받아온다.
오늘은 여기까지
'Server > Spring Boot' 카테고리의 다른 글
SpringBoot #5 POST, Body, Param 으로 데이터 받기 (0) | 2020.04.30 |
---|---|
SpringBoot #4 Spring boot - JDBC - MySQL (0) | 2020.04.30 |
SpringBoot #2 Hello World 삽질 (2) | 2020.04.22 |
SpringBoot #1 개발환경 세팅(Mac) (1) | 2020.04.21 |
댓글