반응형

↓↓↓ 이전내용

2024.01.05 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예제

 

[REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예제

↓↓↓ 이전 내용 2023.12.30 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST 기본 설정 및 repository 계층 test 예제 [REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST

im-gonna.tistory.com

 

이전 내용에 이어서 이번에는 최상단 layer인 Controller의 test를 진행해보자.

 

main에 있는 controller class 이름에 ctrl+shift+t를 눌러서 test 클래스를 자동 생성해준다.

 

💡 Controller test class

 

import 되어있는 jupiter assertion은 제거한다. (이전에 설명했지만 기본 assertion 사용!)

 

  • 클래스 어노테이션 추가

외부 data와 직접 상호작용하는 계층이기 때문에 test class에 어노테이션을 추가해준다.

 

그리고 컨트롤러 클래스를 인자로 가져온다.

 

  • mockMvc 주입

 

web layer url test이기 때문에, 모의 MVC가 필요하다.

따라서 MockMvc 객체를 하나 선언하여 사용한다.

 

  • 필요한 객체 선언

controller 레이어는 service 레이어와 통신하기 때문에 모의 service를 만들어주어야 한다.

그리고 cloudvendor 인스턴스 2개를 각각 만들어 주고, 이들을 담을 리스트로 하나 선언해준다.

 

  • setUp 메서드

 cloudVendorOne 인스턴스와 cloudVendorTwo 인스턴스를 생성해주고, list에 담아준다.

 

 

  • getCloudVendorDetails()의 test 메서드

  • controller에 정의된 getCloudVendorDetails()의 로직을 보면, cloudVendorService의 getCloudVendor()를 호출한 결과를 반환한다.
  • 따라서 cloudVendorService의 getCloudVendor()에 cloudVendorId가 1로 전달하며, 이 메서드를 호출하면 cloudVendorOne을 return하여 성공적으로 보이도록 한다.
  • 그리고 mockMvc의 perform메서드를 통해 get 동작을 수행하도록 하고 url은 controller에서 지정한대로 cloudVendorId에 따라 달라지므로, /cloudvendor/1 로 가져오도록 한다.
  • andDo(print()): 이 메서드는 테스트 결과를 출력한다. 테스트가 실행될 때 컨트롤러가 반환하는 HTTP 응답 등의 정보를 콘솔에 출력하여 디버깅이나 테스트 결과 확인을 도와준다.
  • andExpect(status().isOk()): 이 메서드는 특정한 조건을 검증하는데, 여기서는 HTTP 응답의 상태 코드가 "200 OK"인지 확인한다. 만약 상태 코드가 200이 아니면 테스트는 실패하는 것이다.
  • 여기까지하면, perform get에서 빨간 줄이 쳐질 것이다. 예외처리가 필요하다. 메서드에 throws Exception을 추가해주자.

getCloudVendorDetails test 결과

 

테스트 결과 성공이다.

getCloudVendorDetails()의 test를 위해 모의(mock) http 서블릿 요청이 제대로 구축되는 것을 확인할 수 있다.

(이는 마치 레포지토리 레이어에서 내장된 데이터베이스 h2를 사용하는 것과 유사한 상황이다)

파라미터와 헤더는 아무것도 필요로 하지 않았다.

 

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"data":{"vendorId":"1","vendorName":"Amazon","vendorAddress":"USA","vendorPhoneNumber":"xxxxx"},"httpStatus":"OK","message":"Requested Vendor Details are given here"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

그 아래 더 내려서 response 부분을 보면, status는 200, 에러 사항 없음, body data에 controller에서 반환하는 CloudVendorOne에 대한 data가 담겨져 있는 것을 확인할 수 있다.

 

 

  • getAllCloudVendorDetails()의 test 메서드

  • 앞의 get메서드와 로직은 유사하나, service 메서드를 getAll로 수정해주고, 가져오는 단위가 list이기 때문에, 초기에 설정해주었던 cloudVendorList를 return하도록 한다.
  • 모든 cloudVendor를 가져올 때에는 기본 url을 사용하기 때문에, /cloudvendor로 mapping되도록 한다. 

getAllCloudVendorDetails test 결과

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = [{"vendorId":"1","vendorName":"Amazon","vendorAddress":"USA","vendorPhoneNumber":"xxxxx"},{"vendorId":"2","vendorName":"GCP","vendorAddress":"UK","vendorPhoneNumber":"yyyyy"}]
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

body를 보면 one, tow 모두 값을 가져오는 것을 확인할 수 있다.

 

 

  • deleteCloudVendorDetails()의 test 메서드

  • controller의 delete 메서드 로직을 보면, service의 delete메서드를 호출하고, 이 안에서는 Success 메시지를 반환하도록 되어있다.
  • 따라서 cloudvendorId가 1인 cloudvendor를 delete하도록 service 메서드가 호출되면, Success를 return하도록 하여, service의 delete 메서드 호출을 성공적으로 보이게 한다.
  • 그리고 mockMvc의 perform을 통해 delete mapping시 url은 /cloudvendor/1에 매핑되도록 한다.

deleteCloudVendorDetails test 결과

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"33"]
     Content type = text/plain;charset=UTF-8
             Body = Cloud Vendor Deleted Successfully
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

response의 body를 보면 controller의 해당 메서드에서 반환하는 값인 메시지를 반환하는 것을 확인할 수 있다.

 

 

  • createCloudVendorDetails()의 test 메서드

 

  • controller에서 create메서드 로직을 살펴보면, cloudVendorService의 create메서드를 호출한다. 그 메서드 안에서는 success를 return한다.
  • 따라서 create메서드가 호출되면 Sucess를 return하도록 한다.
  • perform의 post mapping 시 /cloudvendor url과 mapping되도록 한다.
  • 그런데, 내용으로 전달되는 data를 json형식으로 전달하여야 하기 때문에, contentType을 json으로 지정해준다.
  • 그런데, cloudvendor는 entity이기 때문에, json형식으로 바꾸어주기 위해 위 코드를 작성해준다.
  • json 형식의 cloudvendor를 requestJson에 저장해주고, requestJson을 content에 넣는다.

createCloudVendorDetails test 결과

 

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"33"]
     Content type = text/plain;charset=UTF-8
             Body = Cloud Vendor Created Successfully
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

