Mockito, a dependency injection

0

Witam,
Mam problem w jaki sposób mockować/wskazać mockowi, że ma wykonać prawdziwą metodę, która jest dostarczana za pomocą dependency injection (np. za pomocą adnotacji @ejb lub @Inject).

Oczywiście EJB nie jest tutaj istotną kwestią (transakcje nie mają znaczenia: liczy się mockowanie, gdzie występuje dependency injection). Celem jest wytestowanie logiki biznesowej, nie transakcji itp.

@Stateless
public class TaskService implements TaskServiceLocal {
    
    @EJB
    private TaskFacadeLocal taskDao;

    @Override
    public boolean taskExists(long id) {
        Task idTask = taskDao.find(id);
        if (idTask == null) {
            return false;
        } else {
            return true;
        }
    }

}
@Stateless
public class TaskFacade extends AbstractFacade<Task> implements TaskFacadeLocal {
    
    @PersistenceContext(unitName = "taskList-PU")
    private EntityManager em;

    @Override
    protected EntityManager getEntityManager() {
        return em;
    }

    public TaskFacade() {
        super(Task.class);
    }

}
public abstract class AbstractFacade<T> {
    private Class<T> entityClass;

    public AbstractFacade(Class<T> entityClass) {
        this.entityClass = entityClass;
    }

    protected abstract EntityManager getEntityManager();

    public T find(Object id) {
        return getEntityManager().find(entityClass, id);
    }

}

Problem: w jaki sposób dostarczyć mock TaskFacadeLocal do środka mocka TaskService?

W teście brakuje mocka na TaskFacadeLocal

public class TaskServiceTest {

    private static final Long RET_TASK_ID = 1L;

    @Test
    public void testTaskExists() throws Exception {
        TaskService taskSrvUnderTest = mock(TaskService.class);
        TaskFacade taskFacadeUnderTest = mock(TaskFacade.class);
        Task a = new Task();
        a.setTaskId(RET_TASK_ID);
        when(taskSrvUnderTest.taskExists(RET_TASK_ID)).thenCallRealMethod();
        when(taskFacadeUnderTest.find(a)).thenReturn(a);
        assertTrue(taskSrvUnderTest.taskExists(RET_TASK_ID));
        verify(taskSrvUnderTest).taskExists(RET_TASK_ID);
    }

}

0

Źle to robię i mało rozumiem. Każda wskazówka będzie mile widziana, bo mam w głowie mętlik.

@RunWith(MockitoJUnitRunner.class)
public class TaskServiceTest {

    private static final Long RET_TASK_ID = 1L;
    
    @InjectMocks
    private TaskService taskSrvUnderTest = new TaskService();
    @Mock
    private TaskFacadeLocal taskFacade;
    
    @Test
    public void testTaskExists() throws Exception {
        Task a = new Task();
        a.setTaskId(RET_TASK_ID);
        when(taskFacade.find(a)).thenReturn(a);
        assertTrue(taskSrvUnderTest.taskExists(RET_TASK_ID));
        verify(taskSrvUnderTest).taskExists(RET_TASK_ID);
    }
}

Test wykłada się na assercie tzn. owszem jakiś (raczej nie mój) mock został wstrzyknięty (bo nie ma NullPointerException), jednak zwraca domyślną wartość. Dla @InjectMocks podaje prawdziwy obiekt (nie rozumiem dlaczego), a nie @Mock (na którym mógłbym wykonać callRealMethod).

Zmodyfikowałem też klasę TaskService (w praktyce nie chcę modyfikować klas biznesowych, aby móc testować, ale najpierw chcę zrobić tak, aby zadziałało):

@Stateless
public class TaskService implements TaskServiceLocal {

    @EJB
    private TaskFacadeLocal taskDao;

    @Override
    public boolean taskExists(long id) {
        Task idTask = taskDao.find(id);
        if (idTask == null) {
            return false;
        } else {
            return true;
        }
    }

    /*
     * setters
     */
    public void setTaskDao(TaskFacadeLocal taskDao) {
        this.taskDao = taskDao;
    }
    
}

Pisząc: 'Dla @InjectMocks podaje prawdziwy obiekt (nie rozumiem dlaczego)' miałem na myśli:

  • tworze prawdziwy obiekt (new, konstruktor) a nie mocka podając mu callRealMethod

Wciąż walczę. Co udało mi się ustalić:

@RunWith(MockitoJUnitRunner.class)
public class TaskServiceTest {

    private static final Long RET_TASK_ID = 1L;

    @InjectMocks
    private TaskService taskSrvUnderTest;
    @Mock
    private TaskFacadeLocal taskDao;

