Ja to bym zrobił tak:
- Wywalam mocki w niebyt (najważniejszy krok).
- 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));
}
}
- 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());
}
}
- We wszystkich testach, gdzie Dao jest jedną z zależności testowanej klasy (a nie testowaną wprost klasą, jak w DaoSpec) podstawiam VolatileDao.
- Do VolatileDao mogę sobie dorzucić metody pomocnicze dzięki którym mogę sobie wygodnie i zwięźle ustawić stan początkowy.
- 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).