Testowanie metody zwracającej void

0

Witam,

Mam problem z testowaniem metod, które zwracają void. Nie bardzo rozumiem jak takie coś przetestować. Przecież metoda nic nie zwraca wiec nawet asercji nie mogę użyć. Na przykład chce przetestować taką metode zapisu do bazy danych.

public void addSomething(Something s) {
        somethingRepository.save(s);
    }

Mój test dla takiej metody wygląda następująco:


@Mock
private SomethingRepository somethingRepository;

private Jpa jpa;

@BeforeEach
public void setUp(){
      jpa = new Jpa(somethingRepository);
}

@Test
    void shouldDoNothingWhenSomethingRepoCallSave(){
        // given
        final Something s = Something.builder().name("soooom").build();
        given(somethingRepository.save(s)).willReturn(s);

        // when
        jpa.addSomething(s);

        // then
        then(somethingRepository).should(times(1)).save(s);
    }

Czy ktoś mógł by mi powiedzieć co robie źle, i jak to przetestować dobrze ? Nie bardzo mam pomysł jak tutaj wcisnąć asercje. Używam Junit5 oraz Mockito 2. Z góry dzięki za pomoc.

0

Czy ktoś mógł by mi powiedzieć co robie źle, i jak to przetestować dobrze ? Nie bardzo mam pomysł jak tutaj wcisnąć asercje. Używam Junit5 oraz Mockito 2. Z góry dzięki za pomoc.

Mockito verify

5

Takie rzeczy testuje sie integracyjnie, choćby na jakiejś bazie in-memory. To co zrobiłeś ma bardzo niewielki sens, bo jedyne co sprawdziłeś to czy biblioteka do mockowania działa i czy ktoś nie usunął ci tej jednej linijki kodu.

1

Powinieneś najpierw się zastanowić co Ty tak na prawdę chcesz testować. Zapisywanie (to czy się zapisuje i gdzie się zapisuje) czy logikę (czy zapisuje się wartość która jest wyliczana poprawnie, np poprawna data zamiast 1988-01-12).

Jeśli to pierwsze to w teście sobie zapisz, a w asercji odczytaj tę wartość.

Jeśli do drugie, to powinieneś jakoś rozdzielić metodę od zapisywania od metody do wyliczania, i testować tę drugą która już ma sensowny return type.

Ps: Jeśli ta metoda na prawdę wygląda tak

public void addSomething(Something s) {
      somethingRepository.save(s);
}

...to tu nie ma co testować. Tu nie ma żadnej logiki, funkcja nie ma żadnej odpowiedzialności, tylko delegację do innej funkcji, i jest 0 powodów dla których mogłaby się zmienić. Pokrywanie tej metody nie ma sensu.

0

Void = czyli nic nie robi. Wywalamy metode. Nie trzeba testować. Profit.

1
	@Test
	public void create() {
		Attribute attribute = Attribute.builder().name(EXAMPLE_NAME).type(EXAMPLE_TYPE).build();

		doAnswer((Answer<?>) invocation -> {
			Attribute answer = (Attribute) invocation.getArguments()[0];

			assertEquals("Pole [AttributeId] powinno być ustawione na 0", 0, answer.getAttributeId());
			assertEquals("Pole [Active] powinno być ustawione na 1", 1, answer.getActive());
			//etc.

			return null;
		}).when(attributeRepo).save(any(Attribute.class));

		attributeService.create(attribute);
	}

Zwróć uwagę, że w takim przypadku When jest zamienione z Then. Trochę na innym przykładzie, twój generalnie nie miałby sensu.

Polecam poczytać: http://www.baeldung.com/mockito-void-methods

1

Testujesz normalnie, z dokumentacją. Czytasz, co metoda powinna robić, szykujesz jej dane wejściowe, wywołujesz, weryfikujesz skutki uboczne. Metoda typu add dodaje, więc można sprawdzić, czy rozmiar listy zwiększył się o 1.

2

Ja to bym zrobił tak:

  1. Wywalam mocki w niebyt (najważniejszy krok).
  2. W kodzie produkcyjnym zostawiam DAO operujące na rzeczywistej bazie danych, a do testów dorzucam DAO operujące na kolekcjach w pamięci, np:
// kod produkcyjny
class Dao {
  public void save(CośTam cośTam) {
    // implementacja
  }

  public Optional<CośTam> load(CośTamId cośTamId) {
    // implementacja
  }
}
// kod testowy
class VolatileDao extends Dao {
  private Map<CośTamId, CośTam> różneCosie = new HashMap<>();
  
  @Override
  public void save(CośTam cośTam) {
    różneCosie.put(cośTam.id, cośTam);
  }

  @Override
  public Optional<CośTam> load(CośTamId cośTamId) {
    return Optional.ofNullable(różneCosie.get(cośTamId));
  }
}
  1. Piszę zestaw testów do tych klas. Testy mają testować funkcjonalności, czyli w tym przypadku konkretne efekty uboczne (zmiana działania metody load po wywołaniu metody save). Dla przykładu po save(obiekt) ma mi zadziałać metoda load(obiekt.id) i zwrócić obiekt o takiej samej zawartości. Dokładnie tymi samymi testami mogę przetestować zarówno implementację produkcyjną jak i testową, bo mają ten sam interfejs - to też jest bardzo ważna właściwość tego podejścia. Dla przykładu:
abstract class DaoSpec {
  protected abstract Dao makeDao();
  void testSaving() {
    Dao dao = makeDao();
    CośTam cośTam = testoweCośTam();
    assertTrue(dao.load(cośTam.id).isEmpty);
    dao.save(cośTam);
    assertEquals(dao.load(cośTam.id), Optional.of(cośTam));
  }

  // tutaj kolejne testy
}

class ProductionDaoSpec extends DaoSpec {
  @Override
  protected Dao makeDao() = {
    return new Dao(parametry, do, rzeczywistej, bazki, w, pamięci));
  }
}

class VolatileDaoSpec extends DaoSpec {
  @Override
  protected Dao makeDao() = {
    return new VolatileDao());
  }
}
  1. We wszystkich testach, gdzie Dao jest jedną z zależności testowanej klasy (a nie testowaną wprost klasą, jak w DaoSpec) podstawiam VolatileDao.
  2. Do VolatileDao mogę sobie dorzucić metody pomocnicze dzięki którym mogę sobie wygodnie i zwięźle ustawić stan początkowy.
  3. Warto zauważyć, że powyższy sposób skaluje się dobrze dla wielu wariantów Dao. Najpierw wydzielamy interfejs:
interface Dao {
  void save(CośTam cośTam);
  Optional<CośTam> load(CośTamId cośTamId);
}

Następnie możemy mieć wiele implementacji produkcyjnych, np OracleDao, MsSqlDao, HsqldbDao, itd oraz testowych czyli JavaCollectionsDao, H2InMemoryDao, itd i wszystkie je testować tymi samymi testami, bo przecież testom samych Dao powinien wystarczać interfejs Dao (mojemu hipotetycznemu testowi powyżej wystarcza - metoda testSaving przetestuje pod kątem poprawności każdą implementację Dao).

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