    @Test
    public void testTaskExists() throws Exception {
        Task a = new Task();
        a.setTaskId(RET_TASK_ID);
        when(taskDao.find(a)).thenReturn(a);
        assertTrue(taskSrvUnderTest.taskExists(RET_TASK_ID));
        verify(taskSrvUnderTest).taskExists(RET_TASK_ID);
    }
}

  1. Na debugerze ustaliłem, że taskDao wstrzykuje się do taskSrvUnderTest.
  2. Dodatkowy setter nie jest do tego celu potrzebny.
  3. Podczas egzekucji metody testu, wstrzyknięty, nienullowy mock zwraca mi null (wartość domyślną) zamiast obiektu a:
assertTrue(taskSrvUnderTest.taskExists(RET_TASK_ID));

Wykonywana metoda (idTask dostaje null zamiast obiekt a):

    public boolean taskExists(long id) {
        Task idTask = taskDao.find(id);
        if (idTask == null) {
            return false;
        } else {
            return true;
        }
    }
0

Problem w tym, że Twoja metoda find w AbstractFacade przyjmuje argument typu Object, w teście spodziewasz się, że zostanie ona wywołana z argumentem typu Task (when(taskDao.find(a)).thenReturn(a);) kiedy tak na prawdę wywołujesz ją z Longiem (taskDao.find(id);).

Powinno zadziałać, jeżeli zamockujesz to w ten sposób:
when(taskDao.find(RET_TASK_ID)).thenReturn(a);

P.S.
Ten verify tam jest zupełnie bez sensu.

0
airborn napisał(a):

Ten verify tam jest zupełnie bez sensu.

Moim zdaniem caly test jest zupelnie bez sensu. W pierwszym poscie mockuje i TaskFacade i TaskService, i chce testowac TaskService.
Ogolnie to juz ktorys temat w ostatnim czasie gdzie ludzie robia takie rzeczy. Nie czaje, rozumiem ze moze testowanie jest na czasie i mockowanie rowniez, ale powinno sie przede wszystkim zrozumiec co sie chce testowac. W tym przypadku testowany jest kod biblioteki do mockowania. Wszystko jest zle. Costam swita ze chce wolac prawdziwa metode (callRealMethod) ale nie czai ze to zupelnie niepotrzebne itp.

Jesli chcesz testowac TaskService (tak wynika z nazwy testu), to znaczy ze chcesz testowac twoj wlasny kod. Aby moc latwo testowac TaskService, musisz w jakis sposob tak podstawic cos do pola TaskFacadeLocal, zeby robilo to co ty chcesz. Mozesz napisac wlasne klasy ktore sa podklasami TaskFacadeLocal i podstawic w to pole, albo wlasnie stworzyc mocka i nawtykac mu funkcjonalnosci za pomoca when itp. Nastepnie, po tescie, w fazie weryfikacji, sprawdzasz czy winiki dostajesz takie jak chcesz oraz mozesz weryfikowac czy metody mocka zostaly tak wywolane, tyle razy, i z takimi parametrami jak oczekiwales. Nie zawsze jest to potrzebne. Nigdy nie powinienes mockowac klasy ktora chcesz testowac, tylko jej zaleznosci. W twoim przypadku bedzie ciezko cos podstawic do taskDao w TaskService bo uzywasz field injection. Zmien na setter injection albo constructor injection, i w tescie wywolaj ta metode / konstruktor. Cos takiego:

@Stateless
public class TaskService implements TaskServiceLocal {
    private TaskFacadeLocal taskDao;

    @Inject
    public TaskService(TaskFacadeLocal taskDao) {
        this.taskDao = taskDao;
    }

    @Override
    public boolean taskExists(long id) {
        return taskDao.find(id) != null;
    }
}

public class TaskServiceTest {
 
    @Test
    public void testTaskExists() {
        long taskId = 1L;

        // konfiguracja mockow
        TaskFacade taskFacade = mock(TaskFacadeLocal.class);
        Task a = new Task();
        // mockujesz szukanie Task po id
        when(taskFacade.find(taskId)).thenReturn(a);

        // tworzenie prawdziwego SUT (system under test) z mockiem
        TaskService testServiceUnderTest = new TasService(taskFacade);

        // wolanie kodu ktory chcemy testowac
        bool exists = testServiceUnderTest.taskExists(taskId);

        // weryfikacja
        assertTrue(exists);
        // moim zdaniem to jest niepotrzebne, bo oczywiste jest ze skoro test dziala to ta metoda byla wolana
        verify(taskFacade).find(taskId);
    }

    @Test
    public void testTaskDoesNotExist() {
        long taskId = 1L;

        // konfiguracja mockow
        // mocka nie trzeba konfigurowac poniewaz domyslnie metody zwracaja nulle (tak mi sie wydaje)
        TaskFacade taskFacade = mock(TaskFacadeLocal.class);

        // tworzenie prawdziwego SUT (system under test) z mockiem
        TaskService testServiceUnderTest = new TasService(taskFacade);

        // wolanie kodu ktory chcemy testowac
        bool exists = testServiceUnderTest.taskExists(taskId);

        // weryfikacja
        assertFalse(exists);
        // moim zdaniem to jest niepotrzebne, bo oczywiste jest ze skoro test dziala to ta metoda byla wolana
        verify(taskFacade).find(taskId);
    }
}

