Problem beim Mocken

8u3631984

Bekanntes Mitglied
Hallo zusammen, ich verwende das Mockito Framework. HIer mal Source Code :
Java:
package my.photoalbum.photo.tag;
...

@Builder(setterPrefix = "with", toBuilder = true)
@Entity
@Table(name = "tag")
@ToString
public class Tag implements DatabaseObject {

    @Id
    @GeneratedValue
    @Column(updatable = false)
    @Getter
    private long ID;

    // required parameter
    @NonNull
    @Getter
    private String name;
}

Java:
package my.photoalbum.photo.tag;

...

public interface TagRepository extends JpaRepository<Tag, Long> {

    /**
     * can find a tag object
     *
     * @param name
     * @return the existing tag object or an empty object
     */
    Optional<Tag> findByName(String name);
}

Java:
package my.photoalbum.photo.tag;

import static net.logstash.logback.argument.StructuredArguments.kv;

import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Service;

import com.google.common.collect.Lists;

import lombok.NonNull;
import lombok.extern.log4j.Log4j2;
import my.photoalbum.database.DatabaseService;
import my.photoalbum.photo.geolocation.GeoLocation;
import my.photoalbum.photo.metadata.MetaData;

@Service
@Log4j2
public class TagService extends DatabaseService<Tag> {

    private final TagRepository repository;

    protected TagService(@NonNull TagRepository repository) {
        super(repository);
        this.repository = repository;
    }

    @Override
    protected Optional<Tag> getExisting(@NonNull Tag tag) {
        return repository.findByName(tag.getName());
    }

    /**
     * build tag by name
     *
     * @param name
     * @return the build tag object
     */
    protected Tag buildTag(@NonNull String name) {
        var tag = Tag.builder()
                .withName(name)
                .build();

        log.info("build {} ", kv("tag object", tag));
        return tag;
    }

    /**
     * build tags from geo location object
     *
     * @param geolocation
     * @return the build tags
     */
    public List<Tag> buildTag(@NonNull GeoLocation geolocation) {
        List<Tag> tagList = Lists.newArrayList();

        if (!geolocation.getCountry().isEmpty())
            tagList.add(getExistingOrSave(buildTag(geolocation.getCountry())));

        if (!geolocation.getCity().isEmpty())
            tagList.add(getExistingOrSave(buildTag(geolocation.getCity())));

        if (!geolocation.getSuburb().isEmpty())
            tagList.add(getExistingOrSave(buildTag(geolocation.getSuburb())));

        if (!geolocation.getStreet().isEmpty())
            tagList.add(getExistingOrSave(buildTag(geolocation.getStreet())));

        if (!geolocation.getPostCode().isEmpty())
            tagList.add(getExistingOrSave(buildTag(geolocation.getPostCode())));

        return tagList;
    }

    /**
     * build tags from metadata object
     *
     * @param metaData
     * @return the build tags
     */
    public List<Tag> buildTag(@NonNull MetaData metaData) {
        return Lists.newArrayList();
    }
}

So und nun mein Testcode :

Java:
package my.photoalbum.photo.tag;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class TagServiceTest {

    private TagService service;

    @Mock
    private TagRepository repository;

    @BeforeEach
    void init() {
        service = new TagService(repository);
    }

    @Test
    void canGetAllTagOjects() {
        service.getAll();

        verify(repository).findAll();
    }

    @Test
    void canSaveTagObjectIfNotExists() {
        given(repository.findByName("Test"))
                .willReturn(Optional.empty());

        service.getExistingOrSave(mock(Tag.class));

        verify(repository).saveAndFlush(any(Tag.class));
    }

    @Test
    void canBuildTagObjectByName() {
        var name = "TestTagName";
        var tag = service.buildTag(name);

        assertThat(tag).isNotNull();

        assertThat(tag.getName()).isNotNull();
        assertThat(tag.getName()).isEqualTo(name);
    }

}

Im Test canSaveTagObjectIfNotExists kommt folgende Exception :
Java:
org.mockito.exceptions.misusing.PotentialStubbingProblem:
Strict stubbing argument mismatch. Please check:
 - this invocation of 'findByName' method:
    repository.findByName(null);
 at my.photoalbum.photo.tag.TagService.getExisting(TagService.java:31)
 - has following stubbing(s) with different arguments:
    1. repository.findByName("Test");
 at my.photoalbum.photo.tag.TagServiceTest.canSaveTagObjectIfNotExists(TagServiceTest.java:42)
