<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>

                # update update的開發方法較getById方法稍微復雜一些有限。按開發的步驟首先進行初始化工作如下: 服務接口: ``` public interface StudentService { ... /** * 更新學生 * @param id ID * @param student 更新的學生信息 * @return 學生 */ Student update(Long id, Student student); ``` 服務實現類: ``` public class StudentServiceImpl implements StudentService { ... @Override public Student update(Long id, Student student) { return null; } ``` C層: ``` public class StudentController { ... public Student update(Long id, Student student) { return null; } ``` ## M層功能開發與測試 做為新手,在進行更新的代碼編寫前應該首先來到[3.4.5](http://www.hmoore.net/yunzhiclub/springboot_angular_guide/1368364)、[3.4.6](http://www.hmoore.net/yunzhiclub/springboot_angular_guide/1368365) 回顧一下更新數據的思想。有了參考的代碼后,完成更新學生的功能便會相對簡單許多。 參考實序圖: ![](https://img.kancloud.cn/21/bb/21bb73b3b14554c43ffdd39287e8a4fa_826x441.png) 其功能性的代碼應該大概長這樣: ``` public class StudentServiceImpl implements StudentService { ... @Override public Student update(Long id, Student student) { ? Student oldStudent = this.studentRepository.findById(id).get(); ? Student newStudent = this.updateFields(student,oldStudent); ? return this.studentRepository.save(newStudent); ? } /** * 更新學生 * @param newStudent 新學生信息 * @param oldStudent 老學生信息 * @return 更新后的學生信息 */ public Student updateFields(Student newStudent, Student oldStudent) { // 更新各個字段后返回更新后的學生 return null; } ``` * ? 對應時序圖中的序號2 * ? 對應時序圖中的序號2.1 * ? 對應時序圖中的序號2.2 * ? 對應時序圖中的序號2.3 如果如下定義時序圖: ![](https://img.kancloud.cn/9e/6f/9e6f0ecee786082b5a72400ba276d8b1_680x278.png) 則功能性代碼就應該變成這樣: ``` public class StudentServiceImpl implements StudentService { ... @Override public Student update(Long id, Student student) { Student oldStudent = this.studentRepository.findById(id).get(); return this.updateFields(student,oldStudent); } /** * 更新學生 * @param newStudent 新學生信息 * @param oldStudent 老學生信息 * @return 更新后的學生信息 */ public Student updateFields(Student newStudent, Student oldStudent) { // 更新各個字段 // 更新各個字段 return this.studentRepository.save(newStudent); } ``` 從功能實現上這兩種方法難分伯仲,但就可測試性而言,第二種時序圖在為其準備一些調用替身(spy)時會更輕松一些。為此,本例中采取第二個時序圖做為開發方案。 ### 單元測試 雖然單元測試邏輯并不復雜,但提前寫點注釋整理下思路也大有益處。 ``` public class StudentServiceImplTest { ... @Test public void update() { // 準備替身及調用替身后的模擬返回值 // 調用update方法測試 // 斷言傳入參數符合預期 // 斷言返回值符合預期 } @Test public void updateFields() { // 準備替身 // 調用updateFields方法 // 斷言傳入替身的參數符合預期(更新了學生信息) // 斷言返回值符合預期 } ``` 完善測試代碼: ``` @Test public void update() { // 準備替身及調用替身后的模擬返回值 // 第一個替身(間諜) Long id = new Random().nextLong(); Student mockResultStudent = new Student(); Mockito.when(this.studentRepository.findById(id)).thenReturn(Optional.of(mockResultStudent)); // 第二個替身 ``` # Mockito.spy 準備第一個替身我們已經輕車熟路,先準備測試的參數再準備相應的返回值。但第二個替身就不那么簡單了。這是因為此時要構造的替身與我們測試的方法位于同一個對象上。也就是我們要保證執行 `this.studentService`這個對象的update方法時執行是真實的方法,在但這個真實的update方法中調用本對象的`updateFields`時執行的卻是替身的方法。前面已有的知識要么該整個對象做為真實的對象看待,比如此時的`this.studentService`,要么將整個對象做為替身來看待,比如此時的`this.studentRepository`。但還沒有學習過如何將一個真實的對象的部分方法保留的同時,又將特定的方法變成替身方法(在部分方法上創建間諜)。實際上,筆者也嘗試查找過此類的解決方案。最終約以失敗告終。`Mockito`貌似早就得知了此時的需求,所以提供了另外一種思路來解決當前面臨的問題。雖然沒有辦法把一個真實的對象的部分方法替換掉,但是可以由真實對象的clone出一個替身。此時這個替身具有兩個特點:1.該替身由于是由真實的對象clone而來,所以真實對象上方法具有的功能,該替身上的方法中均有。2.由于其本質是替身,所以可以在該替身的任意方法上安排間諜。這樣一來便基于`this.studentService`clone出一個替身,將替換掉該替身上的`updateFields`方法,從而對`update`方法進行測試了。 > 我們說的替身(間諜)有兩種:第一種是對象的替身,該替身擁有原對的所有的功能。第二種是方法的替身(間諜),一旦某個方法被安排了間諜,那么訪問該方法那么間諜將替待原方法接受調用并替待原方法返回數據。 具體代碼如下: ``` @Test public void update() { // 準備替身及調用替身后的模擬返回值 // 第一個替身(間諜) Long id = new Random().nextLong(); Student mockResultStudent = new Student(); Mockito.when(this.studentRepository.findById(id)).thenReturn(Optional.of(mockResultStudent)); ? // 第二個替身. StudentService studentServiceSpy = Mockito.spy(this.studentService); ? StudentServiceImpl studentServiceImplSpy = (StudentServiceImpl) studentServiceSpy; ★? Student mockResultStudent1 = new Student(); Mockito.doReturn(mockResultStudent1).when(studentServiceImplSpy).updateFields(Mockito.any(Student.class), Mockito.any(Student.class)); ? ``` * ? 由this.studentService clone出一個替身,該替身具有原studentService中的所有功能及屬性 * ? 由于updateFields方法未存在于StudentService接口上而是存在于StudentServiceImpl。所以我們沒有辦法對類型是StudentService的對象設置updateFields方法的替身。 * ? 但雖然注入時聲明的為StudentService,但實際注入的為StudentServiceImpl。所以實際上當前的this.studentService是基于StudentServiceImpl創建的實例,也就是說當前的this.studentService是有updateFields方法的。 * ? 基于此,在這里可以使用類型轉換將其轉換為StudentServiceImpl。 * ★ 看不懂的話可以暫時略過,照著抄上就好了。這就像乘客網上約一輛出租車時并沒有約定其有座椅加熱功能,但租車平臺派送過來的車帶了座椅加熱功能。雖然這個功能并沒有在規定的列表中,但它的確是實實在在的存在于當前的出租車了。 * ? 注意此處的語法為:Mockito.doReturn().when()。區別于?處的Mockito.when().thenReturn() >[success] Mockito.doReturn().when() VS Mockito.when().thenReturn():大多數時候,這兩種用法無區別,推薦優先使用Mockito.when().thenReturn()。但與Mockito.spy配合使用時,則只能用Mockito.doReturn().when()。延伸閱讀:[Mockito: doReturn vs thenReturn](https://sangsoonam.github.io/2019/02/04/mockito-doreturn-vs-thenreturn.html) 替身設置完畢后,正式開始進行功能測試: ``` @Test public void update() { // 準備替身及調用替身后的模擬返回值 // 第一個替身(間諜) Long id = new Random().nextLong(); Student mockResultStudent = new Student(); Mockito.when(this.studentRepository.findById(id)).thenReturn(Optional.of(mockResultStudent)); // 第二個替身. 1. 由this.studentService clone出一個替身,該替身具有原studentService中的所有功能及屬性 StudentService studentServiceSpy = Mockito.spy(this.studentService); // 由于updateFields方法并不存在于StudentService接口上,所以預對updateFields設置替身 // 則需要對類型進行轉制轉換 // (雖然注入時聲明的為StudentService,但實際注入的為StudentServiceImpl,這是強制轉換的基礎) StudentServiceImpl studentServiceImplSpy = (StudentServiceImpl) studentServiceSpy; Student mockResultStudent1 = new Student(); Mockito.doReturn(mockResultStudent1).when(studentServiceImplSpy).updateFields(Mockito.any(Student.class), Mockito.any(Student.class)); // 調用update方法測試 Student student = new Student(); Student resultStudent = studentServiceImplSpy.update(id, student); // 斷言傳入第一個替身參數符合預期 ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class); Mockito.verify(this.studentRepository).findById(longArgumentCaptor.capture()); Assertions.assertThat(longArgumentCaptor.getValue()).isEqualTo(id); // 斷言第二個替身參數符合預期:參數1為傳入update方法的學生,參數2為替身1的返回值 ArgumentCaptor<Student> studentArgumentCaptor = ArgumentCaptor.forClass(Student.class); ArgumentCaptor<Student> studentArgumentCaptor1 = ArgumentCaptor.forClass(Student.class); Mockito.verify(studentServiceImplSpy).updateFields(studentArgumentCaptor.capture(), studentArgumentCaptor1.capture()); Assertions.assertThat(studentArgumentCaptor.getValue()).isEqualTo(student); Assertions.assertThat(studentArgumentCaptor1.getValue()).isEqualTo(mockResultStudent); // 斷言返回值就是第二個替身的返回值 Assertions.assertThat(resultStudent).isEqualTo(mockResultStudent1); } ``` ### updateFields 本方法主要是使用新傳入的學生信息更新原學生信息,并把更新后的信息存入數據庫。最后返回更新后的學生。則測試功能點有二:1. 更新學生信息。2.調用數據倉庫并返回其返回值 ``` @Test public void updateFields() { // 準備替身 Student mockResultStudent = new Student(); Mockito.when(this.studentRepository.save(Mockito.any(Student.class))).thenReturn(mockResultStudent); // 調用updateFields方法 StudentServiceImpl studentServiceImpl = (StudentServiceImpl) this.studentService; Student newStudent = new Student(); newStudent.setKlass(new Klass()); ? newStudent.setName(RandomString.make(8)); ? newStudent.setSno(RandomString.make(4)); ? Student oldStudent = new Student(); ? oldStudent.setId(new Random().nextLong()); ? Student resultStudent = studentServiceImpl.updateFields(newStudent, oldStudent); // 斷言傳入替身的參數符合預期(更新了學生信息) ArgumentCaptor<Student> studentArgumentCaptor = ArgumentCaptor.forClass(Student.class); Mockito.verify(this.studentRepository).save(studentArgumentCaptor.capture()); Student editedStudent = studentArgumentCaptor.getValue(); Assertions.assertThat(editedStudent.getId()).isEqualTo(oldStudent.getId()); ? Assertions.assertThat(editedStudent.getName()).isEqualTo(newStudent.getName()); ? Assertions.assertThat(editedStudent.getSno()).isEqualTo(newStudent.getSno()); ? Assertions.assertThat(editedStudent.getKlass()).isEqualTo(newStudent.getKlass()); ? // 斷言返回值符合預期 Assertions.assertThat(resultStudent).isEqualTo(mockResultStudent); } ``` * ? 準備更新的信息 * ? 設置老學生ID * ? 斷言老學生id不變 * ? 斷言其它信息更新成功 功能代碼如下: ``` public class StudentServiceImpl implements StudentService { ... /** * 更新學生 * @param newStudent 新學生信息 * @param oldStudent 老學生信息 * @return 更新后的學生信息 */ public Student updateFields(Student newStudent, Student oldStudent) { oldStudent.setSno(newStudent.getSno()); oldStudent.setName(newStudent.getName()); oldStudent.setKlass(newStudent.getKlass()); return this.studentRepository.save(oldStudent); } ``` 單元測試通過。 ## C層 單元測試初始化如下: ``` public class StudentControllerTest { ... @Test public void update() { // 準備傳入參數的數據 // 準備服務層替身被調用后的返回數據 // 按接口規范發起請求,斷言狀態碼正常,接收的數據符合預期 // 斷言C層進行了數據轉發(替身接收的參數值符合預期) } ``` 補充測試代碼如下: ``` @Test public void update() throws Exception { // 準備傳入參數的數據 Long id = new Random().nextLong(); // 準備服務層替身被調用后的返回數據 Student mockResult = new Student(); mockResult.setId(id); mockResult.setName(RandomString.make(6)); mockResult.setSno(RandomString.make(4)); mockResult.setKlass(new Klass()); mockResult.getKlass().setId(new Random().nextLong()); mockResult.getKlass().setName(RandomString.make(10)); Mockito.when(this.studentService.update(Mockito.anyLong(), Mockito.any(Student.class))).thenReturn(mockResult); JSONObject studentJsonObject = new JSONObject(); ? JSONObject klassJsonObject = new JSONObject(); ? studentJsonObject.put("sno", RandomString.make(4)); studentJsonObject.put("name", RandomString.make(6)); klassJsonObject.put("id", new Random().nextLong()); studentJsonObject.put("klass", klassJsonObject); // 按接口規范發起請求,斷言狀態碼正常,接收的數據符合預期 String url = "/Student/" + id.toString(); this.mockMvc .perform(MockMvcRequestBuilders.put(url) .content(studentJsonObject.toString()) .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("id").value(id)) ? .andExpect(MockMvcResultMatchers.jsonPath("sno").exists()) ? .andExpect(MockMvcResultMatchers.jsonPath("name").exists()) ? .andExpect(MockMvcResultMatchers.jsonPath("klass.id").exists()) ? .andExpect(MockMvcResultMatchers.jsonPath("klass.name").exists()) ? ; // 斷言C層進行了數據轉發(替身接收的參數值符合預期) ArgumentCaptor<Long> longArgumentCaptor = ArgumentCaptor.forClass(Long.class); ArgumentCaptor<Student> studentArgumentCaptor = ArgumentCaptor.forClass(Student.class); Mockito.verify(this.studentService).update(longArgumentCaptor.capture(), studentArgumentCaptor.capture()); Assertions.assertThat(longArgumentCaptor.getValue()).isEqualTo(id); Student resultStudent = studentArgumentCaptor.getValue(); Assertions.assertThat(resultStudent.getSno()).isEqualTo(studentJsonObject.get("sno")); Assertions.assertThat(resultStudent.getName()).isEqualTo(studentJsonObject.get("name")); Assertions.assertThat(resultStudent.getKlass().getId()).isEqualTo(klassJsonObject.get("id")); Assertions.assertThat(resultStudent.getKlass().getName()).isEqualTo(klassJsonObject.get("name")); } ``` * ? 構造json數據 * ? 同上一小節的測試,斷言ID值符合預期 * ? ID符合預期,則說明返回的對象是正確的。那么此時只需要驗證返回了前臺需要的字段即可 運行單元測試并按提示完成功能代碼或修正單元測試代碼: ``` java.lang.AssertionError: Status Expected :200 Actual :405 ``` 期望狀態碼200,但接收到了405。405對應的是:Request method not supported。在上個小節中C層進行第一次單元測試時接收的狀態碼為404。與上一小節相同,在這同樣是由于沒有定義`RequestMapping`引發的錯誤,但為何此時接收的是405而不是404呢?這是由于向后臺以地址 `/Student/id`發起請求時,此地址正好對應了`getById`方法的請求路徑: ``` public class StudentController { ... @GetMapping("{id}") public Student getById(@PathVariable Long id) { return this.studentService.findById(id); } ``` 雖然請求路徑對應上了,但請求的方法在getById方法上規定是的get,而非此時發起的`put`。也就是說當發生405錯誤時,說明找到了對應的請求路徑,而請求路徑對應的請求方法卻未對應成功。而404錯誤說明根本未找到對應的請求路徑。 如下修正代碼: ``` public class StudentController { ... @PutMapping("{id}") ? public Student update(Long id, Student student) { return null; } ``` 再次進行單元測試,發生了如下錯誤: ``` .andExpect(MockMvcResultMatchers.jsonPath("id").value(id)) java.lang.AssertionError: No value at JSON path "id" ``` 它說沒有在返回的json字符串上找到想到的id屬性,出現該錯誤首先想到的是由于C層中返回null引起的。所以首先想到的修正C層中的調用: ```java public class StudentController { ... @PutMapping("{id}") public Student update(Long id, Student student) { return this.studentService.update(id, student); ? } ``` 再次執行單元測試,錯誤依舊。這是由于我們的間諜設置`Mockito.when(this.studentService.update(Mockito.anyLong(), Mockito.any(Student.class))).thenReturn(mockResult)`沒有起作用。該間諜設置的意思是:當`this.studentService.update`傳入的第一個參數的值的類型為`Long`,且傳入的第二個參數的值的類型為`Student`時,將`mockResult`做為返回值返回。 debug一下看看更清晰: ![](https://img.kancloud.cn/a0/e8/a0e877a73f0ae7dbfd317614217aa307_923x191.png) ![](https://img.kancloud.cn/36/6d/366de23c207c338957d8b6bab5c69938_674x85.png) ![](https://img.kancloud.cn/37/a9/37a91b0551f2993de75ae750e1c86e1e_528x93.png) debug發現此時的id為null,而null并不屬于類型Long。所以并不符合間諜設置中:`傳入的第一個參數的值的類型為`Long\`\`的條件,當然也就沒有執行間諜設置的程序了。 id為null的原因是由于沒有設置`@PathVariable`: ``` public class StudentController { ... @PutMapping("{id}") public Student update(@PathVariable? Long id, Student student) { return this.studentService.update(id, student); } ``` 再次運行單元測試: ``` Assertions.assertThat(resultStudent.getSno()).isEqualTo(studentJsonObject.get("sno")); org.junit.ComparisonFailure: Expected :"ZEUg" Actual :null ``` 它說在斷言傳值時發生了錯誤,期望的是一個隨機值,但卻收到了null。按數據流向分析,產生此錯誤的原因也有三個:0:根本就沒有向C層傳值。1. C層根據就沒有成功接收到相應的值。 2. C層雖然成功接收到了,但卻沒有成功的進行轉發。在本例中,屬于錯誤1:C層中并沒有成功的接收。 ``` public class StudentController { ... @PutMapping("{id}") public Student update(@PathVariable Long id, @RequestBody? Student student) { return this.studentService.update(id, student); } ``` 單元測試顯示如下錯誤: ``` Assertions.assertThat(resultStudent.getKlass().getId()).isEqualTo(klassJsonObject.get("id")); Assertions.assertThat(resultStudent.getKlass().getName()).isEqualTo(klassJsonObject.get("name")); ? org.json.JSONException: No value for name ``` 在斷言參數轉發時發現了錯誤。此錯誤類型同上一點。要么壓根沒發送,要么發送了沒接收,要么接收了沒轉發到下一個節點。而學生的接收與轉發是整體性的,既然已經接收并轉發了klass的id, 那么klass的name也必然會被成功的接收并轉發。此時問題點便聚集在第0點:根本沒有向C層傳值。 ``` public class StudentControllerTest { ... klassJsonObject.put("id", new Random().nextLong()); klassJsonObject.put("name", RandomString.make(6)); ? studentJsonObject.put("klass", klassJsonObject); ``` 再次運行單元測試,通過。 # 參考文檔 | 名稱 | 鏈接 | 預計學習時長(分) | | --- | --- | --- | | 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.7.5) | \- | | Mockito.spy | [https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#spy-T-](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#spy-T-) | \- |
                  <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>

                              哎呀哎呀视频在线观看