@Shalom: @InjectMocks sluzy do wstrzykiwania mockow do testu - do pol ktore maja adnotacje @mock. Nie ma to nic do rzeczy z tym co aurot chce zrobic. Jest to smutne co piszesz. Znaczy ze albo nie masz pojecia o testach i mockowaniu, albo ze nie przeczytales o co autor pyta i masz to w dupie i piszesz jakies bzdury bez zrozumienia tamatu (zakladam ze jest to bardziej pradwopodobne niz to ze nie umiesz testowac). Juz lepiej nie pisz nic jak masz dawac takie rady.

Autorowi polecam przeczytac http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html i jesli nadal nie rozumie, to jakas ksiazke o testowaniu.

0

@mućka: Bardzo doceniam przykład, który przedstawiłeś. I konstruktywną krytykę. Dzięki. Super przykład z tym wstrzyknięciem przez konstruktor (wszystko mi się rozjaśniło).

@Shalom:
Czy ten kod był taki dziwny? Ledwie jeden ifek, myślałem że jest prosty.

0

Mysle ze chodzilo o to ze te testy byly bardzo dziwne. Sam kod jest szczegolnie zly ;d

0

@mućka:
A tak w ogóle ten pomysł z używaniem callRealMethod (a więc i mocka). zamiast prawdziwego obiektu (i dostarczenia do niego zmockowanych zależności wynikał z jednej rzeczy):

  1. Często w moich klasach metody pobierające z bazy danych i logika biznesowa (która operuje na danych z bazy) przeplatały się. Dzięki temu udawało mi się je wytestować (na partial mocku). Rozumiem, że to krzywy sposób projektowania klas, bo robią się do tego dziwne testy.
  2. Widzę jedno rozwiązanie tego problemu: tworzyć dwa oddzielne service na komunikacje z bazą (DAO) i logikę biznesową (tu po prostu wstrzykuje DAO). Wtedy problem partial-mocków po prostu znika (callRealMethod nie będzie potrzebne, bo będę działać na prawdziwym obiekcie, a baza (zależność) będzie zmockowana).
0

@student911 przypadkiem trafiłem na to:
Mockito prosty test z instrukcją warunkową
co mnie smuci bo oznacza że niczego się nie nauczyłeś przez te kilka miesięcy...
Przeczytaj ten temat:
Kłopot podczas testów przy pomocy bibloteki Mockito.

0

@Shalom: bez urazy, ale imo czepiasz się (jednocześnie doceniam Twoje rady, jeśli nawet są kąśliwe). Zauważ proszę, że tam był zupełnie inny case: celowo przekształciłem ten sam sztuczny przykład, aby zbliżyć się do kodów, które widuje produkcyjnie. Może popełniłem błąd, że nie podlinkowałem tematu. Uznałem, ze to inny case, bo tutaj mam dependency-injection, a tam nie miałem i uznałem, że to drastycznie zmienia przetoczony wcześnie przykład.

Uznałem, że warto zapytać na forum w nowym temacie (bo to inny case z IoC) z jednego powodu: na temat testów ludzie w sieci często piszą bzdury i czasem trudno dojść do wniosku jak właściwie powinno się to robić, a tak przynajmniej ktoś kto się zna zrobi mi kod review. IMO niewiele osób potrafi testować jednostkowo.

Celowo nawiązałem do tego przykładu (ponieważ wracam do nauki Mockito, a poprzednio temat został zarzucony na callRealMethod i partial mocku, który wydawał się ok). Żyłem w błogiej nieświadomości, że partial-mocki są ok.

Kilka razy zastosowałem partial-mocki (tak jak wtedy), ponieważ klasy na to pozwalały i mogłem coś w ten sposób wytestować (z callRealMethod). Z tego co patrzę na ten przykład krzywo używałem verify. Wyciągne wniosek.

Nie przejmuje się, że mogłem nauczyć się niewiele przez ten czas:

  1. Bo robiłem wiele innych rzeczy (priorytety).
  2. Dla mnie porażka to po prostu część drogi do sukcesu.
0
Shalom napisał(a):

@student911 przypadkiem trafiłem na to:
http://4programmers.net/Forum/Java/238241-mockito_prosty_test_z_instrukcja_warunkowa

Heh, to byl wlasnie temat o ktorym pisalem ze ludzie biora sie za mocki nie rozumiejac podstaw testowania. Przyklad niemal identyczny, tyle ze tutaj costam probowano z wstrzykiwaniem (czego w tych testach wcale nie trzeba). Mam nadzieje jednak ze teraz wyciagnie z tego korzysci.

1 użytkowników online, w tym zalogowanych: 0, gości: 1