총 인원 N명이 설 수 있는 모든 경우의 수 중 K의 경우가 몇번쨰인지 구하시오.
ex) N = 3일경우, [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
K = [2, 3, 1]라면 정답은 3
처음에는 예시와 같이 이중배열을 만들어서 K의 인덱스를 찾을까 싶었지만 더 간단한 방법이 떠올랐다.
아래와 같이 K의 요소와 길이를 통해 몇가지 경우의 수가 앞에서 지나갔는지 카운팅하는 것이다.
public class FindDoubleArrangement { // 규칙이 있는 이중배열에서 k의 인덱스 찾기
public int orderOfPresentation(int N, int[] K) {
int result = 0;
for(int i=0; i<K.length; i++){
result = result + numberOfCase(K[i],N-i);
}
return result;
}
public int numberOfCase(int num, int N){
int result = 0;
result = result + (num-1)*factorial(N-1);
return result;
}
public int factorial(int num){
if(num<=0) return 0;
if(num==1) return 1;
return num*factorial(num-1);
}
}
//입력
N = 3, K = [2, 3, 1]
//출력
4
앞에 요소가 이미 사용한 숫자인 case는 경우의 수에서 제외해야하는데 깜빡했다.
public class FindDoubleArrangement { // 규칙이 있는 이중배열에서 k의 인덱스 찾기
public int orderOfPresentation(int N, int[] K) {
int result = 0;
ArrayList<Integer> arrayList = new ArrayList<Integer>();
for(int i=0; i<K.length; i++){
int num = K[i];
for(Integer o : arrayList){
if(o<K[i]) num--;
}
result = result + numberOfCase(num,N-i);
arrayList.add(K[i]);
}
return result;
}
public int numberOfCase(int num, int N){
int result = 0;
result = result + (num-1)*factorial(N-1);
return result;
}
public int factorial(int num){
if(num<=0) return 0;
if(num==1) return 1;
return num*factorial(num-1);
}
}
모든 테스트케이스 통과
막상 작성하고 보니 이것도 결국 이중반복문을 사용했기 때문에 시간복잡도면에서 좋다고 보기 힘들 것 같다.
public int orderOfPresentation2(int N, int[] K) {
// 조의 개수 N, 발표 순서 K
// 발표 순서를 만드는 것은 순열(permutation)이므로, 발표 순서의 모든 경우의 수는 !(팩토리얼)이다.
int order = 0;
boolean[] isUsed = new boolean[N + 1];
for (int i = 0; i < K.length; i++) {
int num = K[i];
isUsed[num] = true;
boolean[] candidates = Arrays.copyOfRange(isUsed, 1, num); // // num보다 앞에 올 수 있는 수들의 배열을 복제
int validCnt = 0;
for (boolean candidate : candidates) if (!candidate) validCnt++; // 아직 사용되지 않은 수의 개수 카운팅
int formerCnt = validCnt * factorial(N - i - 1);
order = order + formerCnt;
}
return order;
}
boolean을 이용해서 BFS알고리즘 처럼 방문체크 하듯 해봤지만 이것도 결국 순회해야하고 오히려 순회해야하는 양이 많아졌다.
일단은 시간복잡도를 더 줄일 방법이 생각나지 않아 여기까지만 고민해봤다.
단위 테스트 코드는 대부분 메서드 단위로 작성된다.
테스트 케이스(Test Case)
- 테스트를 위한 입력 데이터, 실행 조건, 기대 결과를 표현하기 위한 명세
- 메서드 등 하나의 단위를 테스트하기 위해 작성하는 테스트 코드
Assertion(어써션)
- 예상하는 결과 값이 참(true)이길 바라는 것
- 테스트 결과를 검증할 때 주로 사용하는 단어
기존에 코딩테스트 하던 것 처럼 테스트하는 방식이다.
JUnit
- Java의 테스트 프레임워크 중 하나
- Spring Boot의 디폴트 테스트 프레임워크
@DisplayName("테스트 이름")
assertEquals(aaa, bbb)
assertNotNull()
assertThrows()
//예시
assertThrows(NullPointerException.class, () -> getCryptoCurrency("XRP"));
예외 클래스의 상속 관계를 이해한 상태에서 테스트 실행 결과를 예상해야 된다.
(NullPointerException
은 RuntimeException
을 상속하는 하위 타입이고, RuntimeException
은 Exception
을 상속하는 하위 타입이다.)
Executable
함수형 인터페이스
- assertThrows() 의 두 번째 파라미터인 람다 표현식은 JUnit에서 지원하는 Executable 함수형 인터페이스다.
- Java에서 지원하는 함수형 인터페이스 중에서 리턴값이 없는
Consumer
에 해당 (리턴값이 없음)
Consumer 함수형 인터페이스
Supplier 함수형 인터페이스
Function 함수형 인터페이스
Predicate 함수형 인터페이스
Executable 함수형 인터페이스
@BeforeEach
@BeforeAll
assertDoesNotThrow()
assertDoesNotThrow(() -> getCryptoCurrency("XRP"));
@AfterEach
@BeforeEach
와 동작 방식 같음@AfterAll
@BeforeAll
과 동작 방식 같음Assumption 기능 = 특정 환경에만 테스트 케이스가 실행 되도록 하는 기능
assumeTrue()
assertTrue()
위 두가지 메서드 모두 조건 검증에 사용되는 메서드이지만, assumeTrue 메서드는 검증이 실패해도 해당 테스트를 통과한 것으로 처리하고, 해당 테스트를 실행하지 않는다. 반면 assertTrue 메서드는 검증이 실패하면 해당 테스트를 실패로 처리한다. 따라서, assumeTrue 메서드는 전제 조건을 검증할 때 사용하고, assertTrue 메서드는 결과 값을 검증할 때 사용한다.
Hamcrest
- JUnit 기반의 단위 테스트에서 사용할 수 있는 Assertion Framework
- Assertion을 위한 매쳐(Matcher)가 자연스러운 문장으로 이어지므로 가독성이 향상 된다.
- 테스트 실패 메시지를 이해하기 쉽다.
- 다양한 Matcher를 제공한다.
슬라이스 테스트
- 개발자가 각 계층에 구현해 놓은 기능들이 잘 동작하는지 특정 계층만 잘라서(Slice) 테스트하는 것
@SpringBootTest
@AutoConfigureMockMvc
Gson 라이브러리
- JSON 데이터를 Java 객체로 변환하거나 Java 객체를 JSON 데이터로 변환하는 기능을 제공해주는 라이브러리
- Gson 라이브러리를 사용하기 위해서는 build.gradle의 dependencies {...}에
implementation 'com.google.code.gson:gson'
를 추가해야 한다.
MockMvc
클래스
mockMvc.perform()
ResultActions
타입의 객체를 리턴, ResultActions
객체를 이용해서 전송한 request에 대한 검증을 수행 가능MockMvcRequestBuilders
클래스
post("/v11/members")
accept(MediaType.APPLICATION_JSON)
contentType(MediaType.APPLICATION_JSON)
데이터 액세스 계층 테스트 시에는 DB의 상태를 테스트 케이스 실행 이전으로 되돌려서 깨끗하게 만드는 것을 지켜야 한다.
@DataJpaTest
@Transactional
애너테이션을 포함하고 있음@SpringBootTest
@DataJpaTest
import com.codestates.member.dto.MemberDto;
import com.google.gson.Gson;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
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.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.transaction.annotation.Transactional;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@Transactional
@SpringBootTest
@AutoConfigureMockMvc
public class MemberControllerHomeworkTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private Gson gson;
@Test
void postMemberTest() throws Exception {
// given
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
"홍길동",
"010-1234-5678");
String content = gson.toJson(post);
// when
ResultActions actions =
mockMvc.perform(
post("/v11/members")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
);
// then
actions
.andExpect(status().isCreated())
.andExpect(header().string("Location", is(startsWith("/v11/members/"))));
}
@Test
void patchMemberTest() throws Exception {
// TODO MemberController의 patchMember() 핸들러 메서드를 테스트하는 테스트 케이스를 여기에 작성하세요.
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com","홍길동","010-1111-1111");
String postContent = gson.toJson(post);
// ResultActions postActions =
mockMvc.perform(
post("/v11/members")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(postContent)
);
MemberDto.Patch patch = new MemberDto.Patch("홍길동","010-1111-1111");
String patchContent = gson.toJson(patch);
mockMvc.perform(
patch("/v11/members/1")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(patchContent)
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.name").value(patch.getName()))
.andExpect(jsonPath("$.data.phone").value(patch.getPhone()));
}
@Test
void getMemberTest() throws Exception {
// given: MemberController의 getMember()를 테스트하기 위해서 postMember()를 이용해 테스트 데이터를 생성 후, DB에 저장
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com","홍길동","010-1111-1111");
String postContent = gson.toJson(post);
ResultActions postActions =
mockMvc.perform(
post("/v11/members")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(postContent)
);
long memberId;
String location = postActions.andReturn().getResponse().getHeader("Location"); // "/v11/members/1"
memberId = Long.parseLong(location.substring(location.lastIndexOf("/") + 1));
// when / then
mockMvc.perform(
get("/v11/members/" + memberId)
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.email").value(post.getEmail()))
.andExpect(jsonPath("$.data.name").value(post.getName()))
.andExpect(jsonPath("$.data.phone").value(post.getPhone()));
}
@Test
void getMembersTest() throws Exception {
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com","홍길동","010-1111-1111");
String postContent = gson.toJson(post);
MemberDto.Post post2 = new MemberDto.Post("hgd2@gmail.com","둘길동","010-2222-2222");
String postContent2 = gson.toJson(post2);
mockMvc.perform(
post("/v11/members")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(postContent)
);
mockMvc.perform(
post("/v11/members")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(postContent2)
);
mockMvc.perform(get("/v11/members?page=1&size=10").accept(MediaType.APPLICATION_JSON))
.andExpectAll(
status().isOk(),
jsonPath("$.data[0].email").value(post2.getEmail()), // 페이지네이션 역순정렬 때문에 반대로 해야 함
jsonPath("$.data[1].email").value(post.getEmail())
);
// MvcResult result =
// mockMvc.perform(
// get("/v11/members?page=1&size=10")
// .accept(MediaType.APPLICATION_JSON)
// )
// .andExpect(status().isOk())
// .andExpect(jsonPath("$.data[0].email").value(post2.getEmail()))
// .andExpect(jsonPath("$.data[1].email").value(post.getEmail()));
}
@Test
void deleteMemberTest() throws Exception {
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com","홍길동","010-1111-1111");
String postContent = gson.toJson(post);
ResultActions postActions =
mockMvc.perform(
post("/v11/members")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(postContent)
);
long memberId;
String location = postActions.andReturn().getResponse().getHeader("Location");
memberId = Long.parseLong(location.substring(location.lastIndexOf("/")+1)); // Id 가져오기
mockMvc.perform(
delete("/v11/members/" + memberId)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
).andExpect(status().isNoContent());
// mockMvc.perform(
// delete("/v11/members/1")
// .accept(MediaType.APPLICATION_JSON)
// .contentType(MediaType.APPLICATION_JSON)
// ).andExpect(status().isNoContent());
}
}