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

                我們在上個小節中剛剛講了`規范`,在本節中開始前我們先為idea安裝一個規范插件。打開idea,使用`ctrl+,`打開參數設置,然后輸入plugins: ![](https://img.kancloud.cn/9e/81/9e8123fceb84b4d121f658f8232803f1_402x532.png) 在右側的窗口中輸入alibaba ![](https://img.kancloud.cn/a0/dd/a0ddbf6984ae510e326332ba396c7486_965x263.png) 點擊install安裝,完成后重啟idea,此時一個代碼規范約束的插件便被成功的安裝了。以后當我們在書寫一不太規范的代碼時,該插件則會進行自動提示。 # M層 -- 多態 JAVA的多態性允許我們在StudentService中有了一個findAll方法的同時,再寫一個同名findAll方法: ``` /** * 學生 */ public interface StudentService { ... /** * 查詢分頁信息 * * @param pageable 分頁條件 * @return 分頁數據 */ Page<Student> findAll(Pageable pageable); /** * 綜合查詢 * @param name containing 姓名 * @param sno beginWith 學號 * @param klassId equal 班級ID * @param pageable * @return */ Page<Student> findAll(String name, String sno, Long klassId, Pageable pageable); ? ``` * ? 方法名仍然為findAll,但由于參數不同,所以在其它的對象調用StudentService.findAll時,java是可以通過判斷參數的數量、類型來區分我們具體是想調用哪個findAll方法。 > 一個findAll方法,有多種形態,稱為面向對象的多態性 ## 實現類 在實現類中添加方法實現: ``` @Service public class StudentServiceImpl implements StudentService { ... @Override public Page<Student> findAll(String name, String sno, Long klassId, Pageable pageable) { Klass klass = new Klass(); ? klass.setId(klassId); ? return this.studentRepository.findAll(name, sno, klass, pageable); ? } } ``` * ? 根據klassId構造用Klass * ? 調用數據倉庫,完成查詢并返回 ## 單元測試 找開位于測試文件夾的StudentServiceImplTest并增加測試方法findAllSpecs。該測試的思路和以前的一樣,我們只需要測試findAll(String name, String sno, Long klassId, Pageable pageable)是否將數據成功的轉發給數據倉庫層即可。至于數據倉庫層是否成功的執行了查詢功能,按分層理論這是數據倉庫層需要關心的事情。 ``` import java.util.Arrays; ... @SpringBootTest @RunWith(SpringRunner.class) public class StudentServiceImplTest { private static Logger logger = LoggerFactory.getLogger(StudentServiceImplTest.class); @MockBean StudentRepository studentRepository; ... @Test public void findAllSpecs() { /* 參數初始化 */ String name = "hello"; String sno = "032282"; Long klassId = 1L; Pageable pageable = PageRequest.of(0, 2); List<Student> students = Arrays.asList(); Page<Student> mockStudentPage = new PageImpl<>(students, pageable, 100L) ?; /* 設置模擬返回值 */ Mockito.when(this.studentRepository .findAll(Mockito.eq(name)?, Mockito.eq(sno), Mockito.any(Klass.class), Mockito.eq(pageable))) .thenReturn(mockStudentPage) ?; /* 調用測試方法,獲取返回值并斷言與預期相同 */ Page<Student> returnStudentPage = this.studentService.findAll(name, sno, klassId, pageable); Assertions.assertThat(returnStudentPage).isEqualTo(mockStudentPage); ? } } ``` * ? 調用PageImpl<>(當前頁內容, 分頁信息, 總條數)來構建返回值 * ? 當調用findAll的第一個參數與name相等(eq)時 * ? 當參數為符合:`參數1等于(eq)name對象(值:hello),參數2等于(eq)sno對象(值:032282) , 參數1為任意(any)的班級,參數4等于(eq)pageable對象(值:第0頁每頁2條)`規則時,調用findAll方法將模擬返回mockStudentPage對象。 * 預期返回了mockStudentPage對象。 由于預期返回了mockStudentPage對象,則說明在studentService中調用studentRepository.findAll時,傳入的參數符合`參數1等于(eq)name對象(值:hello),參數2等于(eq)sno對象(值:032282) , 參數1為任意(any)的班級,參數4等于(eq)pageable對象(值:第0頁每頁2條)`規則。進而說明調用name, sno, pageable均是我們傳入的值。而是否按我們的預期傳入了klass還需要使用參數捕獲器來獲取: ``` public class StudentServiceImplTest { ... @Test public void findAllSpecs() { ... Assertions.assertThat(returnStudentPage).isEqualTo(mockStudentPage); /* 獲取M層調用studentRepository的findAll方法時klass的參數值,并進行斷言 */ ArgumentCaptor<Klass> klassArgumentCaptor = ArgumentCaptor.forClass(Klass.class); Mockito.verify(this.studentRepository).findAll(Mockito.eq?(name), Mockito.eq(sno), klassArgumentCaptor.capture()①, Mockito.eq(pageable)); Assertions.assertThat(klassArgumentCaptor.getValue().getId()).isEqualTo(klassId); ② } } ``` * ? Mockito.eq與前面作用相同 * ① 把第三個參數替換為klassArgumentCaptor.capture()來獲取參數的值 * ② 斷言klass中id的值 ### 請思索 在剛剛的測試中,我們將測試代碼做以下替換,同樣可以順利通過測試,你知道其中的原因嗎? ``` Mockito.verify(this.studentRepository).findAll(Mockito.any(String.class), Mockito.any(String.class), klassArgumentCaptor.capture(), Mockito.any(Pageable.class)); Assertions.assertThat(klassArgumentCaptor.getValue().getId()).isEqualTo(klassId); ``` ## Null值處理 我們在進行方法調用的原則是:如果該參數沒有標記為@NotNull,則表示其是可以接收并順利處理null值的。而如果M層在findAll方法中如果接收的Pageable為null,則會在調用數據倉庫層時發生異常,而這是我們不希望看到了。 ``` import javax.validation.constraints.NotNull; ... public interface StudentService { ... Page<Student> findAll(String name, String sno, Long klassId, @NotNull Pageable pageable); } ``` 實現類: ``` import org.springframework.util.Assert; import javax.validation.constraints.NotNull; ... public class StudentServiceImpl implements StudentService { ... @Override public Page<Student> findAll(String name, String sno, Long klassId, @NotNull Pageable pageable) { Assert.notNull(pageable, "Pageable不能為null"); ... } } ``` ### null值測試 ``` public class StudentServiceImplTest { ... @Test(expected = IllegalArgumentException.class) ① public void findAllSpecsNullValidate() { try { this.studentService.findAll(null, null, null, null); } catch (Exception e②) { Assertions.assertThat(e.getMessage()).isEqualTo("Pageable不能為null"); throw e; ③ } } } ``` * ① 該測試應該會拋出 IllegalArgumentException * ② 所有的異常都繼承了Exception (java.lang.Exception屬于java內置的類,使用時無需import),所以只要有異常拋出,并必然被此catch捕獲到 * ③ 將獲取到的異常向上拋出,并被 ① 獲取 # 重寫C層 打開StudentController -> findAll方法,在原代碼的基礎上加入參數name,sno以及klassId: ``` public class StudentController { ... @GetMapping public Page<Student> findAll( @RequestParam String name, ? @RequestParam String sno, ? @RequestParam Long klassId, ? @RequestParam int page, @RequestParam int size) { return this.studentService.findAll(PageRequest.of(page, size)); } ``` 修改調用方法: ``` public Page<Student> findAll( @RequestParam String name, @RequestParam String sno, @RequestParam Long klassId, @RequestParam int page, @RequestParam int size) { return this.studentService.findAll( name, ? sno, ? klassId, ? PageRequest.of(page, size)); } ``` ## 單元測試 前面我們一直在強調單元測試是對自己代碼功能的保障,此時我們剛剛變動了StudentController的代碼,那么我們校驗下單元測試是否起到了應有的作用。我們打開單元測試文件StudentControllerTest,找到并找行findAll單元測試: 最終得到了以下錯誤提示: ``` java.lang.AssertionError: Status Expected :200 Actual :400 <Click to see difference> ``` 期待是200,最終返回了400。在控制臺中找到錯誤提示行并進行點擊,報錯的行數如上: ``` public class StudentControllerTest { ... @Test public void findAll() throws Exception { ... MvcResult mvcResult = this.mockMvc.perform( MockMvcRequestBuilders.get(url) .param("page", "1") .param("size", "2")) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) ★ .andReturn(); ``` 說明返回的狀態碼為400,并不是我們期望的200。而400通常代表參數綁定錯誤,在我們當前的用例中:由于我們在C層的方法中聲明需要接收參數name、sno及klassId,而單元測試在測試時,只傳入了參數page與size。所以在參數的綁定過程中spring不清楚需要用什么值來綁定name、sno、klassId,進而發生了400錯誤。 > 在生產環境中,C層的單元測試報400錯誤是需要引起重視的,因為這意味著我們后臺的請求接口的規范發生了變化,而前臺如果想適應這個變化就必須同步進行修改。 單元測試報錯的原因可以分為兩種:第一種是功能的修正導致歷史的單元測試代碼不能適應新的功能需求;第二種是在增加、修正關聯代碼時對歷史的功能造成了影響。如果情況屬性第一種,則應該按新功能修正單元測試代碼,如果情況屬性第二種,則應該進行近一步的排查。我們此處的單元測試則屬于第一種,解決的方法是修正單元測試代碼: 在原請求的參數的基礎上,我們加入以下三個參數: ``` public class StudentControllerTest { ... @Test public void findAll() throws Exception { ... MvcResult mvcResult = this.mockMvc.perform( MockMvcRequestBuilders.get(url) .param("name", "testName") ? .param("sno", "testSno") ? .param("klassId", "1") ? .param("page", "1") .param("size", "2")) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); ``` 加全請求參數后,重新運行該單元測試,400的錯誤消息,得到了新的錯誤: ``` java.lang.IllegalArgumentException: json string can not be null or empty at com.jayway.jsonpath.internal.Utils.notEmpty(Utils.java:386) at com.jayway.jsonpath.internal.ParseContextImpl.parse(ParseContextImpl.java:36) at com.jayway.jsonpath.JsonPath.parse(JsonPath.java:599) at com.mengyunzhi.springbootstudy.controller.StudentControllerTest.findAll(StudentControllerTest.java:87) ★ at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) ``` ★處對應的代碼如下: ``` public class StudentControllerTest { ... @Test public void findAll() throws Exception { ... logger.info("將返回值由string轉為json,并斷言接收到了分頁信息"); LinkedHashMap returnJson = JsonPath.parse(mvcResult.getResponse().getContentAsString()).json(); ★ Assertions.assertThat(returnJson.get("totalPages")).isEqualTo(2); // 總頁數 ``` 該錯誤的出現說明了兩點:1. 未發生400錯誤,說明參數綁定成功,證明剛剛修正的代碼正確。2. 將返回值轉換為JSON時發生錯誤,說明返回不再符合預期,按錯誤提示猜測試返回值為empty或null。 既然對返回數據產生了懷疑,則可以添加斷點并啟用debug模式來對問題進行調試,在錯誤的行上打個斷點: ![](https://img.kancloud.cn/c5/73/c5731fddffc7e1eb2213239b9e21eb9b_1654x132.png) 使用debug模式運行測試 ![](https://img.kancloud.cn/e3/98/e398fc926ed2502c7e33f76c1258e434_596x207.png) 在控制臺中查看返回值mvcResult: ![](https://img.kancloud.cn/ac/71/ac716cac988a037c8944dc6aaee10a84_885x273.png) 結果顯示返回了空字符串,而不是我們預期的分頁信息了。這是由于C層的代碼在改變了調用M層的方法引起的,我們再來查看C層代面在引入綜合查詢前后的變化 ``` public class StudentController { ... @GetMapping public Page<Student> findAll( @RequestParam String name, ? ① @RequestParam String sno, ? ① @RequestParam Long klassId, ? ① @RequestParam int page, @RequestParam int size) { return this.studentService.findAll(PageRequest.of(page, size)); ? ② return this.studentService.findAll( name, sno, klassId, PageRequest.of(page, size)); ? ② } ``` * ① 代碼變動、引起了400問題 * ② 代碼變動,引起了返回空值的問題 在前面的測試代碼中,我們測試的邏輯是:C層代碼調用的是`this.studentService.findAll(Pageable)`方法。而修改后的邏輯變更為調用`this.studentService.findAll(Strng, String, Long, Pageable)`方法。此時單元測試的報錯及時的提醒我們C層調用的邏輯發生了變化。如果邏輯是正確的則應該修正單元測試代碼;如果邏輯是錯誤的,則應該修正單元測試代碼。則修正單元測試代碼如下: ``` public class StudentControllerTest { ... @Test public void findAll() throws Exception { ... Mockito.when(this.studentService.findAll(Mockito.any(Pageable.class))) .thenReturn(mockOutStudentPage); ? Mockito.when(this.studentService .findAll(Mockito.anyString()?, Mockito.anyString(), Mockito.anyLong()?, Mockito.any(Pageable.class))) .thenReturn(mockOutStudentPage); ? ... ``` * ? 請求的參數值類型為String時 * ? 請求的參數值類型為Long時 再次運行單元測試,測試通過。 ## 可選參數及空參數 按接口的設計,前臺在與后臺進行交互時,查詢參數name、sno及klassId為optional(可選項),也就是說:我們允許用戶在查詢時不輸入此參數。而剛剛在測試的過程中我們發現:如果不輸入name sno或klassId則會得到一個400錯誤。在spring中,可以為@RequestParam設置`required=false`來解決該問題: ``` public class StudentController { ... @GetMapping public Page<Student> findAll( @RequestParam(required = false?) String name, @RequestParam(required = false) String sno, @RequestParam(required = false) Long klassId, @RequestParam int page, @RequestParam int size) { return this.studentService.findAll( name, sno, klassId, PageRequest.of(page, size)); } ``` * ? 該參數是可選項(非required),當用戶未傳入該參數時,name的值設置為null。 重新運行單元測試通過,表示我們的此處的修改并未對原功能造成影響。 ### 單元測試 本著單元測試粒度最小的原則,我們StudentControllerTest中新建findAllRequestParam方法: ``` public class StudentControllerTest { ... /** * 請求參數測試 * @throws Exception */ @Test public void findAllRequestParam() throws Exception { String url = "/Student"; logger.info("只傳入page size,不報錯"); this.mockMvc.perform( MockMvcRequestBuilders.get(url) .param("page", "1") .param("size", "2")) .andExpect(MockMvcResultMatchers.status().isOk()); logger.info("不傳page報錯"); this.mockMvc.perform( MockMvcRequestBuilders.get(url) .param("size", "2")) .andExpect(MockMvcResultMatchers.status().is(HttpStatus.BAD_REQUEST.value())); logger.info("不傳size報錯"); this.mockMvc.perform( MockMvcRequestBuilders.get(url) .param("page", "1")) .andExpect(MockMvcResultMatchers.status().is(400)); } ``` # 小測試 在C層的單元測試findAll中我們雖然斷言了`必然調用studentService.findAll方法`,但卻沒有對調用該方法時向個參數的傳入值進行斷言。也就是說如果C層的代碼被不小心寫成:`return this.studentService.findAll(sno, name, klassId, PageRequest.of(page, size))`的話,我們也無從察覺。請參數M層的測試補充該部分,以確保C層的轉發是正確的。 # 參考文檔 | 名稱 | 鏈接 | 預計學習時長(分) | | --- | --- | --- | | 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.5](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.5) | - | | mockito anyString | [https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyString--](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyString--) | 2 | | mockito anyLong | [https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyLong--](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#anyLong--) | 2 | | mockito eq | [https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#eq-T-](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/ArgumentMatchers.html#eq-T-) | 2 |
                  <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>

                              哎呀哎呀视频在线观看