Mockito a przysłonięte pole klasy

Rookie mistake, powiedziałby niejeden zaprawiony programista Javy. Dla mnie to jednak było niemałe zaskoczenie: co ciekawego (czy może raczej nieciekawego) może wydarzyć się podczas pracy z Mockito, gdy spróbujemy wstrzyknąć zależność do klasy potomnej, przysłaniającej właściwość z klasy nadrzędnej?

Niedawno zetknąłem się z nieco frustrującą sytuacją. Refaktorowałem pewien serwis. Jego testy jednostkowe nie korzystały z adnotacji Mockito, a wszystkie mocki wstrzykiwane były za pomocą setterów. Samo w sobie, takie zjawisko nie rusza mnie za bardzo. Problem był jednak taki, że settery były napisane wyłącznie na potrzeby testów jednostkowych; nie były wykorzystywane w kodzie produkcyjnym.

Postanowiłem usunąć settery i oprzeć się na adnotacjach @Mock i @InjectMocks, oferowanych przez Mockito. Dość szybko okazało się, że kilka testów zalicza NPE (NullPointerException). Debugger szybko pokazał, że jedna z zależności w testowanej klasie nie była dostępna (miała wartość null). Użycie dowolnej metody na takim obiekcie oczywiście powodowało NPE.

Ciekawe natomiast było to, że mock był stworzony poprawnie i w teście normalnie na nim mogłem operować. Dlaczego zatem mock nie przechodził przez @InjectMocks i nie trafiał do testowanej klasy?

Po nitce do kłębka, najpierw sprawdziłem, jak Mockito wstrzykuje zależności. Pierwsze, czego próbuje, to konstruktor. Jeśli konstruktor nie przyjmuje danej zależności jako argumentu, Mockito sprawdza, czy istnieje dla niej setter. Jeśli i takowego nie znajdzie, na samym końcu próbuje użyć API refleksji i wstrzyknąć zależność w ten sposób.

To dało mi do myślenia: dopisałem do testowanej klasy usunięty uprzednio setter. IDE umożliwia stworzenie go automatycznie, więc skorzystałem z tej możliwości. Moim oczom ukazał się jednak setter z adnotacją @Override. Bingo!

Okazało się, że klasa rozszerzała inną. W klasie bazowej już istniało pole zależności, a także setter do niego. Mockito napotkał setter i wstrzyknął zależność do klasy bazowej, ale klasa potomna przysłoniła wstrzyknięte pole innym, o tej samej nazwie. To pole nie otrzymało żadnej wartości, powodując dalsze komplikacje.

Innymi słowy: ustawienie wartości pola klasy bazowej nie gwarantuje dostępności tej wartości w klasie potomnej, o ile ta ustawiane pole przysłania. Użycie settera z klasy bazowej ustawia wartość pola tejże samej klasy bazowej, ale nie ma wpływu na przysłonięte pole z klasy potomnej.

Rozwiązaniem tej sytuacji jest albo usunięcie settera z klasy bazowej, albo nadpisanie go w klasie potomnej; w obu przypadkach Mockito idealnie sobie poradzi. Można też w ogóle usunąć przysłoniętą właściwość z klasy potomnej.

Poniżej przedstawiam kod, który odtwarza wspomniane zachowanie:

Greeter.java, czyli wstrzykiwana zależność:

@Service
public class Greeter {
    public String greet(String name) {
        return "Hello, " + name;
    }
}

BaseService.java — to klasa bazowa. Mamy tu pole greeter i setter do niego:

public abstract class BaseService {
    protected Greeter greeter;

    public void setGreeter(Greeter greeter) {
        this.greeter = greeter;
    }
}

ChildService.java, czyli klasa potomna, przysłaniająca pole greeter:

@Service
public class ChildService extends BaseService {
    @Autowired
    private Greeter greeter;

    public String getGreeting(String name) {
        return "Greeting: " + greeter.greet(name);
    }
}

ChildServiceTest.java — test odtwarzający problem:

@RunWith(MockitoJUnitRunner.class)
public class ChildServiceTest {
    @Mock
    private Greeter greeter;

    @InjectMocks
    private ChildService service;

    @Test
    public void testGreeter() {
        // given
        given(greeter.greet(any())).willReturn("Foo");

        // when
        String result = service.getGreeting("Bar");

        // then
        assertThat(result).isEqualTo("Greeting: Foo");
    }
}

Przyznaję, że problem, z którym się zetknąłem, wynika raczej z mojego relatywnie niewielkiego doświadczenia z narzędziami, z którymi przyszło mi pracować. Człowiek jednak uczy się na błędach, częściej własnych niż cudzych. Straciłem na rozwiązanie tego problemu pół godziny, ale nauka płynąca z niego chyba była tego warta.


Napisz komentarz


Szukaj wpisów


Chmura tagów