在写单元测试的过程中,出现过许多次 java.lang.NullPointerException ,而这些空指针的错误又是不同原因造成的,本文从实际代码出发,研究一下空指针的产生原因。
一句话概括: 空指针异常,是在程序执行时在调用某个对象的某个方法时,由于该对象为null产生的 。
所以如果出现此异常,大多数情况要判断测试中的 对象是否被成功的注入 ,以及 Mock方法是否生效 。
出现空指针异常的错误信息如下:
java.lang.NullPointerException at club.yunzhi.workhome.service.WorkServiceImpl.updateOfCurrentStudent(WorkServiceImpl.java:178) at club.yunzhi.workhome.service.WorkServiceImplTest.updateOfCurrentStudent(WorkServiceImplTest.java:137) at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
这实际上是方法栈,就是在 WorkServiceImplTest.java
测试类的137行调用 WorkServiceImpl.java
被测试类的178行出现问题。
下面从两个实例来具体分析。
目的:测试服务层的一个用于更新作业的功能。
接口
/** * 更新作业分数 * @param id * @param score * @return */ Work updateScore(Long id, int score);
接口实现:
@Service public class WorkServiceImpl implements WorkService { private static final Logger logger = LoggerFactory.getLogger(WorkServiceImpl.class); private static final String WORK_PATH = "work/"; final WorkRepository workRepository; final StudentService studentService; final UserService userService; final ItemRepository itemRepository; final AttachmentService attachmentService; public WorkServiceImpl(WorkRepository workRepository, StudentService studentService, UserService userService, ItemRepository itemRepository, AttachmentService attachmentService) { this.workRepository = workRepository; this.studentService = studentService; this.userService = userService; this.itemRepository = itemRepository; this.attachmentService = attachmentService; } ... @Override public Work updateScore(Long id, int score) { Work work = this.workRepository.findById(id) .orElseThrow(() -> new ObjectNotFoundException("未找到ID为" + id + "的作业")); if (!this.isTeacher()) { throw new AccessDeniedException("无权判定作业"); } work.setScore(score); logger.info(String.valueOf(work.getScore())); return this.save(work); } @Override public boolean isTeacher() { User user = this.userService.getCurrentLoginUser(); 130 if (user.getRole() == 1) { return false; } return true; }
测试:
@Test public void updateScore() { Long id = this.random.nextLong(); Work oldWork = new Work(); oldWork.setStudent(this.currentStudent); oldWork.setItem(Mockito.spy(new Item())); int score = 100; Mockito.when(this.workRepository.findById(Mockito.eq(id))) .thenReturn(Optional.of(oldWork)); Mockito.doReturn(true) .when(oldWork.getItem()) .getActive(); Work work = new Work(); work.setScore(score); Work resultWork = new Work(); 203 Mockito.when(this.workRepository.save(Mockito.eq(oldWork))) .thenReturn(resultWork); Assertions.assertEquals(resultWork, this.workService.updateScore(id, score)); Assertions.assertEquals(oldWork.getScore(), work.getScore()); }
运行测试,出现空指针:
java.lang.NullPointerException
at club.yunzhi.workhome.service.WorkServiceImpl.isTeacher(WorkServiceImpl.java:130) at club.yunzhi.workhome.service.WorkServiceImplTest.updateScore(WorkServiceImplTest.java:203) at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
问题出在功能代码的第130行,可以看到报错的代码根本不是要测试的方法,而是被调用的方法。
再看测试代码的203行,测试时的本来目的是为了Mock掉这个方法,但使用的是when().thenReturn方式。
对于 Spy 对象(完全假的对象),使用 when().thenReturn 和 doReturn().when 的效果是一样的,都可以制造一个假的返回值。
但是对于 Mock 对象(半真半假的对象)就不一样了, when().thenReturn 会去执行真正的方法,再返回假的返回值,在这个执行真正方法的过程中,就可能出现空指针错误。
而 doReturn().when 会直接返回假的数据,而根本不执行真正的方法。
所以把测试代码的改成:
- Mockito.when(this.workService.isTeacher()).thenReturn(true); + Mockito.doReturn(true).when(workService).isTeacher();
再次运行,就能通过测试。
目的:还是测试之前的方法,只不过新增了功能。
接口
/** * 更新作业分数 * @param id * @param score * @return */ Work updateScore(Long id, int score);
接口实现(在原有的储存学生成绩方法上新增了计算总分的功能)
@Override public Work updateScore(Long id, int score) { Work work = this.workRepository.findById(id) .orElseThrow(() -> new ObjectNotFoundException("未找到ID为" + id + "的作业")); if (!this.isTeacher()) { throw new AccessDeniedException("无权判定作业"); } work.setScore(score); work.setReviewed(true); logger.info(String.valueOf(work.getScore())); + //取出此学生的所有作业 + List<Work> currentStudentWorks = this.workRepository.findAllByStudent(work.getStudent()); + //取出此学生 + Student currentStudent = this.studentService.findById(work.getStudent().getId()); + currentStudent.setTotalScore(0); + int viewed = 0; + + for (Work awork : currentStudentWorks) { + if (awork.getReviewed() == true) { + viewed++; + //计算总成绩 + currentStudent.setTotalScore(currentStudent.getTotalScore()+awork.getScore()); + //计算平均成绩 + currentStudent.setAverageScore(currentStudent.getTotalScore()/viewed); + } + } + + studentRepository.save(currentStudent); return this.save(work); }
由于出现了对学生仓库studentRepository的调用,需要注入:
final WorkRepository workRepository; final StudentService studentService; final UserService userService; final ItemRepository itemRepository; final AttachmentService attachmentService; +final StudentRepository studentRepository; -public WorkServiceImpl(WorkRepository workRepository, StudentService studentService, UserService userService, ItemRepository itemRepository, AttachmentService attachmentService) { +public WorkServiceImpl(WorkRepository workRepository, StudentService studentService, UserService userService, ItemRepository itemRepository, AttachmentService attachmentService, StudentRepository studentRepository) { this.workRepository = workRepository; this.studentService = studentService; this.userService = userService; this.itemRepository = itemRepository; this.attachmentService = attachmentService; + this.studentRepository = studentRepository; }
然后是测试代码
class WorkServiceImplTest extends ServiceTest { private static final Logger logger = LoggerFactory.getLogger(WorkServiceImplTest.class); WorkRepository workRepository; UserService userService; ItemRepository itemRepository; ItemService itemService; WorkServiceImpl workService; AttachmentService attachmentService; +StudentService studentService; +StudentRepository studentRepository; @Autowired private ResourceLoader loader; @BeforeEach public void beforeEach() { super.beforeEach(); this.itemService = Mockito.mock(ItemService.class); this.workRepository = Mockito.mock(WorkRepository.class); this.userService = Mockito.mock(UserService.class); this.itemRepository = Mockito.mock(ItemRepository.class); this.studentService = Mockito.mock(StudentService.class); this.studentRepository = Mockito.mock(StudentRepository.class); this.workService = Mockito.spy(new WorkServiceImpl(this.workRepository, this.studentService, + this.userService, this.itemRepository, this.attachmentService, this.studentRepository)); } ... @Test public void updateScore() { Long id = this.random.nextLong(); Work oldWork = new Work(); oldWork.setScore(0); oldWork.setStudent(this.currentStudent); oldWork.setItem(Mockito.spy(new Item())); + Work testWork = new Work(); + testWork.setScore(0); + testWork.setReviewed(true); + testWork.setStudent(this.currentStudent); + testWork.setItem(Mockito.spy(new Item())); int score = 100; + List<Work> works= Arrays.asList(oldWork, testWork); + + Mockito.doReturn(Optional.of(oldWork)) + .when(this.workRepository) + .findById(Mockito.eq(id)); + Mockito.doReturn(works) + .when(this.workRepository) + .findAllByStudent(oldWork.getStudent()); Mockito.doReturn(true) .when(oldWork.getItem()) .getActive(); + Mockito.doReturn(this.currentStudent) + .when(this.studentService) .findById(oldWork.getStudent().getId()); Work work = new Work(); work.setScore(score); work.setReviewed(true); Work resultWork = new Work(); Mockito.when(this.workRepository.save(Mockito.eq(oldWork))) .thenReturn(resultWork); Mockito.doReturn(true).when(workService).isTeacher(); Assertions.assertEquals(resultWork, this.workService.updateScore(id, score)); Assertions.assertEquals(oldWork.getScore(), work.getScore()); Assertions.assertEquals(oldWork.getReviewed(),work.getReviewed()); + Assertions.assertEquals(oldWork.getStudent().getTotalScore(), 100); + Assertions.assertEquals(oldWork.getStudent().getAverageScore(), 50); } ... }
顺利通过测试,看似没什么问题,可是一跑全局单元测试,就崩了。
[ERROR] Failures: 492[ERROR] WorkServiceImplTest.saveWorkByItemIdOfCurrentStudent:105 expected: <club.yunzhi.workhome.entity.Student@1eb207c3> but was: <null> 493[ERROR] Errors: 494[ERROR] WorkServiceImplTest.getByItemIdOfCurrentStudent:73 » NullPointer 495[ERROR] WorkServiceImplTest.updateOfCurrentStudent:138 » NullPointer 496[INFO] 497[ERROR] Tests run: 18, Failures: 1, Errors: 2, Skipped: 0
一个断言错误,两个空指针错误。
可是这些三个功能我根本就没有改,而且是之前已经通过测试的功能,为什么会出错呢?
拿出一个具体的错误,从本地跑一下测试:
测试代码
@Test public void updateOfCurrentStudent() { Long id = this.random.nextLong(); Work oldWork = new Work(); oldWork.setStudent(this.currentStudent); oldWork.setItem(Mockito.spy(new Item())); Mockito.when(this.workRepository.findById(Mockito.eq(id))) .thenReturn(Optional.of(oldWork)); //Mockito.when(this.studentService.getCurrentStudent()).thenReturn(this.currentStudent); Mockito.doReturn(true) .when(oldWork.getItem()) .getActive(); Work work = new Work(); work.setContent(RandomString.make(10)); work.setAttachments(Arrays.asList(new Attachment())); Work resultWork = new Work(); Mockito.when(this.workRepository.save(Mockito.eq(oldWork))) .thenReturn(resultWork); 137 Assertions.assertEquals(resultWork, this.workService.updateOfCurrentStudent(id, work)); Assertions.assertEquals(oldWork.getContent(), work.getContent()); Assertions.assertEquals(oldWork.getAttachments(), work.getAttachments()); }
功能代码
@Override public Work updateOfCurrentStudent(Long id, @NotNull Work work) { Assert.notNull(work, "更新的作业实体不能为null"); Work oldWork = this.workRepository.findById(id) .orElseThrow(() -> new ObjectNotFoundException("未找到ID为" + id + "的作业")); 178 if (!oldWork.getStudent().getId().equals(this.studentService.getCurrentStudent().getId())) { throw new AccessDeniedException("无权更新其它学生的作业"); } if (!oldWork.getItem().getActive()) { throw new ValidationException("禁止提交已关闭的实验作业"); } oldWork.setContent(work.getContent()); oldWork.setAttachments(work.getAttachments()); return this.workRepository.save(oldWork); }
报错信息
java.lang.NullPointerException
at club.yunzhi.workhome.service.WorkServiceImpl.updateOfCurrentStudent(WorkServiceImpl.java:178) at club.yunzhi.workhome.service.WorkServiceImplTest.updateOfCurrentStudent(WorkServiceImplTest.java:137) at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) at java.base/java.util.ArrayList.forEach(ArrayList.java:1540) at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33) at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230) at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
根据报错信息来看,是测试类在调用功能代码178行时,出现了空指针,
经过分析,在执行 this.studentService.getCurrentStudent().getId()
时出现的。
然后就来判断studentService的注入情况,
//父类的BeforeEach public void beforeEach() { this.studentService = Mockito.mock(StudentService.class); this.currentStudent.setId(this.random.nextLong()); Mockito.doReturn(currentStudent) .when(this.studentService) .getCurrentStudent(); }
//测试类的BeforeEach @BeforeEach public void beforeEach() { super.beforeEach(); this.itemService = Mockito.mock(ItemService.class); this.workRepository = Mockito.mock(WorkRepository.class); this.userService = Mockito.mock(UserService.class); this.itemRepository = Mockito.mock(ItemRepository.class); this.studentService = Mockito.mock(StudentService.class); this.studentRepository = Mockito.mock(StudentRepository.class); this.workService = Mockito.spy(new WorkServiceImpl(this.workRepository, this.studentService, this.userService, this.itemRepository, this.attachmentService, this.studentRepository)); }
问题就出在这里,由于测试类执行了继承,父类已经Mock了一个studentService并且成功的设定了Moockito的返回值,但测试类又进行了一次赋值,这就使得父类的Mock失效了,于是导致之前本来能通过的单元测试报错了。
所以本实例的根本问题是, 重复注入了对象 。
这导致了原有的mock方法被覆盖,以至于执行了真实的studentService中的方法,返回了空的学生。
解决方法:
java.lang.NullPointerException直接翻译过来是空指针,但根本原因却不是空对象,一定是由于某种错误的操作(错误的注入),导致了空对象。
最常见的情况,就是在测试时执行了真正的方法,而不是mock方法。
此时的解决方案,就是检查所有的依赖注入和Mock是否完全正确,如果正确,就不会出现空指针异常了。
最根本的办法,还是去分析,找到谁是那个空对象,问题就迎刃而解。