diff --git a/.gitignore b/.gitignore index 6720af24..9ea6d579 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db +app/out/ diff --git a/app/src/main/java/com/codesoom/assignment/controllers/HelloController.java b/app/src/main/java/com/codesoom/assignment/controllers/HelloController.java new file mode 100644 index 00000000..77fe6f58 --- /dev/null +++ b/app/src/main/java/com/codesoom/assignment/controllers/HelloController.java @@ -0,0 +1,14 @@ +package com.codesoom.assignment.controllers; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @GetMapping + public String sayHello() { + return "Hello, world!"; + } + +} diff --git a/app/src/test/java/com/codesoom/assignment/application/TaskServiceTest.java b/app/src/test/java/com/codesoom/assignment/application/TaskServiceTest.java new file mode 100644 index 00000000..6e5073e2 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/application/TaskServiceTest.java @@ -0,0 +1,196 @@ +package com.codesoom.assignment.application; + +import com.codesoom.assignment.TaskNotFoundException; +import com.codesoom.assignment.models.Task; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("TaskService 클래스") +class TaskServiceTest { + + private TaskService taskService; + private Task task; + + private static final String TASK_TITLE = "test"; + private static final String UPDATE_POSTFIX = "!!!"; + + private static final Long EXISTING_ID = 1L; + private static final Long NOT_EXISTING_ID = 100L; + + @BeforeEach + void setUp() { + taskService = new TaskService(); + + task = new Task(); + task.setTitle(TASK_TITLE); + } + + @Nested + @DisplayName("getTasks") + class Describe_getTasks { + @Nested + @DisplayName("저장된 task가 여러개 있다면") + class Context_with_tasks { + @BeforeEach + void prepareTasks() { + taskService.createTask(task); + taskService.createTask(task); + } + + @Test + @DisplayName("task list를 리턴한다") + void it_returns_task_list() { + assertThat(taskService.getTasks()).hasSize(2); + } + } + + @Nested + @DisplayName("저장된 task가 없다면") + class Context_with_empty_tasks { + @Test + @DisplayName("빈 task list를 리턴한다") + void it_returns_empty_task_list() { + assertThat(taskService.getTasks()).isEmpty(); + } + } + } + + @Nested + @DisplayName("getTask") + class Describe_getTask { + @BeforeEach + void prepareTask() { + taskService.createTask(task); + } + + @Nested + @DisplayName("존재하는 task id가 주어진다면") + class Context_with_an_existing_task_id { + @Test + @DisplayName("task를 리턴한다") + void it_returns_a_task() { + Task task = taskService.getTask(EXISTING_ID); + + assertThat(task.getTitle()).isEqualTo(TASK_TITLE); + } + } + + @Nested + @DisplayName("존재하지 않는 task id가 주어진다면") + class Context_with_not_an_existing_task_id { + @Test + @DisplayName("TaskNotFoundException을 던진다") + void it_throws_task_not_found_exception() { + assertThatThrownBy(() -> taskService.getTask(NOT_EXISTING_ID)) + .isInstanceOf(TaskNotFoundException.class); + } + } + } + + @Nested + @DisplayName("createTask") + class Describe_createTask { + int oldSize; + Task newTask; + + @BeforeEach + void prepareTask() { + oldSize = taskService.getTasks().size(); + + newTask = new Task(); + newTask.setTitle(TASK_TITLE); + } + + @Test + @DisplayName("생성된 task를 리턴한다") + void it_returns_a_created_task() { + Task createdTask = taskService.createTask(newTask); + + int newSize = taskService.getTasks().size(); + + assertThat(createdTask.getTitle()).isEqualTo(TASK_TITLE); + assertThat(newSize - oldSize).isEqualTo(1); + } + + } + + @Nested + @DisplayName("updateTask") + class Describe_updateTask { + Task source; + + @BeforeEach + void prepareTask() { + taskService.createTask(task); + + source = new Task(); + source.setTitle(TASK_TITLE + UPDATE_POSTFIX); + } + + @Nested + @DisplayName("존재하는 task id가 주어진다면") + class Context_with_an_existing_task_id { + @Test + @DisplayName("수정된 task를 리턴한다") + void it_returns_a_updated_task() { + Task updatedTask = taskService.updateTask(EXISTING_ID, source); + + assertThat(updatedTask.getTitle()).isEqualTo(TASK_TITLE + UPDATE_POSTFIX); + } + } + + @Nested + @DisplayName("존재하지 않는 task id가 주어진다면") + class Context_with_not_an_existing_task_id { + @Test + @DisplayName("TaskNotFoundException을 던진다") + void it_throws_task_not_found_exception() { + assertThatThrownBy(() -> taskService.updateTask(NOT_EXISTING_ID, source)) + .isInstanceOf(TaskNotFoundException.class); + } + } + } + + @Nested + @DisplayName("deleteTask") + class Describe_deleteTask { + int oldSize; + + @BeforeEach + void prepareTask() { + taskService.createTask(task); + + oldSize = taskService.getTasks().size(); + } + + @Nested + @DisplayName("존재하는 task id가 주어진다면") + class Context_with_an_existing_task_id { + @Test + @DisplayName("task를 삭제한다") + void it_deletes_a_task() { + taskService.deleteTask(EXISTING_ID); + + int newSize = taskService.getTasks().size(); + + assertThat(oldSize - newSize).isEqualTo(1); + } + } + + @Nested + @DisplayName("존재하지 않는 task id가 주어진다면") + class Context_with_not_an_existing_task_id { + @Test + @DisplayName("TaskNotFoundException을 던진다") + void it_throws_task_not_found_exception() { + assertThatThrownBy(() -> taskService.deleteTask(NOT_EXISTING_ID)) + .isInstanceOf(TaskNotFoundException.class); + } + } + } +} diff --git a/app/src/test/java/com/codesoom/assignment/controllers/HelloControllerTest.java b/app/src/test/java/com/codesoom/assignment/controllers/HelloControllerTest.java new file mode 100644 index 00000000..860177b1 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/controllers/HelloControllerTest.java @@ -0,0 +1,16 @@ +package com.codesoom.assignment.controllers; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +class HelloControllerTest { + + @Test + void sayHello() { + HelloController controller = new HelloController(); + + assertThat(controller.sayHello()).isEqualTo("Hello, world!"); + } + +} diff --git a/app/src/test/java/com/codesoom/assignment/controllers/HelloControllerWebTest.java b/app/src/test/java/com/codesoom/assignment/controllers/HelloControllerWebTest.java new file mode 100644 index 00000000..77b8fdb4 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/controllers/HelloControllerWebTest.java @@ -0,0 +1,29 @@ +package com.codesoom.assignment.controllers; + +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.test.web.servlet.MockMvc; + +import static org.hamcrest.Matchers.containsString; +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; + +@SpringBootTest +@AutoConfigureMockMvc +public class HelloControllerWebTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void sayHello() throws Exception { + mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Hello, world!"))); + + } + +} diff --git a/app/src/test/java/com/codesoom/assignment/controllers/TaskControllerTest.java b/app/src/test/java/com/codesoom/assignment/controllers/TaskControllerTest.java new file mode 100644 index 00000000..bd02a336 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/controllers/TaskControllerTest.java @@ -0,0 +1,109 @@ +package com.codesoom.assignment.controllers; + +import com.codesoom.assignment.TaskNotFoundException; +import com.codesoom.assignment.application.TaskService; +import com.codesoom.assignment.models.Task; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TaskControllerTest { + + private TaskController controller; + private TaskService taskService; + + private static final String TASK_TITLE = "test"; + private static final String UPDATE_POSTFIX = "!!!"; + + private static final Long EXISTING_ID = 1L; + private static final Long NOT_EXISTING_ID = 100L; + + @BeforeEach + void setUp() { + taskService = new TaskService(); + controller = new TaskController(taskService); + + // fixtures + Task task = new Task(); + task.setTitle(TASK_TITLE); + controller.create(task); + } + + @Test + void list() { + assertThat(controller.list()).hasSize(1); + } + + @Test + void detail() { + Task found = controller.detail(EXISTING_ID); + + assertThat(found.getId()).isEqualTo(EXISTING_ID); + assertThat(found.getTitle()).isEqualTo(TASK_TITLE); + } + + @Test + void detailWithNotExistingId() { + assertThatThrownBy(() -> controller.detail(NOT_EXISTING_ID)) + .isInstanceOf(TaskNotFoundException.class); + } + + @Test + void create() { + int oldSize = controller.list().size(); + + Task task = new Task(); + task.setTitle(TASK_TITLE); + controller.create(task); + + int newSize = controller.list().size(); + + assertThat(newSize - oldSize).isEqualTo(1); + } + + @Test + void update() { + Task source = new Task(); + source.setTitle(TASK_TITLE + UPDATE_POSTFIX); + controller.update(EXISTING_ID, source); + + Task task = controller.detail(EXISTING_ID); + assertThat(task.getTitle()).isEqualTo(TASK_TITLE + UPDATE_POSTFIX); + } + + @Test + void updateWithNotExistingId() { + Task source = new Task(); + source.setTitle(TASK_TITLE + UPDATE_POSTFIX); + + assertThatThrownBy(() -> controller.update(NOT_EXISTING_ID, source)) + .isInstanceOf(TaskNotFoundException.class); + } + + @Test + void delete() { + controller.delete(EXISTING_ID); + + assertThatThrownBy(() -> controller.delete(EXISTING_ID)) + .isInstanceOf(TaskNotFoundException.class); + } + + @Test + void deleteWithNotExistingId() { + assertThatThrownBy(() -> controller.delete(NOT_EXISTING_ID)) + .isInstanceOf(TaskNotFoundException.class); + } + + @Test + void patch() { + update(); + } + + @Test + void patchWithNotExistId() { + updateWithNotExistingId(); + } + +} diff --git a/app/src/test/java/com/codesoom/assignment/controllers/TaskControllerWebTest.java b/app/src/test/java/com/codesoom/assignment/controllers/TaskControllerWebTest.java new file mode 100644 index 00000000..cc145394 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/controllers/TaskControllerWebTest.java @@ -0,0 +1,328 @@ +package com.codesoom.assignment.controllers; + +import com.codesoom.assignment.TaskNotFoundException; +import com.codesoom.assignment.application.TaskService; +import com.codesoom.assignment.models.Task; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest +@DisplayName("TaskController 클래스") +public class TaskControllerWebTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private TaskService taskService; + + private static final String TASK_TITLE = "test"; + + private static final Long EXISTING_ID = 1L; + private static final Long NOT_EXISTING_ID = 100L; + + private List tasks; + private Task task; + + @BeforeEach + void setUp() { + tasks = new ArrayList<>(); + task = new Task(); + task.setId(EXISTING_ID); + task.setTitle(TASK_TITLE); + } + + @AfterEach + void clear() { + Mockito.reset(taskService); + } + + @Nested + @DisplayName("GET 요청은") + class Describe_GET { + @Nested + @DisplayName("저장된 task가 여러개 있다면") + class Context_with_tasks { + @BeforeEach + void setUp() { + tasks.add(task); + tasks.add(task); + given(taskService.getTasks()) + .willReturn(tasks); + } + + @Test + @DisplayName("모든 task들과 상태코드 200을 응답한다") + void list() throws Exception { + mockMvc.perform(get("/tasks") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(content().string(containsString(TASK_TITLE))) + .andExpect(content().string(objectMapper.writeValueAsString(tasks))) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("저장된 task가 없다면") + class Context_without_tasks { + @BeforeEach + void setUp() { + given(taskService.getTasks()) + .willReturn(tasks); + } + + @Test + @DisplayName("비어있는 배열과 상태코드 200을 응답한다") + void listWithEmptyTasks() throws Exception { + mockMvc.perform(get("/tasks") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(content().string("[]")) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("존재하는 task id가 주어진다면") + class Context_with_an_existing_task_id { + @BeforeEach + void setUp() { + given(taskService.getTask(EXISTING_ID)) + .willReturn(task); + } + + @Test + @DisplayName("찾은 task와 상태코드 200을 응답한다") + void detail() throws Exception { + mockMvc.perform(get("/tasks/{id}", EXISTING_ID) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("title").exists()) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("존재하지 않는 task id가 주어진다면") + class Context_with_not_an_existing_task_id { + @BeforeEach + void setUp() { + given(taskService.getTask(NOT_EXISTING_ID)) + .willThrow(new TaskNotFoundException(NOT_EXISTING_ID)); + } + + @Test + @DisplayName("에러메시지와 상태코드 404를 응답한다") + void detailWithNotExistingId() throws Exception { + mockMvc.perform(get("/tasks/{id}", NOT_EXISTING_ID) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("id").doesNotExist()) + .andExpect(jsonPath("title").doesNotExist()) + .andExpect(jsonPath("message").exists()) + .andExpect(status().isNotFound()); + } + } + } + + @Nested + @DisplayName("POST 요청은") + class Describe_POST { + @Nested + @DisplayName("task가 주어진다면") + class Context_with_task { + @BeforeEach + void setUp() { + given(taskService.createTask(any(Task.class))) + .willReturn(task); + } + + @Test + @DisplayName("생성된 task와 상태코드 201을 응답한다") + void create() throws Exception { + mockMvc.perform(post("/tasks") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(task))) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("title").exists()) + .andExpect(status().isCreated()); + } + } + } + + @Nested + @DisplayName("PUT 요청은") + class Describe_PUT { + @Nested + @DisplayName("존재하는 task id가 주어진다면") + class Context_with_an_existing_task_id { + @BeforeEach + void setUp() { + given(taskService.updateTask(eq(EXISTING_ID), any(Task.class))) + .willReturn(task); + } + + @Test + @DisplayName("수정된 task와 상태코드 200을 응답한다") + void update() throws Exception { + mockMvc.perform(put("/tasks/{id}", EXISTING_ID) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(task))) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("title").exists()) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("존재하지 않는 task id가 주어진다면") + class Context_with_not_an_existing_task_id { + @BeforeEach + void setUp() { + given(taskService.updateTask(eq(NOT_EXISTING_ID), any(Task.class))) + .willThrow(new TaskNotFoundException(NOT_EXISTING_ID)); + } + + @Test + @DisplayName("에러메시지와 상태코드 404를 응답한다") + void updateWithNotExistingId() throws Exception { + mockMvc.perform(put("/tasks/{id}", NOT_EXISTING_ID) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(task))) + .andExpect(jsonPath("id").doesNotExist()) + .andExpect(jsonPath("title").doesNotExist()) + .andExpect(status().isNotFound()); + } + } + } + + @Nested + @DisplayName("PATCH 요청은") + class Describe_PATCH { + @Nested + @DisplayName("존재하는 task id가 주어진다면") + class Context_with_an_existing_task_id { + @BeforeEach + void setUp() { + given(taskService.updateTask(eq(EXISTING_ID), any(Task.class))) + .willReturn(task); + } + + @Test + @DisplayName("수정된 task와 상태코드 200을 응답한다") + void update() throws Exception { + mockMvc.perform(patch("/tasks/{id}", EXISTING_ID) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(task))) + .andExpect(jsonPath("id").exists()) + .andExpect(jsonPath("title").exists()) + .andExpect(status().isOk()); + } + } + + @Nested + @DisplayName("존재하지 않는 task id가 주어진다면") + class Context_with_not_an_existing_task_id { + @BeforeEach + void setUp() { + given(taskService.updateTask(eq(NOT_EXISTING_ID), any(Task.class))) + .willThrow(new TaskNotFoundException(NOT_EXISTING_ID)); + } + + @Test + @DisplayName("에러메시지와 상태코드 404를 응답한다") + void updateWithNotExistingId() throws Exception { + mockMvc.perform(patch("/tasks/{id}", NOT_EXISTING_ID) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(task))) + .andExpect(jsonPath("id").doesNotExist()) + .andExpect(jsonPath("title").doesNotExist()) + .andExpect(status().isNotFound()); + } + } + } + + @Nested + @DisplayName("DELETE 요청은") + class Describe_DELETE { + @Nested + @DisplayName("존재하는 task id가 주어진다면") + class Context_with_an_existing_task_id { + @BeforeEach + void setUp() { + given(taskService.deleteTask(eq(EXISTING_ID))) + .willReturn(task); + } + + @Test + @DisplayName("상태코드 204를 응답한다") + void deleteTask() throws Exception { + mockMvc.perform(delete("/tasks/{id}", EXISTING_ID) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("id").doesNotExist()) + .andExpect(jsonPath("title").doesNotExist()) + .andExpect(status().isNoContent()); + } + } + + @Nested + @DisplayName("존재하지 않는 task id가 주어진다면") + class Context_with_not_an_existing_task_id { + @BeforeEach + void setUp() { + given(taskService.deleteTask(eq(NOT_EXISTING_ID))) + .willThrow(new TaskNotFoundException(NOT_EXISTING_ID)); + } + + @Test + @DisplayName("에러메시지와 상태코드 404를 응답한다") + void deleteWithNotExistingId() throws Exception { + mockMvc.perform(delete("/tasks/{id}", NOT_EXISTING_ID) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("id").doesNotExist()) + .andExpect(jsonPath("title").doesNotExist()) + .andExpect(jsonPath("message").exists()) + .andExpect(status().isNotFound()); + } + } + } + +} diff --git a/app/src/test/java/com/codesoom/assignment/models/TaskTest.java b/app/src/test/java/com/codesoom/assignment/models/TaskTest.java new file mode 100644 index 00000000..d8a37214 --- /dev/null +++ b/app/src/test/java/com/codesoom/assignment/models/TaskTest.java @@ -0,0 +1,22 @@ +package com.codesoom.assignment.models; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TaskTest { + + @Test + void create() { + Long id = 10L; + String title = "test"; + + Task task = new Task(); + task.setId(id); + task.setTitle(title); + + assertThat(task.getId()).isEqualTo(id); + assertThat(task.getTitle()).isEqualTo(title); + } + +}