luchob/softuni-feb2023

UnitTesting difficulty

Closed this issue · 8 comments

Здравейте, господин Балев! Много се затруднявам с тестовете. Постоянно имам грешки от NullPointerException. Парчето код, което искам да тествам, е това :

@service
public class NewsServiceImpl implements NewsService {
private final NewsRepository newsRepository;
private final ModelMapper modelMapper;

public NewsServiceImpl(NewsRepository newsRepository, ModelMapper modelMapper) {
    this.newsRepository = newsRepository;
    this.modelMapper = modelMapper;
}

@Override
public NewsServiceModel saveNews(NewsServiceModel newsServiceModel) {
    News news = modelMapper.map(newsServiceModel,News.class);
    return modelMapper.map(newsRepository.save(news), NewsServiceModel.class);
} 

А това ми е тестовият код :

@ExtendWith(MockitoExtension.class)
public class NewsServiceImplTest {

private NewsServiceImpl testNSI;
private ModelMapper modelMapper = new ModelMapper();
@Mock
NewsRepository mockNewsRepository;

@BeforeEach
void setup(){
    testNSI = new NewsServiceImpl(mockNewsRepository, modelMapper);
}

@Test
public void saveNewsTest(){
    NewsServiceModel exampleServiceModel = new NewsServiceModel();
    exampleServiceModel.setDescription("Some description in here.");
    exampleServiceModel.setFromDate(LocalDate.parse("2023-04-15"));
    exampleServiceModel.setTitle("Some Title in here");
    exampleServiceModel.setImageUrl("https://m.media-amazon.com/images/W/IMAGERENDERING_521856-T1/images/I/61+yI8cp94L._AC_SL1500_.jpg");

    NewsServiceModel newsServiceModel = testNSI.saveNews(exampleServiceModel);

    Assertions.assertEquals(newsServiceModel.getDescription(),exampleServiceModel.getDescription());
}

}
И IntelliJ ме пресича с : java.lang.NullPointerException: Cannot invoke "com.example.gametaste.service.impl.NewsServiceImpl.saveNews(com.example.gametaste.model.service.NewsServiceModel)" because "this.testNSI" is null. Не знам защо бъркам, като в сетъпа съм инициализирал testNSI. Ще съм благодарен за помощта Ви !

luchob commented

Здравей!

Може ли линк към проекта за да разгледам фейлналия тест?

Благодаря,
Л.

luchob commented

Здравей!

Проблемът е че си анотирал теста си с анотация @Test в пакет org.junit.Test. Ето:

import org.junit.Test;

Това е JUnit4. А BeforeEach е в JUnit5.

Промени import org.junit.Test;(JUnit 4) на import org.junit.jupiter.api.Test;(Junit 5).

Поздрави,
Л.

Благодаря за бързите отговори, господин Балев ! А може ли насоки за теста, който правя в момента ? Добре ли изглежда ? Проверявам ли правилно метода ? Какви насоки може да ми дадете ?

Господин Балев, може ли отново да Ви помоля за помощ ? Последният ми тест deleteById проверявам първо дали пълня репозиторито и после с testNSI изтривам конкретната новина с id 1. Обаче на този ред Assertions.assertEquals(1, mockNewsRepository.count()); ми гърми с : org.opentest4j.AssertionFailedError:
Expected :1
Actual :0 Репозиторито изобщо даже не мога да го напълня с обекта. Не знам даже дали така е правилно да го пълня, понеже нали е кух обект и не съм сигурен дали не трябва да го науча и на вградените му методи като mockNewsRepository.save(newsTest); Може да ползвате горните линкове, които поискахте да изпратя

luchob commented

Здравей!

Mock-a е кух, нищо правещ обект. Това не е истинско репозитори. Представи си приблизително така. Имаш един интерфейс, който изглежда така:

interface StudentIfc {
    
    int getGrade();
    
    Optional<Integer> findGradeForYear();
    
    boolean isNewStudent();
    
