<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                既然是常用的功能,那么spring必然已經有了最佳實踐。在進行最佳實踐前,我們來簡單匯制下時序圖: ![](https://img.kancloud.cn/57/4b/574b504d0ad95f90f9dc703efacbef3f_1040x443.png) 考慮到該功能實現的復雜性,我們在此使用敏捷開發(agile development)的方法,先開發分頁功能,再開發綜合查詢功能。 # CurdRepository 無論經過多少次轉發,最終實現數據分頁查詢的必然是倉庫層。StudentRepository繼承了CurdRepository,進行spring為其自動實現了一些基本的增改查刪的功能。我們打開CurdRepository來簡單瀏覽一下這個文件: CurdRepository ``` package org.springframework.data.repository; import java.util.Optional; @NoRepositoryBean public interface CrudRepository<T, ID> extends Repository<T, ID> { <S extends T> S save(S var1); ? <S extends T> Iterable<S> saveAll(Iterable<S> var1); ? Optional<T> findById(ID var1); ? boolean existsById(ID var1); ? Iterable<T> findAll(); ? Iterable<T> findAllById(Iterable<ID> var1); ? long count(); ? void deleteById(ID var1); ? void delete(T var1); ? void deleteAll(Iterable<? extends T> var1); ? void deleteAll(); ? } ``` * ? 新增/更新功能 * ? 查詢功能 * ? 刪除功能 通過查看我們發現其提供的查詢功能中并沒有找到我們需要的分頁功能。的確是這樣,在spring中CurdRepository只提供了基本的增改查刪功能,如果想實現更復雜的分頁功能,則需要繼承其它的接口。 # PagingAndSortingRepository spring為我們提供了`org.springframework.data.repository.PagingAndSortingRepository;`來滿足對分頁功能的需求,要想使用此接口給我們帶來的功能,只需要繼承該接口即可. repository/StudentRepository.java ``` package com.mengyunzhi.springBootStudy.repository; import com.mengyunzhi.springBootStudy.entity.Student; import org.springframework.data.repository.PagingAndSortingRepository; ① /** * 學生 */ public interface StudentRepository extends PagingAndSortingRepository<Student, Long>② { } ``` * ① 使用前先引入 * ② 和CrudRepository相同,繼承該接口時,需要指定實體類型及實體的主健類型 此時,我們應該有個疑問:在歷史的代碼中,我們是通過間接調用CrudRepository的save方法來完成的數據新增功能。而當前修改了繼承的接口,那么以前代碼中間接調用CrudRepository.save方法還可以正常工作嗎?為此,我們借助idea來看一下當前接口的繼承關系: ![](https://img.kancloud.cn/28/fb/28fb948d0e6956479527ee5163ee6d8a_639x492.png) 依圖所示,StudentRepository繼承了PagingAndSortingRepository,PagingAndSortingRepository又繼承了CrudRepository。因而我們在歷史的代碼中書寫的學生保存的相關功能性代碼仍然可用。在調用studentRepository的save方法時,它會按照繼承的原則:此類沒有則轉向父類、父類沒有則轉向父父類,依此累推,最終仍然會調用到CrudRepository的save方法。 PagingAndSortingRepository中有兩個方法: ``` @NoRepositoryBean public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> { Iterable<T> findAll(Sort var1); ? Page<T> findAll(Pageable var1); ? } ``` * ? 接收的參數類型為**排序**,返回值為**迭代器**,**迭代器**可以認為是數組的一種,與數組不同的是:我們獲取數組中的子項時,不能夠再使用索引的方法,而只能使用其它特定的方法。 * ? 接收的參數類型為\*\*(可)分頁\*\*,返回值為**含有總頁數及當前頁數組的特定類型**。Page類型除包含總頁數、當前頁數據外,還包含了第幾頁、每頁大小、總條數、排序規則、是否首頁、是否尾頁、是否還有下一頁、是否還有上一頁等其它的與分頁相關的信息。 # 獲取分頁數據 要想獲取分頁數據,首先需要獲取一個實現了Pageable接口的對象,該對象可使用`Pageable pageable = PageRequest.of(page, size)`來初始化。比如我們想獲取每頁10條情況下,第1頁的數據則可以使用如下的方法: repository/StudentRepositoryTest.java(請新建) ``` package com.mengyunzhi.springBootStudy.repository; 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.data.domain.Page; ① import org.springframework.data.domain.PageRequest; ② import org.springframework.data.domain.Pageable; ③ import org.springframework.test.context.junit4.SpringRunner; @SpringBootTest @RunWith(SpringRunner.class) public class StudentRepositoryTest { @Autowired StudentRepository studentRepository; @Test public void page() { Pageable pageable = PageRequest.of(0, 10); ? Page<Student> studentPage = studentRepository.findAll(pageable); ? return; ? } } ``` * ①②引入特定的類,由于有多個重名的類,所以在此處需要注意該類的位置。 * ? 初始化第0頁、每頁10條的分頁查詢條件 * ? 查詢分頁數據 * ? 加個冗余的return用于debug程序 接著我們在此處打個斷點: ![](https://img.kancloud.cn/4f/20/4f20c13909259ca4e6a1e1b1700485b0_753x211.png) 然后用debug模式啟動該單元測試 ![](https://img.kancloud.cn/c5/c8/c5c8593e184e27fcbe27ec67cff49751_565x223.png) 并展開studentPage如下: ![](https://img.kancloud.cn/39/c6/39c6dd26104cb2296539bcb0f4e9c600_518x277.png) 上圖所示,返回值Page中含有: * 數據總條數0 * 當前面數據content * 分頁信息pageable * 當前為第0頁 * 每頁10條數據 將如上對象直接返回給前臺,完全可以滿足我們的當前需求。 ## 數據測試 接下來,我們在測試中加入測試數據,再次debug看看實際的返回值 ``` @Autowired KlassRepository klassRepository; @Test public void page() { Klass klass = new Klass(); klass.setName("testKlass"); this.klassRepository.save(klass); for (int i = 0; i < 100; i++) { Student student = new Student(); student.setName(RandomString.make(4)); student.setSno(RandomString.make(6)); student.setKlass(klass); this.studentRepository.save(student); } Pageable pageable = PageRequest.of(2, 15); Page<Student> studentPage = studentRepository.findAll(pageable); return; } ``` 再次debug中斷查看: ![](https://img.kancloud.cn/7f/49/7f4977737b1371780a12f187c14e7349_725x306.png) 查看content: ![](https://img.kancloud.cn/34/4b/344b4f0f8b1e65c281f88b3c3fa63973_467x280.png) 如此,我們便有了實現數據分頁功能的基礎。 # M層 當我們第一次使用某個功能的時候(還處于解決技術障礙中),我們的首頁目標是借助于debug來弄清楚該功能的具體使用方法,傳入值與返回的類型等,單元測試應該放在后面進行補充。TDD測試驅動開發僅限于我們對某個功能的實現不存在技術上的障礙時。在此,我們先完成M層的功能部分,再對應進行測試代碼的編寫. service/StudentService.java ``` /** * 查詢分頁信息 * * @param pageable 分頁條件 * @return 分頁數據 */ Page<Student> findAll(Pageable pageable); ``` service/StudentServiceImpl.java ``` @Override public Page<Student> findAll(Pageable pageable) { return this.studentRepository.findAll(pageable); } ``` ## 單元測試 按前面的經驗, 整理單元測試代碼如下: service/StudentServiceImplTest.java ``` /** * 分頁查詢 * 1. 模擬輸入、輸出、調用studentRepository * 2. 調用測試方法 * 3. 斷言輸入與輸出與模擬值相符 */ @Test public void findAll() { Pageable mockInPageable = PageRequest.of(1, 20); ① List<Student> mockStudents = Arrays.asList(new Student()); ② Page<Student> mockOutStudentPage = new PageImpl<Student>( mockStudents, PageRequest.of(1, 20), 21); ? Mockito.when(this.studentRepository.findAll(Mockito.any(Pageable.class))) .thenReturn(mockOutStudentPage); ④ Page<Student> studentPage = this.studentService.findAll(mockInPageable); ⑤ Assertions.assertThat(studentPage).isEqualTo(mockOutStudentPage); ⑥ ArgumentCaptor<Pageable> pageableArgumentCaptor = ArgumentCaptor.forClass(Pageable.class); Mockito.verify(this.studentRepository).findAll(pageableArgumentCaptor.capture()); Assertions.assertThat(pageableArgumentCaptor.getValue()).isEqualTo(mockInPageable); ⑦ ``` * ① 模擬輸入 * ② 初始化返回分頁信息的本頁數據部分 * ③ 使用 本頁數據、 分頁情況、總條件來初始化模擬返回值Page<Student> * ④ 模擬studentRepository.findAll方法的返回值 * ⑤ 調用被測試方法 * ⑥ 斷言返回值 * ⑦ 斷言傳入參數 ![](https://img.kancloud.cn/6e/c3/6ec37d065a5c2efbe7b12f9df392e6fd_431x139.png) # C層 C層的代碼也很簡單: controller/StudentController.java ``` @GetMapping public Page<Student> findAll(@RequestParam int page, @RequestParam int size) { return this.studentService.findAll(PageRequest.of(page, size)); } ``` ## 單元測試一 為了更清楚的了解真實情況的返回值,我們暫且將單元測試中StudentService的注解由@MockBean改為@Autowired,然后模擬添加一些數據,看看真實情況下會給我們返回什么樣的數據(注意:這違背了單元測試的原則。在單元測試中,我們的測試內容應該圍繞輸入與輸出展開。對于被測試方法在執行期間調用其它的方法的,應該使用MOCK來進行模擬)。 contoroller/StudentControllerTest.java ``` @Autowired ? @MockBean ? private StudentService studentService; @Autowired private KlassRepository klassRepository; ① @Autowired private StudentRepository studentRepository; ① @Test public void findAll() throws Exception { logger.info("準備100條測試數據"); Klass klass = new Klass(); klass.setName("testKlass"); this.klassRepository.save(klass); for (int i = 0; i < 100; i++) { Student student = new Student(); student.setName(RandomString.make(4)); student.setSno(RandomString.make(6)); student.setKlass(klass); this.studentRepository.save(student); } logger.info("每頁2條,請求第1頁數據"); String url = "/Student?page=49&size=2"; ② this.mockMvc.perform(MockMvcRequestBuilders.get(url)) .andDo(MockMvcResultHandlers.print()) ③ .andExpect(MockMvcResultMatchers.status().isOk()); } ``` * ① 引入數據倉庫 * ② 將每頁大小、當前頁兩個查詢參數直接拼接到URL中 * ③ 在控制臺中打印返回的結果 啟動單元測試后在控制臺中得到如下返回信息: ``` Body = {"content":[{"id":99,"name":"FtJf","sno":"56IhJV","klass":{"id":1,"teacher":null,"name":"testKlass"}},{"id":100,"name":"WHpT","sno":"YVwSqA","klass":{"id":1,"teacher":null,"name":"testKlass"}}],"pageable":{"sort":{"sorted":false,"unsorted":true,"empty":true},"offset":98,"pageSize":2,"pageNumber":49,"paged":true,"unpaged":false},"totalPages":50,"totalElements":100,"last":true,"size":2,"number":49,"numberOfElements":2,"first":false,"sort":{"sorted":false,"unsorted":true,"empty":true},"empty":false} ``` 對其進行格式化后如下: ``` { ① "content": [ ②③ { ④ "id": 99, "name": "FtJf", "sno": "56IhJV", "klass": { "id": 1, "teacher": null, "name": "testKlass" } }, { ④ "id": 100, ⑤ "name": "WHpT", ⑤ "sno": "YVwSqA", ⑤ "klass": ⑤ { "id": 1, ⑥ "teacher": null, ⑥ "name": "testKlass" ⑥ } }], "pageable": ②⑦ { "sort": { "sorted": false, "unsorted": true, "empty": true }, "offset": 98, "pageSize": 2, "pageNumber": 49, "paged": true, "unpaged": false }, "totalPages": 50, ② "totalElements": 100, ② "last": true, ② "size": 2, ② "number": 49, ② "numberOfElements": 2, ② "first": false, ② "sort": ②⑧ { "sorted": false, "unsorted": true, "empty": true }, "empty": false ② } ``` * ① 返回值為一個對象 Page * ② 對象①的各個屬性 * ③ 當前頁內容 Array<Student> * ④ 數組中有兩個對象 Student * ⑤ Student對象④的屬性 * ⑥ Klass對象⑤的屬性 * ⑦ 分頁條件信息 * ⑧ 排序條件信息 如上所示,spring不僅僅返回了當前頁的數據、分頁條件、總頁數、數據總數信息,還返回了是否尾頁、每頁大小、當前頁碼(0基)、當前頁數據條數、是否首頁、排序、當前數據是否為空信息。這些數據為前臺提供了良好的支持。 ## 單元測試二 讓我們恢復剛剛的測試,繼續使用模擬的服務層來完成C層的測試。 ``` @Autowired ? @MockBean ? private StudentService studentService; @Autowired ? private KlassRepository klassRepository; ? @Autowired ? private StudentRepository studentRepository; ? @Test public void findAll() throws Exception { logger.info("初始化模擬返回數據"); List<Student> students = new ArrayList<>(); Klass klass = new Klass(); klass.setId(-2L); for (long i = 0; i < 2; i++) { Student student = new Student(); student.setId(-i - 1); student.setSno(RandomString.make(6)); student.setName(RandomString.make(4)); student.setKlass(klass); students.add(student); } logger.info("初始化分頁信息及設置模擬返回數據"); Page<Student> mockOutStudentPage = new PageImpl<Student>( students, PageRequest.of(1, 2), 4 ); Mockito.when(this.studentService.findAll(Mockito.any(Pageable.class))) .thenReturn(mockOutStudentPage); logger.info("以'每頁2條,請求第1頁'為參數發起請求,斷言返回狀態碼為200,并接收響應數據"); String url = "/Student"; MvcResult mvcResult = this.mockMvc.perform( MockMvcRequestBuilders.get(url) .param("page", "1") .param("size", "2")) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); logger.info("將返回值由string轉為json,并斷言接收到了分頁信息"); LinkedHashMap① returnJson = JsonPath.parse(mvcResult.getResponse().getContentAsString()).json(); Assertions.assertThat(returnJson.get("totalPages")).isEqualTo(2); // 總頁數 Assertions.assertThat(returnJson.get("totalElements")).isEqualTo(4); // 總條數 Assertions.assertThat(returnJson.get("size")).isEqualTo(2); // 每頁大小 Assertions.assertThat(returnJson.get("number")).isEqualTo(1); // 第幾頁(0基) Assertions.assertThat(returnJson.get("numberOfElements")).isEqualTo(2); // 當前頁條數 //todo: 斷言獲取到了content,類型為數組 ? return; } ``` 通過??標記可以看到,由于Mock的加入,在進行C層功能代碼的測試時,我們僅僅需要考慮C層直接調用的服務層StudentService.findAll的輸入輸出即可,而StudentService是否實現了其描述的功能以及如何實現的其描述的功能,我們完全不關心也不需要關心。 * ? 只所以在這里出現todo,是由于①LinkedHashMap這個容器可以裝入任意類型,所以我們無法通過returnJson.get("content")來獲取其content的數據類型。在第一次接觸時,我們需要debug來幫我查看content的數據類型然后繼續完成后續的操作。 為此,我們在此處打個斷點: ![](https://img.kancloud.cn/b2/cd/b2cde832b89583857f74429396e4ac69_1464x220.png) 然后啟動debug,并在debug控制臺中找到returnJson: ![](https://img.kancloud.cn/25/7d/257df9b230e33ca65de476c80d8df2ad_672x333.png) 得到具體的類型后,我們繼續完成測試: ``` import net.minidev.json.JSONArray; ★ ... @Test public void findAll() throws Exception { ... Assertions.assertThat(returnJson.get("numberOfElements")).isEqualTo(2); // 當前頁條數 logger.info("測試content"); JSONArray content = (JSONArray★) returnJson.get("content"); Assertions.assertThat(content.size()).isEqualTo(2); // 返回了2個學生 logger.info("測試返回的學生"); for (int① i = 0; i < 2; i++) { LinkedHashMap studentHashMap = (LinkedHashMap) content.get(i); // 獲取第一個學生 Assertions.assertThat(studentHashMap.get("id")).isEqualTo(-i - 1); Assertions.assertThat(studentHashMap.get("name").toString().length()).isEqualTo(4); Assertions.assertThat(studentHashMap.get("sno").toString().length()).isEqualTo(6); logger.info("測試返回學生所在的班級"); LinkedHashMap klassHashMap = (LinkedHashMap) studentHashMap.get("klass"); Assertions.assertThat(klassHashMap.get("id")).isEqualTo(-2); Assertions.assertThat(klassHashMap.get("name")).isEqualTo("test klass name"); } return; } ``` * ★ 注意此處的類型為:net.minidev.json.JSONArray * ① 此處是int,不是long。 >[success] 在C層的單元測試中,對每個前臺需要的測試都加入相應的斷言是非常有必要的。在生產項目中如果未對C層的輸出字段進行斷言,則必然發生在后臺的敏捷開發中造成前臺部分功能失效的問題。 # 總結 我們在本小節中花費了大量的精力來編寫單元測試。在編寫的過程中我們感受到:編寫單元測試的難度遠遠超出了編寫功能代碼的難度;編寫單元測試的時間遠遠的超出了編寫功能代碼的時間。而這,是非常有必要的。在軟件開發的所有的專業課中,軟件工程是在學習的時候最不容易引起重視但卻在實戰中起出保障軟件質量關鍵一環的核心課程。如果你不希望自己以后編寫的軟件在每次更新后都會發生或多或少的非預期錯誤,如果你不希望自己本已經編寫好的功能在其它團隊成員的協助開發下變得不可用,如果你想做一個對前臺負責的后臺開發工程師、如果你不想在新的版本上線后天天打噴嚏、如果你希望隨著需求的發展及新技術的普通而能夠放開手腳的重構代碼、如果你的目標是Engineer而不是Programmer,那么從現在起請注重**單元測試**! # 參考文檔 | 名稱 | 鏈接 | 預計學習時長(分) | | --- | --- | --- | | 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.1](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.1) | - |
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看