response의 body를 보면 controller의 해당 메서드에서 반환하는 값인 메시지를 반환하는 것을 확인할 수 있다.

 

 

  • updateCloudVendorDetails()의 test 메서드

  • 앞의 create 메서드와 로직이 유사하기 때문에, service 메서드의 create를 update로 변경하고, perform메서드는 post를 put으로 변경한다.

updateCloudVendorDetails test 결과

 

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"33"]
     Content type = text/plain;charset=UTF-8
             Body = Cloud Vendor Updated Successfully
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

역시 response의 body에는 controller의 해당 메서드에서 반환하는 값인 메시지를 담고 있다.

 

모든 unit test를 마쳤으므로, 전체 test class의 test coverage를 확인해보자.

 

controller의 test coverage 결과

test coverage가 100% 인것을 확인하였다.

 

summery

 

 

↓↓↓ 다음 내용

2024.01.25 - [Web Application/Backend] - [REST API] Spring Boot로 REST API 프로젝트 (9) - Swagger로 api document 생성하기

 

[REST API] Spring Boot로 REST API 프로젝트 (9) - Swagger로 api document 생성하기

↓↓↓이전내용 2024.01.10 - [Web Application/Backend] - [REST API] Spring Boot로 REST API 프로젝트 (8) - JUnit TEST controller 계층 test 예제 [REST API] Spring Boot로 REST API 프로젝트 (8) - JUnit TEST controller 계층 test 예제 ↓

im-gonna.tistory.com

반응형
반응형

https://www.acmicpc.net/problem/1149

 

1149번: RGB거리

첫째 줄에 집의 수 N(2 ≤ N ≤ 1,000)이 주어진다. 둘째 줄부터 N개의 줄에는 각 집을 빨강, 초록, 파랑으로 칠하는 비용이 1번 집부터 한 줄에 하나씩 주어진다. 집을 칠하는 비용은 1,000보다 작거나

www.acmicpc.net

 

 

 

💡 RGB거리
[문제]
RGB거리에는 집이 N개 있다. 거리는 선분으로 나타낼 수 있고, 1번 집부터 N번 집이 순서대로 있다.집은 빨강, 초록, 파랑 중 하나의 색으로 칠해야 한다. 각각의 집을 빨강, 초록, 파랑으로 칠하는 비용이 주어졌을 때, 아래 규칙을 만족하면서 모든 집을 칠하는 비용의 최솟값을 구해보자.1번 집의 색은 2번 집의 색과 같지 않아야 한다.N번 집의 색은 N-1번 집의 색과 같지 않아야 한다.i(2 ≤ i ≤ N-1)번 집의 색은 i-1번, i+1번 집의 색과 같지 않아야 한다.

