上节小定义了接口规范: <hr> ``` POST /Teacher/login ``` #### 参数 Parameters | type | name | Description | Schema | | --- | --- | --- | --- | | **Body** | **用户名密码** <br> *requried* | 登录教师 | {username: 用户名, password: 密码} | #### 返回值 Responses | HTTP Code | Description | Schema | | --- | --- | --- | | **200** | Ok | 用户密码是否正确:正确,true; 不正确, false | <hr> 定制时序图如下: ![](https://img.kancloud.cn/74/59/7459962638bcca890e81a8ab562d3c17_733x267.png) 按由后至前的开发顺序进行开发。 # Repository 在TeacherRepository中加入findByUsername方法: repository/TeacherRepository.java ```java public interface TeacherRepository extends CrudRepository<Teacher, Long> { /** * 查找用户 * @param username 用户名 * @return */ Teacher findByUsername(String username); } ``` spring可以按照一些特定的方法名来自动组装常用的查询语句。spring见到`findByUsername`便会我们自动完成通过`username`来查找某个教师的功能。上述语句在返回值中使用了`Teacher`,则spring将通过传入的`username`进行查找时:找到对应的教师,返回该教师实体;未找到对应的教师,则返回null。此外spring还支持以下写法: repository/TeacherRepository.java ``` public interface TeacherRepository extends CrudRepository<Teacher, Long> { /** * 查找用户 * @param username 用户名 * @return */ Option<Teacher>➊ findByUsername(String username); } ``` 即使用`Option<Teacher>`来代替`Teacher`做为返回值,此写法意在告知调用者:调用本方法时,可能最终获取不到数据(比如用户输入了错误的用户名,便不会通过该用户在数据库中找到对应的数据)。此处我们暂时使用第一种方式,直接将返回值类型声明为Teacher,意在展示一种**过时**的使用方法以便我们在某些生产环境下处理一些历史项目时也能够得心应手。 repository/TeacherRepository.java ```java public interface TeacherRepository extends CrudRepository<Teacher, Long> { /** * 查找用户 * @param username 用户名 * @return */ Teacher findByUsername(String username); } ``` ## 测试 新建测试类TeacherRepositoryTest并初始化如下: ```java @SpringBootTest @RunWith(SpringRunner.class) public class TeacherRepositoryTest { @Test public void findByUsername() { } } ``` 补充测试代码: ```java @Test public void findByUsername() { // 准备测试数据并持久化 Teacher teacher = new Teacher(); teacher.setUsername(RandomString.make(6)); this.teacherRepository.save(teacher); // 调用测试方法并断言 Teacher teacher1 = this.teacherRepository.findByUsername(teacher.getUsername()); Assert.assertEquals(teacher, teacher1); } ``` 报错如下: ``` java.lang.AssertionError: Expected :com.mengyunzhi.springbootstudy.entity.Teacher@608b35fa Actual :com.mengyunzhi.springbootstudy.entity.Teacher@965bcbc ``` 那么是否意味着findByUsername方法未失效呢?我们于相应的位置上加入断言,并使用debug模式启动项目进行查看: ![](https://img.kancloud.cn/f6/ed/f6ed4a27f9b59a2cccd27c8409d6a502_428x67.png) ![](https://img.kancloud.cn/43/1f/431fd6348914b1576252167e432e139a_331x52.png) 查看变量: ![](https://img.kancloud.cn/72/24/7224fb85fd4829ec5502f6c4ec4b1609_256x253.png) 这是由于:虽然使用findByUsername方法查询出的教师也是数据表中ID为1的**教师数据**,但是此时该**教师数据**却被装入了一个全新的对象。这就像虽然两个人的名字都叫李刚,但是他们终究是两个人。而`Assert.assertEquals`能够分辨出这两个不同的李刚。 幸运的在建立数据表的时候,为每个数据都分配了一个具有唯一标识作用的`id`,在此完成可以通过该标识来判断两个teacher对象是否是基于同一条数据创建的。 ``` Assert.assertEquals(teacher, teacher1); ✘ Assert.assertEquals(teacher.getId(), teacher1.getId()); ``` 单元测试通过 # Service 服务层的初始化需要创建一个接口并同时创建该接口的实现类: service/TeacherService.java ```java package com.mengyunzhi.springbootstudy.service; import com.mengyunzhi.springbootstudy.entity.Teacher; /** * 教师 * @author 梦云智 */ public interface TeacherService { /** * 用户登录 * @param username 用户名 * @param password 密码 * @return 成功 true */ boolean login(String username, String password); /** * 验证密码的有效性 * @param teacher 教师 * @param password 密码 * @return 有效 true */ boolean validatePassword(Teacher teacher, String password); } ``` 实现类:service/TeacherServiceImpl.java ```java package com.mengyunzhi.springbootstudy.service; import com.mengyunzhi.springbootstudy.entity.Teacher; import org.springframework.stereotype.Service; @Service public class TeacherServiceImpl implements TeacherService { @Override public boolean login(String username, String password) { return false; } @Override public boolean validatePassword(Teacher teacher, String password) { return false; } } ``` ## login 此方法先调用仓库层获取相关的教师,接着将数据转发给validatePassword。 service/TeacherServiceImpl.java ``` public class TeacherServiceImpl implements TeacherService { private TeacherRepository teacherRepository; ➊ @Autowired ➋ public TeacherServiceImpl(TeacherRepository teacherRepository) { this.teacherRepository = teacherRepository; } ``` * ➊ 声明私有变量 * ➋ 使用Autowired对构造函数进行注解,spring将自动装入构造函数中声明的全部类型。 >[success] 这种将Autowired添加在构造函数上的方法是推荐使用的方式 补充功能代码: ```java @Override public boolean login(String username, String password) { Teacher teacher = this.teacherRepository.findByUsername(username); return this.validatePassword(teacher, password); } ``` ### 单元测试 初始化如下: service/TeacherServiceImplTest.java ```java package com.mengyunzhi.springbootstudy.service; import org.junit.Test; import static org.junit.Assert.*; public class TeacherServiceImplTest { @Test public void login() { } @Test public void validatePassword() { } } ``` 在每次测试用例执行完先创建一个供测试的`TeacherServiceImpl` service/TeacherServiceImplTest.java ```java public class TeacherServiceImplTest { private TeacherServiceImpl teacherService; ➊ private TeacherRepository teacherRepository; ➊ @Before public void before() { this.teacherRepository = Mockito.mock(TeacherRepository.class); ➋ TeacherServiceImpl teacherService = new TeacherServiceImpl(this.teacherRepository); ➌ this.teacherService = Mockito.spy(teacherService); ➍ } ``` * ➊ 定义两个在测试中可能被使用的私有属性 * ➋ 获取一个TeacherRepository替身 * ➌ 创建一个真实的TeacherServiceImpl * ➍ clone一个与TeacherServiceImpl具有相同功能的替身。 * ➍ 在该替身上可以使用Mockito对替身上的部分方法进行自主替换 >[success] 此测试方法的最大优点在于:由于单元测试未依赖spring,所以将大幅度减小单元用例的启动时间。这也将是教程在此后的单元测试中优先使用的测试方法。 补充代码: service/TeacherServiceImplTest.java ```java @Test public void login() { // 请求及模拟返回数据准备 String username = RandomString.make(6); String password = RandomString.make(6); Teacher teacher = new Teacher(); Mockito.when(this.teacherRepository.findByUsername(username)).thenReturn(teacher); ➊ Mockito.doReturn(true).when(this.teacherService).validatePassword(teacher, password); ➋ // 调用 boolean result = this.teacherService.login(username, password); // 断言 Assert.assertTrue(result); ArgumentCaptor<String> stringArgumentCaptor = ArgumentCaptor.forClass(String.class); Mockito.verify(this.teacherRepository).findByUsername(stringArgumentCaptor.capture()); Assert.assertEquals(stringArgumentCaptor.getValue(), username); } ``` * ➊ this.teacherRepository是由Mockito.mock初始化而来,是一个彻头彻尾的替身,推荐使用Mockito.when对其进行设置。 * ➋ this.teacherService是由Mockito.spy根据真实的对象clone而言,该替身拥有着与真身相同的功能,只能够使用Mockito.doReturn对其进行设置 ## validatePassword 初始化如下: service/TeacherServiceImpl.java ``` @Override public boolean validatePassword(Teacher teacher, String password) { if (teacher == null || teacher.getPassword() == null || password == null) { return false; } return teacher.getPassword().equals(password); } ``` 此时发现在教师实体中未设置password字段,进行相应的增加,这说明在系统规划时并没有考虑充分。虽然我们要尽力减少诸如此类的变更,但事实告诉我们此类变更不可避免。这当然也是软件工程存在意义,因为软件工程的目标是:致力于打造易维护、易修改的代码。 entity/Teacher.java ```java private String password = "yunzhi"; ➊ public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } ``` * ➊ 设置个默认密码 ### 单元测试 service/TeacherServiceImplTest.java ```java @Test public void validatePassword() { // 教师中有密码,且密码与传入的密码相同,返回true ➊ // 教师为null返回false // 传入的密码为null返回false // 未设置教师的密码,返回false // 教师中的密码与传入的密码不相同返回false } ``` * ➊ 先测试成功的,这很重要 测试返回true service/TeacherServiceImplTest.java ```java @Test public void validatePassword() { // 教师中有密码,且密码与传入的密码相同,返回true Teacher teacher = new Teacher(); String password = RandomString.make(6); teacher.setPassword(password); Assert.assertTrue(this.teacherService.validatePassword(teacher, password)); ``` 测试其它 service/TeacherServiceImplTest.java ```java @Test public void validatePassword() { // 教师中有密码,且密码与传入的密码相同,返回true Teacher teacher = new Teacher(); String password = RandomString.make(6); teacher.setPassword(password); Assert.assertTrue(this.teacherService.validatePassword(teacher, password)); // 教师为null返回false Assert.assertFalse( this.teacherService.validatePassword( null, password)); // 传入的密码为null返回false Assert.assertFalse( this.teacherService.validatePassword( teacher, null)); // 未设置教师的密码,返回false teacher.setPassword(null); Assert.assertFalse( this.teacherService.validatePassword( teacher, password)); // 教师中的密码与传入的密码不相同返回false teacher.setPassword(RandomString.make(6)); Assert.assertFalse( this.teacherService.validatePassword( teacher, password)); } ``` 单元测试通过 # C层 C层的开发主要参数接口文档 controller/TeacherController.java ```java @Autowired TeacherService teacherService; ✚ ... @PostMapping("login") public boolean login(@RequestBody Teacher teacher) { return this.teacherService.login(teacher.getUsername(), teacher.getPassword()); } ``` ## 单元测试 初始化如下: controller/TeacherControllerTest.java ```java package com.mengyunzhi.springbootstudy.controller; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import static org.junit.Assert.*; @SpringBootTest ★ @RunWith(SpringRunner.class) ★ @AutoConfigureMockMvc ★ public class TeacherControllerTest { @Autowired MockMvc mockMvc; ★ @Test public void login() { } } ``` * ★ C层的单元测试需要借助于spring的MockMvc,必须依赖于spring。 完善代码: controller/TeacherControllerTest.java ``` @Test public void login() throws Exception { // 准备数据 String url = "/Teacher/login"; String username = RandomString.make(6); String password = RandomString.make(6); JSONObject jsonObject = new JSONObject(); jsonObject.put("username", username); jsonObject.put("password", password); // 当以参数username, password调用teacherService.login方法时,返回true Mockito.when(this.teacherService.login(username, password)).thenReturn(true); // 触发C层并断言返回值 this.mockMvc.perform(MockMvcRequestBuilders.post(url) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(jsonObject.toJSONString())) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string("true")➊); // 断言获取的参数与传入值相同 ArgumentCaptor<String> usernameArgumentCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor<String> passwordArgumentCaptor = ArgumentCaptor.forClass(String.class); Mockito.verify(this.teacherService).login( usernameArgumentCaptor.capture(), passwordArgumentCaptor.capture()); Assert.assertEquals(username, usernameArgumentCaptor.getValue()); Assert.assertEquals(password, passwordArgumentCaptor.getValue()); } ``` * ➊ 断言返回的内容为true 单元测试通过。 # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.1.3](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step5.1.3) | - |