diff --git a/chapter-3-spring-boot-web/src/main/java/demo/springboot/domain/Book.java b/chapter-3-spring-boot-web/src/main/java/demo/springboot/domain/Book.java index 29dda95..3102a87 100644 --- a/chapter-3-spring-boot-web/src/main/java/demo/springboot/domain/Book.java +++ b/chapter-3-spring-boot-web/src/main/java/demo/springboot/domain/Book.java @@ -60,4 +60,23 @@ public class Book implements Serializable { public void setIntroduction(String introduction) { this.introduction = introduction; } + + public Book(Long id, String name, String writer, String introduction) { + this.id = id; + this.name = name; + this.writer = writer; + this.introduction = introduction; + } + + public Book(Long id, String name) { + this.id = id; + this.name = name; + } + + public Book(String name) { + this.name = name; + } + + public Book() { + } } diff --git a/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/BookService.java b/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/BookService.java index 8fb63d1..cd895d7 100644 --- a/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/BookService.java +++ b/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/BookService.java @@ -42,4 +42,18 @@ public interface BookService { * @param id 编号 */ Book findById(Long id); + + /** + * 查找书是否存在 + * @param book + * @return + */ + boolean exists(Book book); + + /** + * 根据书名获取书籍 + * @param name + * @return + */ + Book findByName(String name); } diff --git a/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/impl/BookServiceImpl.java b/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/impl/BookServiceImpl.java index 8d63aaf..18a5cfe 100644 --- a/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/impl/BookServiceImpl.java +++ b/chapter-3-spring-boot-web/src/main/java/demo/springboot/service/impl/BookServiceImpl.java @@ -2,24 +2,40 @@ package demo.springboot.service.impl; import demo.springboot.domain.Book; import demo.springboot.service.BookService; +import demo.springboot.web.BookController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import javax.annotation.PostConstruct; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; /** * Book 业务层实现 - * + *

* Created by bysocket on 27/09/2017. */ @Service public class BookServiceImpl implements BookService { + private static final AtomicLong counter = new AtomicLong(); + + + /** + * 使用集合模拟数据库 + */ + private static List books = new ArrayList<>( + Arrays.asList( + new Book(counter.incrementAndGet(), "book"))); + + // 模拟数据库,存储 Book 信息 // 第五章《数据存储》会替换成 MySQL 存储 - private static Map BOOK_DB = new HashMap<>(); + private static Map BOOK_DB = new HashMap<>(); @Override public List findAll() { @@ -29,23 +45,40 @@ public class BookServiceImpl implements BookService { @Override public Book insertByBook(Book book) { book.setId(BOOK_DB.size() + 1L); - BOOK_DB.put(book.getId(), book); + BOOK_DB.put(book.getId().toString(), book); return book; } @Override public Book update(Book book) { - BOOK_DB.put(book.getId(), book); + BOOK_DB.put(book.getId().toString(), book); return book; } @Override public Book delete(Long id) { - return BOOK_DB.remove(id); + return BOOK_DB.remove(id.toString()); } @Override public Book findById(Long id) { - return BOOK_DB.get(id); + return BOOK_DB.get(id.toString()); + } + + @Override + public boolean exists(Book book) { + return findByName(book.getName()) != null; + } + + @Override + public Book findByName(String name) { + + for (Book book : books) { + if (book.getName().equals(name)) { + return book; + } + } + + return null; } } diff --git a/chapter-3-spring-boot-web/src/main/java/demo/springboot/web/BookController.java b/chapter-3-spring-boot-web/src/main/java/demo/springboot/web/BookController.java index 3f85249..d9069a3 100644 --- a/chapter-3-spring-boot-web/src/main/java/demo/springboot/web/BookController.java +++ b/chapter-3-spring-boot-web/src/main/java/demo/springboot/web/BookController.java @@ -2,8 +2,14 @@ package demo.springboot.web; import demo.springboot.domain.Book; import demo.springboot.service.BookService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.util.UriComponentsBuilder; import java.util.List; @@ -16,6 +22,10 @@ import java.util.List; @RequestMapping(value = "/book") public class BookController { + + private final Logger LOG = LoggerFactory.getLogger(BookController.class); + + @Autowired BookService bookService; @@ -43,8 +53,20 @@ public class BookController { * 通过 @RequestBody 绑定实体参数,也通过 @RequestParam 传递参数 */ @RequestMapping(value = "/create", method = RequestMethod.POST) - public Book postBook(@RequestBody Book book) { - return bookService.insertByBook(book); + public ResponseEntity postBook(@RequestBody Book book, UriComponentsBuilder ucBuilder) { + + LOG.info("creating new book: {}", book); + + if (book.getName().equals("conflict")){ + LOG.info("a book with name " + book.getName() + " already exists"); + return new ResponseEntity<>(HttpStatus.CONFLICT); + } + + bookService.insertByBook(book); + + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ucBuilder.path("/book/{id}").buildAndExpand(book.getId()).toUri()); + return new ResponseEntity<>(headers, HttpStatus.CREATED); } /** diff --git a/chapter-3-spring-boot-web/src/test/java/demo/springboot/web/BookControllerTest.java b/chapter-3-spring-boot-web/src/test/java/demo/springboot/web/BookControllerTest.java new file mode 100644 index 0000000..2344ee1 --- /dev/null +++ b/chapter-3-spring-boot-web/src/test/java/demo/springboot/web/BookControllerTest.java @@ -0,0 +1,198 @@ +package demo.springboot.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import demo.springboot.WebApplication; +import demo.springboot.domain.Book; +import demo.springboot.service.BookService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +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.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = WebApplication.class) +@AutoConfigureMockMvc +@TestPropertySource(locations = "classpath:application.properties") +public class BookControllerTest { + + private MockMvc mockMvc; + + @Mock + private BookService bookService; + + @InjectMocks + private BookController bookController; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + mockMvc = MockMvcBuilders + .standaloneSetup(bookController) + //.addFilters(new CORSFilter()) + .build(); + } + + + @Test + public void getBookList() throws Exception { + mockMvc.perform(get("/book") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$", hasSize(0))); + + } + + + @Test + public void test_create_book_success() throws Exception { + + Book book = createOneBook(); + + when(bookService.insertByBook(book)).thenReturn(book); + + mockMvc.perform( + post("/book/create") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(book))) + + .andExpect(status().isCreated()) + .andExpect(header().string("location", containsString("/book/1"))); + } + + + @Test + public void test_create_book_fail_404_not_found() throws Exception { + + Book book = new Book(99L, "conflict"); + + when(bookService.exists(book)).thenReturn(true); + + mockMvc.perform( + post("/book/create") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(book))) + .andExpect(status().isConflict()); + } + + @Test + public void test_get_book_success() throws Exception { + + Book book = new Book(1L, "测试获取一本书", "strongant作者", "社区 www.spring4all.com 出版社出版"); + + when(bookService.findById(1L)).thenReturn(book); + + mockMvc.perform(get("/book/{id}", 1L)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.name", is("测试获取一本书"))); + + verify(bookService, times(1)).findById(1L); + verifyNoMoreInteractions(bookService); + } + + @Test + public void test_get_by_id_fail_null_not_found() throws Exception { + when(bookService.findById(1L)).thenReturn(null); + + //TODO: 查找不到应该抛出 404 状态码, Demo 待优化 + mockMvc.perform(get("/book/{id}", 1L)) + .andExpect(status().isOk()) + .andExpect(content().string("")); + + verify(bookService, times(1)).findById(1L); + verifyNoMoreInteractions(bookService); + } + + @Test + public void test_update_book_success() throws Exception { + + Book book = createOneBook(); + + when(bookService.findById(book.getId())).thenReturn(book); + doReturn(book).when(bookService).update(book); + + mockMvc.perform( + put("/book/update", book) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(book))) + .andExpect(status().isOk()); + } + + @Test + public void test_update_book_fail_not_found() throws Exception { + Book book = new Book(999L, "测试书名1"); + + when(bookService.findById(book.getId())).thenReturn(null); + + mockMvc.perform( + put("/book/update", book) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(book))) + .andExpect(status().isOk()) + .andExpect(content().string("")); + } + + // =========================================== Delete Book ============================================ + + @Test + public void test_delete_book_success() throws Exception { + + Book book = new Book(1L, "这本书会被删除啦"); + + when(bookService.findById(book.getId())).thenReturn(book); + doReturn(book).when(bookService).delete(book.getId()); + + mockMvc.perform( + delete("/book/delete/{id}", book.getId()) + ).andExpect(status().isOk()); + } + + @Test + public void test_delete_book_fail_404_not_found() throws Exception { + Book book = new Book(1L, "这本书会被删除啦"); + + when(bookService.findById(book.getId())).thenReturn(null); + + mockMvc.perform( + delete("/book/delete/{id}", book.getId())) + .andExpect(status().isOk()); + } + + + public static String asJsonString(final Object obj) { + try { + final ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private Book createOneBook() { + Book book = new Book(); + book.setId(1L); + book.setName("测试书名1"); + book.setIntroduction("这是一本 www.spring4all.com 社区出版的很不错的一本书籍"); + book.setWriter("strongant"); + return book; + } +} \ No newline at end of file diff --git a/chapter-3-spring-boot-web/src/test/resources/application.properties b/chapter-3-spring-boot-web/src/test/resources/application.properties new file mode 100644 index 0000000..5ff0285 --- /dev/null +++ b/chapter-3-spring-boot-web/src/test/resources/application.properties @@ -0,0 +1 @@ +server.port=9090 \ No newline at end of file diff --git a/springboot-properties/springboot-properties.iml b/springboot-properties/springboot-properties.iml deleted file mode 100644 index 33f8196..0000000 --- a/springboot-properties/springboot-properties.iml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file