前言
在写单元测试的过程中,出现过许多次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行出现问题。
下面从两个实例来具体分析。
实例
(代码仅为了报错时方便分析,请勿仔细阅读,避免浪费时间)
1
目的:测试服务层的一个用于更新作业的功能。
接口
/**
* 更新作业分数
* @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();
Mockito.when(this.workRepository.save(Mockito.eq(oldWork)))
.thenReturn(resultWork);
203 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方式。
对于Mock对象(完全假的对象),使用when().thenReturn和doReturn().when的效果是一样的,都可以制造一个假的返回值。
但是对于Spy对象(半真半假的对象)就不一样了,when().thenReturn会去执行真正的方法,再返回假的返回值,在这个执行真正方法的过程中,就可能出现空指针错误。
而doReturn().when会直接返回假的数据,而根本不执行真正的方法。
参考链接:https://sangsoonam.github.io/...
所以把测试代码的改成:
- Mockito.when(this.workService.isTeacher()).thenReturn(true);
+ Mockito.doReturn(true).when(workService).isTeacher();
再次运行,就能通过测试。
2
目的:还是测试之前的方法,只不过新增了功能。
接口
/**
* 更新作业分数
* @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 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 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: but was:
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中的方法,返回了空的学生。
解决方法:
- 在测试类WorkServiceImplTest中删除studentService的注入,使用父类。
- 使用子类的studentService,并在所有的报错位置,加入对应的mock方法
总结
java.lang.NullPointerException直接翻译过来是空指针,但根本原因却不是空对象,一定是由于某种错误的操作(错误的注入),导致了空对象。
最常见的情况,就是在测试时执行了真正的方法,而不是mock方法。
此时的解决方案,就是检查所有的依赖注入和Mock是否完全正确,如果正确,就不会出现空指针异常了。
最根本的办法,还是去分析,找到谁是那个空对象,问题就迎刃而解。