上节小定义了接口规范:
<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) | - |
- 序言
- 第一章:Hello World
- 第一节:Angular准备工作
- 1 Node.js
- 2 npm
- 3 WebStorm
- 第二节:Hello Angular
- 第三节:Spring Boot准备工作
- 1 JDK
- 2 MAVEN
- 3 IDEA
- 第四节:Hello Spring Boot
- 1 Spring Initializr
- 2 Hello Spring Boot!
- 3 maven国内源配置
- 4 package与import
- 第五节:Hello Spring Boot + Angular
- 1 依赖注入【前】
- 2 HttpClient获取数据【前】
- 3 数据绑定【前】
- 4 回调函数【选学】
- 第二章 教师管理
- 第一节 数据库初始化
- 第二节 CRUD之R查数据
- 1 原型初始化【前】
- 2 连接数据库【后】
- 3 使用JDBC读取数据【后】
- 4 前后台对接
- 5 ng-if【前】
- 6 日期管道【前】
- 第三节 CRUD之C增数据
- 1 新建组件并映射路由【前】
- 2 模板驱动表单【前】
- 3 httpClient post请求【前】
- 4 保存数据【后】
- 5 组件间调用【前】
- 第四节 CRUD之U改数据
- 1 路由参数【前】
- 2 请求映射【后】
- 3 前后台对接【前】
- 4 更新数据【前】
- 5 更新某个教师【后】
- 6 路由器链接【前】
- 7 观察者模式【前】
- 第五节 CRUD之D删数据
- 1 绑定到用户输入事件【前】
- 2 删除某个教师【后】
- 第六节 代码重构
- 1 文件夹化【前】
- 2 优化交互体验【前】
- 3 相对与绝对地址【前】
- 第三章 班级管理
- 第一节 JPA初始化数据表
- 第二节 班级列表
- 1 新建模块【前】
- 2 初识单元测试【前】
- 3 初始化原型【前】
- 4 面向对象【前】
- 5 测试HTTP请求【前】
- 6 测试INPUT【前】
- 7 测试BUTTON【前】
- 8 @RequestParam【后】
- 9 Repository【后】
- 10 前后台对接【前】
- 第三节 新增班级
- 1 初始化【前】
- 2 响应式表单【前】
- 3 测试POST请求【前】
- 4 JPA插入数据【后】
- 5 单元测试【后】
- 6 惰性加载【前】
- 7 对接【前】
- 第四节 编辑班级
- 1 FormGroup【前】
- 2 x、[x]、{{x}}与(x)【前】
- 3 模拟路由服务【前】
- 4 测试间谍spy【前】
- 5 使用JPA更新数据【后】
- 6 分层开发【后】
- 7 前后台对接
- 8 深入imports【前】
- 9 深入exports【前】
- 第五节 选择教师组件
- 1 初始化【前】
- 2 动态数据绑定【前】
- 3 初识泛型
- 4 @Output()【前】
- 5 @Input()【前】
- 6 再识单元测试【前】
- 7 其它问题
- 第六节 删除班级
- 1 TDD【前】
- 2 TDD【后】
- 3 前后台对接
- 第四章 学生管理
- 第一节 引入Bootstrap【前】
- 第二节 NAV导航组件【前】
- 1 初始化
- 2 Bootstrap格式化
- 3 RouterLinkActive
- 第三节 footer组件【前】
- 第四节 欢迎界面【前】
- 第五节 新增学生
- 1 初始化【前】
- 2 选择班级组件【前】
- 3 复用选择组件【前】
- 4 完善功能【前】
- 5 MVC【前】
- 6 非NULL校验【后】
- 7 唯一性校验【后】
- 8 @PrePersist【后】
- 9 CM层开发【后】
- 10 集成测试
- 第六节 学生列表
- 1 分页【后】
- 2 HashMap与LinkedHashMap
- 3 初识综合查询【后】
- 4 综合查询进阶【后】
- 5 小试综合查询【后】
- 6 初始化【前】
- 7 M层【前】
- 8 单元测试与分页【前】
- 9 单选与多选【前】
- 10 集成测试
- 第七节 编辑学生
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 功能开发【前】
- 4 JsonPath【后】
- 5 spyOn【后】
- 6 集成测试
- 7 @Input 异步传值【前】
- 8 值传递与引入传递
- 9 @PreUpdate【后】
- 10 表单验证【前】
- 第八节 删除学生
- 1 CSS选择器【前】
- 2 confirm【前】
- 3 功能开发与测试【后】
- 4 集成测试
- 5 定制提示框【前】
- 6 引入图标库【前】
- 第九节 集成测试
- 第五章 登录与注销
- 第一节:普通登录
- 1 原型【前】
- 2 功能设计【前】
- 3 功能设计【后】
- 4 应用登录组件【前】
- 5 注销【前】
- 6 保留登录状态【前】
- 第二节:你是谁
- 1 过滤器【后】
- 2 令牌机制【后】
- 3 装饰器模式【后】
- 4 拦截器【前】
- 5 RxJS操作符【前】
- 6 用户登录与注销【后】
- 7 个人中心【前】
- 8 拦截器【后】
- 9 集成测试
- 10 单例模式
- 第六章 课程管理
- 第一节 新增课程
- 1 初始化【前】
- 2 嵌套组件测试【前】
- 3 async管道【前】
- 4 优雅的测试【前】
- 5 功能开发【前】
- 6 实体监听器【后】
- 7 @ManyToMany【后】
- 8 集成测试【前】
- 9 异步验证器【前】
- 10 详解CORS【前】
- 第二节 课程列表
- 第三节 果断
- 1 初始化【前】
- 2 分页组件【前】
- 2 分页组件【前】
- 3 综合查询【前】
- 4 综合查询【后】
- 4 综合查询【后】
- 第节 班级列表
- 第节 教师列表
- 第节 编辑课程
- TODO返回机制【前】
- 4 弹出框组件【前】
- 5 多路由出口【前】
- 第节 删除课程
- 第七章 权限管理
- 第一节 AOP
- 总结
- 开发规范
- 备用