后台的功能与其它实体的添加无异,也是先建立实体然后建立对应的仓库,再建立serivce,最终建立controller。与前面学习的知识点稍有不同的是每门课程中可以有多个班级,它们间的关系是多对多,这个知识点将放到下节中单独进行讲解。本节除使用前面已学习的知识点完成基本的功能开发外,还将使用**实体监听器**来替换`@PrePersist`及`@PreUpdate`完成对课程名称长度的校验。 # 实体开发 在entity包中新建Course课程实体。 ```java package com.mengyunzhi.springbootstudy.entity; import javax.persistence.*; /** * 课程 * @author panjie */ @Entity public class Course { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String name = ""; @ManyToOne private Teacher teacher; // 省略构造函数及setter/getter } ``` 对Course的测试依赖于仓库层,下面继续开发仓库层。 # 仓库层开发 repository/CourseRepository.java ```java package com.mengyunzhi.springbootstudy.repository; import com.mengyunzhi.springbootstudy.entity.Course; import org.springframework.data.repository.CrudRepository; public interface CourseRepository extends CrudRepository<Course, Long> { } ``` ## 测试 新建Course实体的测试文件CourseTest.java,然后分别就name字段的unique以及nullable进行验证。初始化如下: ```java package com.mengyunzhi.springbootstudy.entity; import com.mengyunzhi.springbootstudy.repository.CourseRepository; import org.junit.Before; 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.test.context.junit4.SpringRunner; @SpringBootTest @RunWith(SpringRunner.class) public class CourseTest { @Autowired CourseRepository courseRepository; private Course course; @Before public void before() { } @Test public void save() { } @Test public void nameUniqueTest() { } @Test public void nameNullable() { } } ``` 按前面的思路,若想验证某个校验是否生效最好的方法是在@Before先创建一个正常的课程。 entity/CourseTest.java ```java @Before public void before() { this.course = new Course(); this.course.setName(RandomString.make(4)); } @Test public void save() { this.courseRepository.save(this.course); } ``` ### Unique验证 entity/CourseTest.java ```java @Test(expected = DataIntegrityViolationException.class) public void nameUniqueTest() { this.courseRepository.save(this.course); Course course = new Course(); course.setName(this.course.getName()); this.courseRepository.save(course); } ``` ### Nullable验证 entity/CourseTest.java ```java @Test(expected = DataIntegrityViolationException.class) public void nameNullable() { this.course.setName(null); this.courseRepository.save(course); } ``` # M层开发 接口初始化 service/CourseService.java ```java package com.mengyunzhi.springbootstudy.service; import com.mengyunzhi.springbootstudy.entity.Course; /** * 课程 * @author panjie */ public interface CourseService { /** * 新增课程 * @param course 课程 * @return 课程 */ Course save(Course course); } ``` 实现类 service/CourseServiceImpl.java ```java @Service public class CourseServiceImpl implements CourseService { private CourseRepository courseRepository; @Autowired public CourseServiceImpl(CourseRepository courseRepository) { this.courseRepository = courseRepository; } @Override public Course save(Course course) { return this.courseRepository.save(course); } } ``` ## 单元测试 初始化如下: service/CourseServiceImplTest.java ```java public class CourseServiceImplTest { private CourseRepository courseRepository; private CourseService courseService; public CourseServiceImplTest() { this.courseRepository = Mockito.mock(CourseRepository.class); this.courseService = new CourseServiceImpl(this.courseRepository); } @Test public void save() { } } ``` 补充测试代码如下: service/CourseServiceImplTest.java ```java @Test public void save() { Course course = new Course(); Course returnCourse = new Course(); Mockito.when(this.courseRepository.save(course)).thenReturn(returnCourse); Course resultCourse = this.courseService.save(course); Assert.assertEquals(returnCourse, resultCourse); } ``` # C层开发 新建CourseController并初始化如下: controller/CourseController.java ```java @RestController @RequestMapping("Course") public class CourseController { @Autowired CourseService courseService; @PostMapping @ResponseStatus(HttpStatus.CREATED) public Course save(@RequestBody Course course) { return this.courseService.save(course); } } ``` ## 单元测试 初始化单元测试文件Course/CourseControllerTest.java ```java @SpringBootTest @RunWith(SpringRunner.class) @AutoConfigureMockMvc public class CourseControllerTest { @MockBean private CourseService courseService; @Autowired MockMvc mockMvc; @Test public void save() { } } ``` 补充测试代码如下: Course/CourseControllerTest.java ```java @Test public void save() throws Exception { JSONObject jsonObject = new JSONObject(); String name = RandomString.make(4); jsonObject.put("name", name); String url = "/Course"; Course returnCourse = new Course(); returnCourse.setId(new Random().nextLong()); returnCourse.setName(RandomString.make(4)); returnCourse.setTeacher(new Teacher()); returnCourse.getTeacher().setId(new Random().nextLong()); returnCourse.getTeacher().setName(RandomString.make(4)); Mockito.when(this.courseService.save(Mockito.any(Course.class))).thenReturn(returnCourse); this.mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(jsonObject.toString()) ).andExpect(MockMvcResultMatchers.status().is(201)) .andExpect(MockMvcResultMatchers.jsonPath("id").value(returnCourse.getId())) .andExpect(MockMvcResultMatchers.jsonPath("name").value(returnCourse.getName())) .andExpect(MockMvcResultMatchers.jsonPath("teacher.id").value(returnCourse.getTeacher().getId())) .andExpect(MockMvcResultMatchers.jsonPath("teacher.name").value(returnCourse.getTeacher().getName())) ; ArgumentCaptor<Course> courseArgumentCaptor = ArgumentCaptor.forClass(Course.class); Mockito.verify(this.courseService).save(courseArgumentCaptor.capture()); Assert.assertEquals(courseArgumentCaptor.getValue().getName(), name); } ``` 测试结果: ``` 请求的地址为/Course请求的方法为:POST 当前token未绑定登录用户,返回401 java.lang.AssertionError: Response status Expected :201 Actual :401 ``` ## 401 之所以发生401是由于在上一章中启用了拦截器进行了用户认证,而当前的单元测试并没有传递认证成功的authToken。解决这个问题的方法很多,我们采用Mock TeacherService中的isLogin方法来通过认证拦截器。 Course/CourseControllerTest.java ```java @MockBean ➊ private TeacherService teacherService; @Before public void before() { Mockito.when(this.teacherService.isLogin(Mockito.any())).thenReturn(true); ➋ } ``` * ➊ 使用MockBean注入TeacherService。该注解使得在整个测试过程中运行中的teacherService全部为此替身 * ➋ 当认证拦截器调用替身的isLogin方法时返回true,表示teacher已认证 再次运行单元测试通过。 ## 小作业 其它对控制器的单元测试同样发生了401错误,请参考上面的代码进行修正。 # 实体监听器 在前面的章节中使用了`@PrePersist`及`@PreUpdate`完成了对字段长度的校验,本节中展示另外一种对单元测试更友好的方案:实体监听器。 ## 初始化 在entity包中新建CourseListener entity/CourseListener.java ```java package com.mengyunzhi.springbootstudy.entity; import javax.persistence.PrePersist; import javax.persistence.PreUpdate; /** * 实体监听器。当课程实体发生新建、更新操作时执行 */ public class CourseListener { @PrePersist ➊ public void prePersist(Course course➋) { System.out.println("prePersist"); } @PreUpdate ➊ public void perUpdate(Course course➋) { System.out.println("perUpdate"); } } ``` * ➊ 与在实体中直接使用的注解相同 * ➋ 参数中必须传入一个参数,该参数对应的类型为被监听的实体(在只监听某一个实体的前提下) 在Course实体上使用EntityListeners注解来添加实体监听器: entity/Course.java ```java @Entity @EntityListeners(CourseListener.class) public class Course { ``` ### 测试 打到CourseTest建立update方法 entity/CourseTest.java ```java @Test void update() { this.courseRepository.save(this.course); this.course.setName(RandomString.make(4)); this.courseRepository.save(this.course); } ``` 运行单元测试控制台如下: ``` prePersist perUpdate ``` 说明实体监听器已生效。 ## 校验课程长度 entity/CourseListener.java ```java public class CourseListener { @PrePersist public void prePersist(Course course) { if (course.getName() == null || course.getName().length() < 2) { throw new DataIntegrityViolationException("课程名称长度最小为2位"); } } @PreUpdate public void perUpdate(Course course) { if (course.getName() == null || course.getName().length() < 2) { throw new DataIntegrityViolationException("课程名称长度最小为2位"); } } } ``` prePersist与perUpdate方法中的代码是相同的,所以合并如下: entity/CourseListener.java ```java public class CourseListener { @PrePersist @PreUpdate public void prePersistAndUpdate(Course course) { if (course.getName() == null || course.getName().length() < 2) { throw new DataIntegrityViolationException("课程名称长度最小为2位"); } } } ``` ### 单元测试 entity/CourseTest.java ```java @Test public void nameLength() { Boolean catchException = false; this.course.setName(null); try { this.courseRepository.save(this.course); } catch (DataIntegrityViolationException e) { Assert.assertEquals(e.getMessage(), "课程名称长度最小为2位"); catchException = true; } Assert.assertTrue(catchException); catchException = false; this.course.setName(RandomString.make(1)); try { this.courseRepository.save(this.course); } catch (DataIntegrityViolationException e) { Assert.assertEquals(e.getMessage(), "课程名称长度最小为2位"); catchException = true; } Assert.assertTrue(catchException); for (int i = 2; i < 4; i++) { this.course.setName(RandomString.make(i)); this.courseRepository.save(this.course); } } ``` # 总结 本节中使用了已经学习的方法完成了课程的基本保存功能。同时学习了如何使用实体监听器的方法来对课程名称的长度进行校验。在生产环境中,可以对某一实体添加多个监听器,还可以将一个监听器添加到多个实体上。是一种灵活、可复用性强的校验方法。 # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.6](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.6) | - |