Typically, stubbing argument mismatch indicates user mistake when writing tests.
Mockito fails early so that you can debug potential problem easily.
However, there are legit scenarios when this exception generates false negative signal:
  - stubbing the same method multiple times using 'given().will()' or 'when().then()' API
    Please use 'will().given()' or 'doReturn().when()' API for stubbing.
  - stubbed method is intentionally invoked with different arguments by code under test
    Please use default or 'silent' JUnit Rule (equivalent of Strictness.LENIENT).
For more information see javadoc for PotentialStubbingProblem class.
 at my.photoalbum.photo.tag.TagService.getExisting(TagService.java:31)
 at my.photoalbum.photo.tag.TagService.getExisting(TagService.java:18)
 at my.photoalbum.database.DatabaseService.getExistingOrSave(DatabaseService.java:34)
 at my.photoalbum.photo.tag.TagServiceTest.canSaveTagObjectIfNotExists(TagServiceTest.java:45)
 at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
 at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

Leider bin ich bei meinen Versuchen den Fehler zu behebn nicht weiter gekommen. Hat jemand eine Idee ?
Vielen Dank
 
Prüfe mal deine Compiler-Optionen oder verwende die Param-Annotation:
Java:
Optional<Tag> findByName(@Param("name") String name);
 
Wie sieht denn der Code aus, der angemeckert wird:

Java:
 at my.photoalbum.photo.tag.TagService.getExisting(TagService.java:31)
 at my.photoalbum.photo.tag.TagService.getExisting(TagService.java:18)
 at my.photoalbum.database.DatabaseService.getExistingOrSave(DatabaseService.java:34)

Ich würde da erstmal Mockito vertrauen und davon ausgehen, dass es wirklich den unerwarteten Aufruf mit null gab.
 
Hier ist der COde der Basisklasse
Java:
@Service
public abstract class DatabaseService<T extends DatabaseObject> {

    private final JpaRepository<T, Long> repository;

    protected DatabaseService(@NonNull JpaRepository<T, Long> repository) {
        this.repository = repository;
    }

    /**
     * @return a list with all database objects
     */
    public List<T> getAll() {
        return repository.findAll();
    }

    /**
     * get the existing database object or save a new one
     *
     * @param databaseObject
     * @return the existing or saved database object
     */
    public T getExistingOrSave(@NonNull T databaseObject) {
        return getExisting(databaseObject).orElseGet(() -> save(databaseObject));
    }

    /**
     * check if the databse object exists already
     *
     * @param databaseObject
     * @return the existing database object or an empty object
     */
    protected abstract Optional<T> getExisting(@NonNull T databaseObject);

    /**
     * save the database object
     *
     * @param databaseObject
     * @return the saved database object
     */
    protected T save(@NonNull T databaseObject) {
        return repository.saveAndFlush(databaseObject);
    }
}
 
Ich glaube ich habe das Problem gefunden :
Java:
    @Test
    void canSaveTagObjectIfNotExists() {
        var tag = Tag.builder().withName("TestTag").build();
        
        given(repository.findByName(anyString()))
                .willReturn(Optional.empty());

        service.getExistingOrSave(tag);

        verify(repository).saveAndFlush(any(Tag.class));
    }

Das Feld name ist in der Klasse Tag mit @NonNull annotiert. Daher kommt die Nullpointer Exception
 
Daher kommt die Nullpointer Exception
Welche Nullpointer-Exception denn plötzlich?

Fehler wird diese Zeile sein:
service.getExistingOrSave(mock(Tag.class));
Der gemockte Tag hat dann keinen Namen, sondern eben null, was dann bei repository.findByName(tag.getName()); zu dem Aufruf mit null führt, mit dem Mockito nichts anzufangen weiß.

In dem Test solltest du keinen gemockten Tag, sondern eher einen mit Namen Test verwenden.
 
Grundsätzlich gilt - sowenig Mocken wie möglich. Am besten nur externe Resourcen, die man im Test nicht oder nur mit viel Aufwand aufgesetzt bekommt (z.B. externe Webservices).
 

Zurück
Oben