[입력]
첫째 줄에 집의 수 N(2 ≤ N ≤ 1,000)이 주어진다. 둘째 줄부터 N개의 줄에는 각 집을 빨강, 초록, 파랑으로 칠하는 비용이 1번 집부터 한 줄에 하나씩 주어진다. 집을 칠하는 비용은 1,000보다 작거나 같은 자연수이다.

[출력]
첫째 줄에 모든 집을 칠하는 비용의 최솟값을 출력한다.

 

 

 

✅ 문제 풀이

 

이 문제는 전형적인 DP 문제이다.

 

집은 R,G,B 세 색상 중 하나로 칠할 수 있는데, 인접한 집끼리는 색이 겹쳐서는 안된다.

그렇게 칠했을 때 모든 집을 칭하는 최소 비용을 구한다.

 

일단 입력으로 주어지는 각 집당 페인트 비용을 저장하기 위해서 2차원 배열을 사용해보자.

 

문제에서 주어진 예시를 사용할 것이다.

  a[i][0] a[i][1] a[i][2]
1 26 40 83
2 49 60 57
3 13 89 99

 

행은 집의 번호, 열은 각 집의 R,G,B 비용이다.

 

첫번째 집까지 칠했을 때는 첫번째 집에서 고른 비용이 저장되는 것이고, 두번째 집까지 칠했을 때는 첫번째 집까지 쓰인 비용+두번째 집에서 고른 비용으로 값이 정해진다. 따라서 다음과 같은 점화식을 따른다.

 

d[i]=d[i-1]+a[i]

 

 

그런데, 비용을 최소로 한다고 해서 모든 집에서 가장 작은 비용을 선택해버린다면, 위 예에 따르면 26, 49, 13이 가장 최소가 되어버리고, 모두 빨간색으로만 칠하게 되어버린다. 이는 조건을 위배하게 된다.

따라서 이전 집에서 어떤 색을 칠했는지에 따라서 이번 집에 칠할 수 있는 색이 2개로 추려져야 한다.

그러므로 이전 집을 R로 칠했을때, G로 칠했을때, B로 칠했을때를 모두 계산하고 나서, 최종 집까지 칠했을때 세 값중 최소값이 답이 될 수 있다.

 

이때 현재 집(i번 집)에 R을 칠하고자 한다면 이전집(i-1)이 G또는 B가 칠해졌어야 하고, 이 둘 중 최소 비용에 현재 집의 R 비용을 더한 값이 d[i][0]이 될 수 있다.

이를 점화식으로 나타내면 다음과 같다.

 

d[i][0] = min(d[i - 1][1], d[i - 1][2]) + a[i][0]; 
d[i][1] = min(d[i - 1][0], d[i - 1][2]) + a[i][1];
d[i][2] = min(d[i - 1][0], d[i - 1][1]) + a[i][2];

 

  d[i][0] d[i][1] d[i][2]
1 26 40 83
2 89 86 83
3 96 172 146

 

점화식에 따르면 위 표와 같이 정리될 수 있고, 이는 i번째 집까지 칠했을 때의 비용을 나타낸다.

따라서 위 예시의 답은 3번째 집까지 칠했을 때의 비용 96,172,146 중에 최소값인 96이 되는 것이다.

 

min(min(d[t][0], d[t][1]), d[t][2])

이 식을 통해서 답을 구할 수 있다. (t는 마지막 집의 번호이다)

 

 

✏ 코드 전문
#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);

	int t;
	cin >> t;

	vector<vector<int>> d(t + 1, vector<int>(3, 0));
	vector<vector<int>> a(t + 1, vector<int>(3, 0));

	for (int i = 1; i <= t; i++) {
		cin >> a[i][0] >> a[i][1] >> a[i][2];
		d[i][0] = min(d[i - 1][1], d[i - 1][2]) + a[i][0];
		d[i][1] = min(d[i - 1][0], d[i - 1][2]) + a[i][1];
		d[i][2] = min(d[i - 1][0], d[i - 1][1]) + a[i][2];
	}

	int answer = min(min(d[t][0], d[t][1]), d[t][2]);

	cout << answer;

	return 0;
}
반응형
반응형

↓↓↓ 이전 내용

2023.12.30 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST 기본 설정 및 repository 계층 test 예제

 

[REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST 기본 설정 및 repository 계층 test 예제

↓↓↓ 이전 내용 2023.12.15 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (5)-사용자 정의 ResponseEntity [REST API] Spring Boot로 REST API CRUD 간단 구현 (5)-사용자 정의 ResponseEntity 이전 내

im-gonna.tistory.com

 

지난 시간 repository 계층의 test를 수행했다.

이번에는 service 계층의 test를 수행하겠다.

 

💡 Service layer test 클래스 자동 생성하기

 

직접 디렉토리를 생성하고 test 클래스를 작성해도 되지만, service 클래스에서 "create test" 버튼 하나로 test 클래스를 바로 생성할 수 있다.

 

create test를 누르면 다음과 같은 창이 뜬다.

 

JUnit5는 기본으로 설정되어 있으며, test class 이름도 알아서 지정해 주는데, 적절해 보이므로 그대로 사용할 것이다.

마찬가지로 설정/해제 메서드를 추가해준다. setUp과 tearDown에 해당한다.

그리고 class의 모든 메서드에 대한 단위 test를 위해 모두 ✔ 하여 추가해준다.

 

 

OK버튼을 누르면, 위와같이 test 디렉토리 하에 repository와 병렬하게 service 단이 생성되었고, 그 안에 test 클래스도 잘 생성되었다.

 

💡 CloudVendorService test 코드 작성하기

 

클래스를 들여다 보면, jUnit Jupiter assertion이 import되어 있는데, 우리는 저번 시간과 동일하게 기본 assertion을 사용할 것이기 때문에 이 부분은(이미 주석처리 되어있지만) 지워준다.

 

우리는 arg.com에서 제공하는 assertion 라이브러리를 사용할 것이다.

 

 

  • Mock 객체 생성하기

 

서비스 계층이 데이터베이스와 직접적으로 통신해서는 안되기 때문에, 데이터베이스 응답을 얻으려면 repository layer를 moking해야 한다. 이 말은 repository 클래스의 객체를 mock object (모의 객체)로서 생성하여 사용함으로써 데이터를 사용한다는 것이다.

 

가상 객체라고 생각하면 된다.

 

따라서 CloudVendorRepository의 경우 mock 객체로서 사용한다.

 

autoCloseable 객체를 사용하는 이유는 리소스를 자동 해제하는 기능으로서 자원을 안전하게 사용하기 위함이다.

 

 

  • 설정 메서드 setUp()

 

클래스 실행 시 전체 클래스에 대한 mock이 자동으로 열리도록 한다. 이는 Mockito의 openMocks 메서드를 사용한다.

그리고 repository test에서와 동일하게, service 객체와 cloudVendor 객체를 생성해준다.

 

  • 해제 메서드

해제 시에는 autoCloseable을 close해주면 된다.

 

  • create 메서드에 대한 test

  • cloudvendor와 repository에 대해서 mock 객체를 선언해준다.
  • when 메서드를 사용해서 cloudvendorrepository의 save메서드를 호출하였을 때 cloudvendor를 return하도록 하여, 저장이 성공한 것처럼 보이도록 설정한다.
  • asserthat 메서드를 통해 실제 service 클래스의 create 메서드를 호출한 결과가 Success와 같은지 확인한다.
  • 결과가 true이면 test가 통과된다. 

createCloudVendor()의 unit test 결과

 

 

  • update 메서드에 대한 test

    • update 메서드는 앞선 create와 유사하므로 실제 service 클래스에서 호출하는 메서드를 update로만 바꾸어 주면 된다.
    • 이 역시 호출결과가 Success를 반환하면 test 통과이다.

updateCloudVendor()의 unit test 결과

 

  • get 메서드에 대한 test

  • mock 객체를 생성한다.
  • when 메서드를 사용해서 cloudvendorrepository의 findById메서드를 호출하였을 때, cloudvendor를 return하도록 하는데, 이때 find결과가 있을수도 없을 수도 있으므로 optional로 nullable을 설정해준다.
  • asserthat 메서드를 통해 실제 service 클래스의 get 메서드를 호출하여 그 cloudvendor의 name과 클래스에서 선언한 cloudvendor의 name이 동일한지 확인한다.
  • 결과가 true이면 테스트는 통과이다.

getCloudVendor()의 unit test 결과

 

  • get all 메서드에 대한 test

  • 앞선 get메서드와 로직은 비슷하나, all 이므로 list단위로 반환한다는 차이가 있다.
  • 따라서 리스트 중 첫번째 cloudVendor의 phoneNumber를 비교하도록 한다.
  • 결과로 true를 반환하면 test는 통과이다.

getAllCloudVendor()의 unit test 결과

 

  • delete 메서드에 대한 test

  • 이 메서드의 test에서는 mock을 생성할 때, 실제로 repository 클래스의 메서드를 사용할 수 있도록 설정해준다.
  • 이를 위해서 Mokito의 CALLS_REAL_METHODS를 인자로 함께 넣어준다.
  • 또한 doAnswer를 사용해, cloudVendorRepository가 호출되었을 때 실제로 Method를 사용하도록 하고 deleteById 메서드를 호출하도록 한다.
  • asserThat으로 service의 delete메서드를 수행하고 나서 Success를 반환해야 한다.
  • 반환한다면 test는 통과한다.

deleteCloudVendor()의 unit test 결과

 

service test 클래스의 test 실행 결과 모두 통과하는 것을 확인하였다.

service 단의 모든 unit test가 성공적

 

 

test시에는 내 코드가 실제로 구현한 모든 코드를 test할 수 있는지 확인할 필요가 있다.

보통 90%이상이면 옳다고 판단한다.

IntelliJ에서는 아래와 같이 test coverage를 확인할 수 있도록 기능을 제공하고 있다. 

test coverage 확인 결과 91%

 

service class의 test coverage 확인 결과 91%가 cover되는 것을 확인하였다.

그럼 어느 부분을 cover하지 못했을까?

실제 service class의 코드를 살펴보자.

 

각 메서드마다 test가 cover된 부분은 왼쪽에 초록색 바로 표시가 된다. 

내리다 보니, getCloudVendor 메서드 쪽에서 중간에 빨간색 바를 확인하였다.

 

왼쪽을 보면 throw~ 예외처리 부분에 대한 test의 범위는 빨간색으로 표시되면서 포함되지 않은 것을 확인할 수 있다.

 

이렇게 test covergae를 통해서 내가 짠 test 코드가 실제로 구현의 어느 부분까지 cover할 수 있는지 확인 가능하다.

 

 

오늘은 service layer의 test code를 구현해보았다.

 

다음 시간에는 Controller의 test를 구현해보자.

 

↓↓↓ 다음 내용

2024.01.10 - [Web Application/Backend] - [REST API] Spring Boot로 REST API 프로젝트 (8) - JUnit TEST controller 계층 test 예제

 

[REST API] Spring Boot로 REST API 프로젝트 (8) - JUnit TEST controller 계층 test 예제

↓↓↓ 이전내용 2024.01.05 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예제 [REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예

im-gonna.tistory.com

 

반응형
반응형

https://swexpertacademy.com/main/code/problem/problemDetail.do?problemLevel=3&contestProbId=AV13_BWKACUCFAYh&categoryId=AV13_BWKACUCFAYh&categoryType=CODE&problemTitle=&orderBy=INQUERY_COUNT&selectCodeLang=ALL&select-1=3&pageSize=10&pageIndex=1

 

SW Expert Academy

SW 프로그래밍 역량 강화에 도움이 되는 다양한 학습 컨텐츠를 확인하세요!

swexpertacademy.com

 

난이도⭐⭐⭐

 

 

✅ 문제 풀이
  • 이 문제의 핵심은 굳이 100*100의 배열에 입력을 저장할 필요 없이, 입력을 받으면서 누적합을 구하고, max를 갱신해 나가면 된다는 것이다.
  • 입력을 받을 때, 행을 기준으로 열들을 받는다. 따라서 2중 for문을 사용한다.
  • 2중 for문에서 행이 먼저 돌기 때문에, 한 행의 값을 입력받을 때 100개의 열 값을 받는다.
  • 따라서 row는 각 행마다 누적합을 구하고, 현재의 max와 비교해서 큰 값을 갱신한다.
  • col은 각 행마다 한번에 100개씩 주어진다. 따라서 크기가 100인 배열을 선언해서, 해당 열에 누적하도록 하였다.
  • 열의 최대값은 2중 for문이 끝난 후, 구해진 누적값들을 서로 비교해 최대를 가리고, 현재의 max와 비교해 갱신한다.
  • 대각선은, 2중 for문에서 현재의 행과 열이 같은 값이면(if) 오른쪽방향 대각선의 누적합에, 행과 열을 더했을 때 99이면(else if) 왼쪽방향 대각선의 누적합에 더하도록 하였다. 마찬가지로 2중 for문이 끝난 후 두 값중 max를 구하고, 현재의 max와 비교해 갱신한다.
  • 최종 max를 출력한다.

 

✏ 코드 전문
#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

int main() {
	ios_base::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);

	for (int i = 0; i < 10; i++) {
		int t;
		cin >> t;//test case number

		vector<int> col(100,0);
		int rcross = 0;
		int lcross = 0;

		int m = 0;

		for (int j = 0; j < 100; j++) {
			int row = 0;
			for (int k = 0; k < 100; k++) {
				int temp;
				cin >> temp;
				row += temp;
				col[k] += temp;
				if (j == k) rcross += temp;
				else if ((j + k) == 99) lcross += temp;
			}
			m = max(m, row);
		}
		
		for (int j = 0; j < 100; j++) {
			m = max(m, col[j]);
		}

		m = max(m, max(rcross, lcross));
		
		cout << "#" << t << " " << m << "\n";

	}
	return 0;
}
반응형
반응형

 

↓↓↓ 이전 내용

2023.12.15 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (5)-사용자 정의 ResponseEntity

 

[REST API] Spring Boot로 REST API CRUD 간단 구현 (5)-사용자 정의 ResponseEntity

이전 내용↓↓↓ 2023.11.22 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (4)-예외처리/handle Exception [REST API] Spring Boot로 REST API CRUD 간단 구현 (4)-예외처리/handle Exception 이전 내용↓

im-gonna.tistory.com

💡 JUnit test 기본 설정

 

작성한 코드가 백에서 잘 동작하는 지를 확인하기 위해 TEST하는 기능이 있다.

스프링 부트에서는 JUnit을 활용하여 테스트 한다.

 

JUnit을 이용한 테스트 시 다음 네가지 종속성을 확인해야 한다.

JUNIT5, AssertJ, Mockito, H2Database

 

기본적으로 spring initializer로 프로젝트 생성 시 앞에 3개는 자동으로 종속되어 있다.

build.gradle의 dependency를 보면 확인이 가능하다.

 

따라서 h2 database만 따로 종속성을 추가해준다.

test시에 사용할 것이므로 다음 코드를 추가한다.

testImplementation 'com.h2database:h2:1.4.200'

 

test 사용 시 필요한 기능을 갖춘 종속성은 모두 추가해주었다.

 

그렇다면 test는 어디서 진행되는 것일까?

 

 

디렉토리를 보면 main과 test가 나란히 있는 것을 알 수 있다.

우리는 지금까지 main 이하에 디렉토리와 클래스를 생성해가며 실행했었지만, 나란히 존재하는 test 디렉토리에서는 unit test를 진행할 수 있다. 여기서 test를 실행한다.

 

실제 프로젝트가 돌아가는 동작은 main에서 일어난다. main에도 java와 resources 하위 디렉토리가 존재한다.

따라서 test를 위한 test 디렉토리 하에도 java와 병렬하게 resources 디렉토리를 생성해주자.

resources 디렉토리를 생성하였으면 그 안에 application.yml 파일을 추가해준다.

 

테스트 시 사용하는 데이터베이스는 h2이기 때문에, 스프링부트에 내장된 h2 데이터베이스를 연동하기 위해서 아래 코드를 yml파일에 추가해준다.

spring:
  datasource:
    url: jdbc:h2://mem:db;DB_CLOSE_DELAY=-1
    username: sa
    password: sa
    driver-class-name: org.h2.Driver

  # The SQL dialect makes Hibernate generate better SQL for the chosen database
  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.H2Dialect
    show-sql: true

  #JPA settings
  jpa.hibernate.ddl-auto: create-drop

 

create-drop으로 하는 이유는 스프링부트를 종료했을 때 모두 drop하고 새로운 애플리케이션이 돌아갔을 때 새로 받기 위함이다.

 

💡 Repository test

 

이제 테스트를 할 기초 준비를 마쳤으니, repository의 동작부터 test해보자.

 

main의 형태와 동일하게 test에서도 repository 디렉토리를 만들어주고, 그 아래 CloudVendorRepositoryTest 클래스를 추가하여 CloudVendorRepository를 test한다.

 

 

이제 클래스를 test클래스로 정의해주자.

 

repository test 클래스를 test하기 위해서 @DataJpaTest 어노테이션을 추가해 식별해준다.

이때, 테스트 작업을 할 때, 모든 초기화 작업을 수행하는 것이 모범 사례이다.

무슨 의미이냐 하면, 테스트를 할 때도 비교할 만한 대상이 있어야 하기 때문에, 테스트 용 데이터를 넣어준다는 것이다.

 

main에서 만들어준 CloudVendorRepository가 정상 동작하는 지 확인해야 하기 때문에, repository를 하나와, 데이터를 저장해주기 위해 CloudVendor 객체를 하나 정의해 준다.

 

  • 설정 메서드

 

설정 메서드를 통해 각 test케이스가 실행되기 전에, 미리 데이터베이스에 테스트 용 객체를 생성해준다.

위와 같은 값을 가지는 CloudVendor 객체를 생성해주고, 데이터 베이스에 저장해준다.

 

  • 해제 메서드

 

해제 메서드를 통해 각 test케이스가 실행되고 난 후에는, 다시 객체를 반환하여 메모리를 반환해준다.

 

 

  • 성공 테스트

먼저, 성공적으로 조회가 된 경우에 대한 테스트를 정의해보자.

  • 테스트 케이스를 사용할 때마다 @Test 어노테이션을 사용하여, 아래 메서드가 일반 메서드가 아니라, 테스트 케이스임을 식별할 수 있다. 따라서 이 표시가 있는 메서드 옆에는 개별적으로 실행할 수 있도록 재생 표시가 있는 것을 확인할 수 있다.
  • 이 test의 메서드 이름은 testFindByVendorName_Found이다.
  • cloudVendorRepository의 findByVendorName을 통해 "Amazon"을 이름으로 가진 cloudVendor를 찾도록 하여 조회 결과를 list에 저장한다.
  • assertThat을 통해 실제로 예상되는 값을 반환하는지 확인할 수 있다.
  • asserThat을 통해 list에 저장된 cloudVendor의 id가, 우리가 설정해준 Amazon의 id와 동일한지 확인한다.
  • asserThat을 통해 list에 저장된 cloudVendor의 Address가, 우리가 설정해준 Amazon의 Address와 동일한지 확인한다.
  • 모두 동일하다면 이 테스트는 성공적인 결과를 반환할 것이다.

 

첫번째 테스트에 대한 실행 결과 로그를 살펴보자.

 

select와 insert 쿼리가 실행되며 데이터베이스에 입력하고, 주어진 입력에 대해 조회한 것을 확인할 수 있다.

 

또한 EmbeddedDatabaseFactory 사용을 시작하는 것과 함께, 스프링부트에 내장된 jdbc h2 데이터베이스를 사용한다는 것을 확인할 수 있다.

즉, 테스트 시에는 내 pc에 있는 mysql db가 아닌, 내장된 h2 데이터베이스를 사용하여 테스트 한다.

 

  • 실패 테스트

다음으로, 조회에 실패한 경우에 대한 테스트를 정의해보자.

 

 

  • 데이터베이스에 존재하지 않는 cloudvendor 이름을 찾으려고 하는 실패 테스트 케이스에 대한 테스트이다.
  • 현재는 내장 db에 Amazon 객체만 들어가 있는 상태임을 명심하자.
  • 이 메서드는 testFindByVendorName_NotFound이다.
  • 마찬가지로, repository 조회를 GCP로 조회하고 그 결과를 list에 저장하도록 한다.
  • 당연히 내장 db에는 Amazon에 대한 데이터밖에 없기 때문에 GCP로 조회한 결과는 비어있어야 한다.
  • 이를 확인하기 위해 asserThat 메서드를 통해서 lsit가 비어 있음이 true임을 확인하도록 한다.
  • 이 assertion이 통과하면 리스트는 비어있다는 것이고, 이 테스트는 성공적으로 통과하게 된다.

 

💡 respository test 클래스에 대한 실행 결과

 레포지토리 테스트 클래스를 실행한 결과, 내부에 있는 unit 테스트 두개(성공사례, 실패사례) 모두 성공적으로 잘 테스트 되었음을 ✔표시를 통해 확인할 수 있다.

 

만약 test case가 잘못 적용된다면 어떻게 될까? 다음과 같이 실패 케이스에서 isTrue 부분을 isFalse로 바꾸어 보자.

 

 

테스트 결과 NotFound 테스트에서는 실패 표시가 나타나고, 왜 실패했는지 옆에 로그를 통해 설명해주고 있다.

테스트 로직 내에서는 FALSE를 기대했으나, 실제 값은 TRUE를 반환하므로 테스트 결과가 실패로 뜨고 있다.

따라서 이런 로그를 통해 테스트 내에서 어떤 값을 예상하고 출력하는지 확인함으로써 수정할 수 있다.

 

이렇게 Repository test를 수행할 수 있다!

 

 

↓↓↓ 다음 내용

2024.01.05 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예제

 

[REST API] Spring Boot로 REST API CRUD 간단 구현 (7) - JUnit TEST service 계층 test 예제

↓↓↓ 이전 내용 2023.12.30 - [Web Application/Backend] - [REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST 기본 설정 및 repository 계층 test 예제 [REST API] Spring Boot로 REST API CRUD 간단 구현 (6) - JUnit TEST

im-gonna.tistory.com

 

반응형
반응형
REST : 웹(HTTP)의 장점을 활용한 아키텍쳐

 

1. REST (REpresentational State Transfer) 기본

  • REST의 요소
  • method
method 의미 idemptent
POST Create No
GET Select Yes
PUT Update Yes
DELETE Delete Yes

 

** idempotent : 한 번 수행하냐, 여러 번 수행했을 때 결과가 같나?

 

  • Resource
    ✔ http://myweb/users와 같은 URI
    ✔ 모든 것을 Resource (명사)로 표현하고, 세부 Resource에는 id를 붙임

  • Message
    ✔ 메시지 포맷이 존재
    : JSON, XML 과 같은 형태가 있음 (최근에는 JSON 을 씀)
HTTP POST, http://myweb/users/
{
	"users" : {
		"name" : "terry"
	}
}

 

  • REST 특징
  • Uniform Interface (일관된 인터페이스)
    • HTTP 표준을 기반으로 하며, 특정 언어나 기술에 종속되지 않음.
    • 예를 들어, REST API가 HTTP와 JSON을 사용하여 정의되었다면, 어떤 플랫폼이든 해당 API에 접근 가능.
  • Self-Descriptive Messages (자기 서술적 메시지)
    • API 메시지 자체만으로도 그 의미를 이해할 수 있도록 설계해야 함.
    • 메시지를 보고도 어떤 리소스에 어떤 동작을 수행하는지 직관적으로 이해할 수 있어야 함.
  • HATEOAS (Hypermedia As The Engine Of Application State)
    • 응답에는 현재의 상태를 나타내는 하이퍼링크가 포함되어야 함.
    • 클라이언트는 이 링크를 통해 다음에 수행 가능한 작업을 이해하고 진행할 수 있음.
  • Statelessness (무상태성)
    • 각각의 요청은 독립적이며, 서버는 클라이언트의 상태를 저장하지 않음.
    • 이로써 서버는 간단해지고, 확장성이 높아짐.
  • Resource 지향 아키텍처 (ROA)
    • 자원(리소스)을 중심으로 하는 아키텍처
    • 각 리소스는 고유한 URI(Uniform Resource Identifier)를 가지며, 명사 형태로 정의됨.
  • Client-Server Architecture (클라이언트-서버 아키텍처)
    • 시스템을 클라이언트와 서버로 분리함으로써 각각의 역할을 명확하게.
  • Cache Ability (캐시 사용 가능)
    • 응답은 캐싱될 수 있어서, 동일한 요청에 대한 반복적인 처리를 최소화
  • Layered System (계층화 구조)
    • 시스템은 계층화될 수 있어서, 각 계층은 독립적으로 구현

  • Code On Demand (선택적 코드 전송)
    • 서버로부터 클라이언트가 실행 가능한 코드를 전송할 수 있습니다. 이는 선택 사항이며, 일반적으로는 사용되지 않음

=> REST는 자원을 중심으로 하며, 간단하고 일관된 인터페이스를 통해 클라이언트와 서버 간의 통신을 단순화하고 효율적으로 만들기 위한 웹 아키텍처

 

 

***참고
https://gyoogle.dev/blog/web-knowledge/REST%20API.html

반응형

'Web Application' 카테고리의 다른 글

HTTP status code (HTTP 상태 코드)  (0) 2023.12.28
HTTP Request Methods  (0) 2023.12.23
쿠키(cookie)와 세션(session)의 차이  (1) 2023.12.22
브라우저 동작 방법  (0) 2023.12.20
[Node.js] Node.js와 Javascript의 개념  (0) 2023.09.13

+ Recent posts