    String getName();
    
}
@ExtendWith(MockitoExtension.class)
public class StudentServiceTest {
    @Mock
    StudentIfc mockStudent;
}

мокито ще "произведе" за теб горе долу следния студент:

class MockStudent implements StudentIfc {

    @Override
    public int getGrade() {
        return 0;
    }

    @Override
    public Optional<Integer> findGradeForYear() {
        return Optional.empty();
    }

    @Override
    public boolean isNewStudent() {
        return false;
    }

    @Override
    public String getName() {
        return null;
    }
}

Тове е мок. Mockito може да ти помогне да "обучиш" този обект. Например да му кажеш:

when(mockStudent.getName()).thenReturn("Pesho");

Тогава мокито ще се погрижи горният метод да стане такъв:

 @Override
    public String getName() {
        return "Pesho";
    }

Като правиш Unit test-ове, всичко работи по този начин. Затова няма начин да "напълниш" репозиторито в unit test. То не е вързано с база. То е кухо нещо, което ти можеш да обучиш или пък да провериш кои негови методи с какво са извикани.

Надявам се това обяснява, защо горния тест не работи.

Поздрави,
Л.

Тоест ако трябва да съхранявам в репозиторито обект трябва да е when(mockrepository.save(object)).thenReturn(Object). Защо не мога да съхраня обект в репозиторито, къде бъркам, господин Балев ? Например тук репозиторито ми остава празно :
@test
public void deleteByIdTest() {

    newsTest.setId(1L);
    newsTest.setDescription(NEWS_DESCRIPTION);
    newsTest.setTitle(NEWS_TITLE);
    newsTest.setImageUrl(NEWS_IMAGE_URL);
    newsTest.setFromDate(LocalDate.parse(NEWS_DATE));


    mockNewsRepository.save(newsTest);
    Assertions.assertEquals(1, mockNewsRepository.count());

    testNSI.deleteById(1L);

    Assertions.assertEquals(0, mockNewsRepository.count());
} 

Не мога да вкарам обект в него. Изглежда, че само така мога да премина deleteById метода :
@test
public void testDeleteById() {
Long id1 = 1L;
Long id2 = 2L;
Long id3 = 3L;

    testNSI.deleteById(id1);
    testNSI.deleteById(id2);
    testNSI.deleteById(id3);


    verify(mockNewsRepository, times(1)).deleteById(id1);
    verify(mockNewsRepository, times(1)).deleteById(id2);
    verify(mockNewsRepository, times(1)).deleteById(id3);
}
luchob commented

Зравей!

Защо не мога да съхраня обект в репозиторито, къде бъркам

Бъркаш в това, че това не е репозитори. Прочети ми коментара още веднъж. Това е mockRepository и ако искаш ще ти покажа имплементацията на save метода. Тя е ето такава:

void save(Entity e) {
}

Както виждаш, никога няма да го напълниш. Когато работиш с мок можеш да правиш следните неща:

  1. Да обучиш мока да връща определена стойност при определени аргументи. Например:
Mockito.when(aMock.getById(1)).thenReturn(anObject)
  1. Да провериш дали някой (например сървиса ти) е извикал някой метод на мока (напр мок репозиторито). Например:
verify(mockNewsRepository, times(1)).deleteById(id1);
  1. Да видиш провериш с какъв аргумент е извикан метод мока. За това се използва ArgumentCaptor.

Когато пишеш тестове с мок трябва да имаш ясната идея какво трябва да прави теста. Например:
//arrange - създаваш някакви обекти и евентуално обучаваш моковете, които са депендънси на сървиса
//act - вика се метод на сървиса с някакви аргументи
//assert - проверява се дали върната стойност е коректна и дали моковете, които са депендънси са извикани правилно.

Например:

// arrange - подготвяме някаква тестова стойност
Long id = 1;
//act - викаме сървиса
testNSI.deleteById(id1);
//assert - проверяваме че сървиса е извикал правилния метод на мок депендънсито правилен брой пъти
verify(mockNewsRepository, times(1)).deleteById(id1);

Ако искаш да пълниш репозиторита, които са истински тогава ти е необходим интегрейшън тест.

Поздрави,
Л.