本节我们处理两个长度的校验。在原型中我们规定了学号的长度必须是6位,而姓名则最短为2位,最长为20位。我们在上两个小节中分别通过了@JoinColumn及@Column进行非null及unique设置,这是由于数据库本身就是支持这样的校验的。 当我们对其进行null设置时,jpa会自动在数据表的对应字段上设置`不是null`属性: ![](https://img.kancloud.cn/a5/8c/a58c8aa43282f92b389245ae8b2a1635_769x153.png) 当我们对其进行unique设置时,jpa会自动在数据表中为对应的字段添加UNIQUE类型的索引: ![](https://img.kancloud.cn/31/ba/31baf9653f4a9a83c16193536c604e71_1508x218.png) 但数据库却并不支持对某个字段设置其长度必须为多少位,或是其长度必须位于哪两个值之间。所以此时@JoinColumn及@Column便解决不了这个问题了,这也是当我们查看@JoinColumn及@Column官方文档时,并没有找到对应的选项的原因。 > 这两个注解中有一个length选项,但其官方的解释为:(Optional) The column length. (Applies only if a string-valued column is used.) ,译为:字段长度。该长度是指该字段所允许的最大长度,传入的值只要不超过该值即可。但这并不是我们想要的。 为了处理这种问题,JPA为我们提供了@PrePersist注解,在数据正式被保存前,该注解下的方法将被触发执行1次。 ## @PrePersist 我们在entity/Student.java中建立以下方法: ``` /** * 在实体保存到数据库以前,执行1次 */ @PrePersist public void perPersis() { } ``` ### 补充代码 继续补充该方法中的代码,完成name和sno的长度校验。 ``` @Column(nullable = false) private String name; /** * 在实体保存到数据库以前,执行1次 * 1. 校验name 字段长度为2-20 * 2. 校验sno 字段长为为6 */ @PrePersist public void perPersis() { if (this.name != null ) { ① if (this.name.length() < 2) { throw new DataIntegrityViolationException("name length less than 2"); ➊ } if (this.name.length() > 20) { throw new DataIntegrityViolationException("name length more than 20"); ➊ } } if (this.sno != null) { ② if (this.sno.length() != 6) { throw new DataIntegrityViolationException("sno length must be 6"); ➊ } } } ``` * ① 对name进行校验 * ② 对sno进行校验 * ➊ 抛出更通用的DataIntegrityViolationException异常,同时在异常中给出有指导意义的提示 ## 测试 姓名过短: ``` @Test(expected = DataIntegrityViolationException.class) public void nameLengthToShortTest() { this.student.setName("1"); this.studentRepository.save(student); } ``` 姓名过长: ``` @Test(expected = DataIntegrityViolationException.class) public void nameLengthToLongTest() { this.student.setName("123456789012345678901"); this.studentRepository.save(student); } ``` 学号长度非6位: ``` @Test(expected = DataIntegrityViolationException.class) public void snoLengthTest() { this.student.setSno("12345"); this.studentRepository.save(student); } ``` ### 增加测试样本及细化测试 虽然使用@Test(expected = DataIntegrityViolationException.class)能够快速的测试异常,但这种方法存在先天的不足,比如:每个测试用例只能测试一次异常。当我们需要进行多样本测试的时候,它便显得力不从心了。在刚刚测试中,我们每个测试用例中均使用了一个样本。这为我们的后续更新造成了一定的风险。比如学号的长度由6位升级为8位,我们来在Student.java中,将6修改为8,却发现原来的单元测试仍然被通过了。这是由于我们的单元测试的逻辑为:将学号为5位时,触发异常。而无论学号的长度是6位还是8位,都会满足长度不为5的单元测试。而正常的测试逻辑则应该是,我们使用多个长度的学号进行测试,仅当长度为6时不报错。 所以:一个合格的测试应该长成这样: ``` import org.assertj.core.internal.bytebuddy.utility.RandomString; @Test public void snoLengthTest() { for (int i = 1; i <= 255; i++) { ① this.student.setSno(RandomString.make(i)); ② boolean called = false; try { this.studentRepository.save(student); } catch (DataIntegrityViolationException e) { called = true; } if (i != 6) { Assertions.assertThat(called).isTrue(); ③ } else { Assertions.assertThat(called).isFalse(); ④ } this.before(); ⑤ } } ``` * ① 测试255次 * ② 获取长度为i的字符串,并用此字符串来设置学号 * ③ 当字符串的长度为6时,断言未发生异常 * ④ 当字符串的长度不为6时,断言发生异常 * ⑤ 生成一个新学生 此时,如果我们将Student中的长度校验由6改为其它长度时,则单元测试将无法通过。 **请自行完成name字段的长度校验后继续学习** ### 多测试用例间互相影响 至此我们完成了学生实体的校验过程,我们大概写了10来个单元测试。接下来我们做个奇怪的实验:单独运行任何一个单元测试均正常通过测试;但统一运行该测试文件的所有测试却发生了错误: ![](https://img.kancloud.cn/1b/f9/1bf904a4e72bf68c1fbcd6ef5b63bce7_493x167.png) 失败: ![](https://img.kancloud.cn/b7/92/b792824b2c95539453b2fe70a10c9e59_412x162.png) 这是由于对某个测试文件进行测试时相当于对该文件中的所有测试文件进行逐个测试,这就会面临多个单元测试用例互相影响的问题。 * [ ] 只运行一个测试用例,该测试用例执行完毕后,JPA自动为我们删除了数据库;再运行另一个测试用例时,数据库为空库。两个测试用例互不影响。 * [ ] 运行一个测试文件,该测试文件中的所有测试用例执行完毕后,此时JPA自动为我们删除了数据库。也就是说在此测试文件中的测试没有全部被执行完前,该测试文件中的测试用例使用的是同一个数据库。这便是产生冲突异常的原因。 我们点击单元测试如下按钮后,将显示各个测试用例的执行顺序: ![](https://img.kancloud.cn/0e/61/0e61c290df02832b0e9ad0516370d3b6_346x364.png) 如上图所示,在执行save操作前已经执行过了snoUniqueTest方法。而该方法中的测试代码曾经在数据表中为我们成功的添加了一个学号为032282的学生;在后续执行save方法时,我们再次尝试在数据表中写入一个学号为032282的学生,此时便发生了唯一性校验错误。解决这个问题的方法也很简单----随机字符串: 我们把before的方法修正如下: ``` this.student.setName("测试名称"); this.student.setSno("032282"); ✘ this.student.setSno(RandomString.make(6)); ✚ ① this.student.setKlass(this.klass); ``` * ① 每次运行都生成一个随机的学号 然后再测试便可以规避学号互相影响的问题: ![](https://img.kancloud.cn/5b/83/5b83efe5a82f59e9c3df2a2bfbdbbf12_377x388.png) 当然,这也引发了一个snoUniqueTest无法通过的新问题,我们打开该方法再查看一下: ``` @Test public void snoUniqueTest() { this.studentRepository.save(this.student); this.before(); boolean called = false; try { this.studentRepository.save(this.student); } catch (DataIntegrityViolationException e) { called = true; } Assertions.assertThat(called).isTrue(); } ``` 最终发现:由于两次生成的学生的学号不一样了,导致第二次学生的保存操作时**未**抛出学号校验异常,我们将此代码修正如下: ``` @Test public void snoUniqueTest() { String sno = RandomString.make(6); ① this.student.setSno(sno); ② this.studentRepository.save(this.student); this.before(); this.student.setSno(sno); ② boolean called = false; try { this.studentRepository.save(this.student); } catch (DataIntegrityViolationException e) { called = true; } Assertions.assertThat(called).isTrue(); } ``` * ① 生成一个在方法内部用的学号 * ② 在两次保存学生前,分别用同一个学号来对学生进行设置 此时我们再测试,所有的单元测试便正常通过了 : ![](https://img.kancloud.cn/8e/86/8e86892d4f3670aa16d993a8870c727b_344x365.png) # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.5.8](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.5.8) | - | | PerPersist | [https://docs.oracle.com/javaee/7/api/javax/persistence/PrePersist.html](https://docs.oracle.com/javaee/7/api/javax/persistence/PrePersist.html) | 2 |