在寫單元測試的過程當中,出現過許屢次java.lang.NullPointerException,而這些空指針的錯誤又是不一樣緣由形成的,本文從實際代碼出發,研究一下空指針的產生緣由。html
一句話歸納:空指針異常,是在程序在調用某個對象的某個方法時,因爲該對象爲null產生的。java
因此若是出現此異常,大多數狀況要判斷測試中的對象是否被成功的注入,以及Mock方法是否生效。git
出現空指針異常的錯誤信息以下:github
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行出現問題。dom
下面從兩個實例來具體分析。ide
目的:測試服務層的一個用於更新做業的功能。單元測試
接口測試
/** * 更新做業分數 * @param id * @param score * @return */ Work updateScore(Long id, int score);
接口實現:this
@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; }
測試:spa
@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方式。
對於Mock對象(徹底假的對象),使用when().thenReturn和doReturn().when的效果是同樣的,均可以製造一個假的返回值。
可是對於Spyk對象(半真半假的對象)就不同了,when().thenReturn會去執行真正的方法,再返回假的返回值,在這個執行真正方法的過程當中,就可能出現空指針錯誤。
而doReturn().when會直接返回假的數據,而根本不執行真正的方法。
參考連接:https://sangsoonam.github.io/...
因此把測試代碼的改爲:
- 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是否徹底正確,若是正確,就不會出現空指針異常了。
最根本的辦法,仍是去分析,找到誰是那個空對象,問題就迎刃而解。