Odpowiadając na pytania, które pojawiły się w komentarzach:
Czemu nie da się tego przetestować?
Złe podejście, nie da się podmienić bazy danych dynamicznie podczas wykonywania programu na czas testowania:
public class CarRentalOptions {
private DataBase dataBase = new DataBase();
CarRentalOptions() throws SQLException {
}
}
Dlatego ktoś wymyślił dependency injection (wstrzykiwanie zależności), np poprzez konstruktor:
public class CarRentalOptions {
private DataBase dataBase;
CarRentalOptions(DataBase dataBase) {
this.dataBase = dataBase;
}
}
W tym momencie zyskaliśmy tylko częśc tego co chcemy osiągnąć - możemy przesyłać klasie CarRentalOptions skonfigurowane obiekty klasy DataBase. Nie jest źle, ale na potrzeby testowania potrzebujemy wstrzyknąć tam całkiem inną klase niż DataBase. Dlatego żeby to zadziałało potrzeba też zmienić typ składowej dataBase tak żeby mogły się tam znaleźć różne implementacje baz danych. Ale musimy się upewnić że wstrzykiwane implementacje będa miały odpowiednie metody na których będziemy w tej klasie polegać np dataBase.add(...), dlatego dobrym podejściem jest stworzenie interfejsu:
interface CarRentalStorage{
void addCustomer(Customer customer);
List<Customer> getAllCustomers();
void deleteCustomer(Customer customer);
}
class CarRentalSQLDatabase implements CarRentalStorage{
@Override
public void addCustomer(Customer customer) {
//database add logic
}
@Override
public List<Customer> getAllCustomers() {
//database read logic
}
@Override
public void deleteCustomer(Customer customer) {
//database delete logic
}
}
class CarRentalFileStorage implements CarRentalStorage{
@Override
public void addCustomer(Customer customer) {
//file add logic
}
@Override
public List<Customer> getAllCustomers() {
//file read logic
}
@Override
public void deleteCustomer(Customer customer) {
//delete from file logic
}
}
class CarRentalOptions {
private CarRentalStorage storage;
CarRentalOptions(CarRentalStorage storage) {
this.storage = storage;
}
}
Interfejs powininen być maksymalnie generyczny, tak żeby nieświadomie nie uzależnić się od konkretnej implementacji np metoda connect w takim interfejsie ograniczyłaby nas do różnego typu baz danych. Z generycznością moglibyśmy też przesadzić tworzać interfejs DataStorage, który operowałby na Objectach. Trzeba znaleźć złoty środek w maksymalizowaniu spójności i minimalizowaniu zależności (low coupling and high cohesion, interface segretation principle (SOLID)).
Ale wróćmy do tematu. Co nam to wszystko dało. Po pierwsze teraz Twoja klasa CarRentalOptions jest bardziej elastyczna, t.j. potrafi współpracować z różnymi wersjami przechowywania danych. Obchodzi ją tylko, żeby obiekt storage implementował odpowiednie metody, z których będzie korzystać. Po drugie możemy wreszcie napisać prosty test:
class CarRentalOptions {
private CarRentalStorage storage;
CarRentalOptions(CarRentalStorage storage) {
this.storage = storage;
}
public void addCustomer(Customer customer){
//some logic, prive methods calls...
storage.addCustomer(customer);
}
public boolean isCustomerRegistered(Customer customer){
return storage.getAllCustomers().contains(customer);
}
}
@RunWith(MockitoJUnitRunner.class)
class CarRentalOptionsTest{
CarRentalOptions objUnderTests;
@Test
public void addedCustomerShouldBeSaved(){
//tworzymy "mocka" dynamicznie programowalny obiekt
CarRentalStorage storageMock = mock(CarRentalStorage.class);
//wstrzykujemy mocka
objUnderTests = new CarRentalOptions(storageMock);
Customer customer = new Customer();
objUnderTests.addCustomer(customer);
//upewniamy sie ze obiekt zostal dodany do przechowalni (storage)
//ze zostal wywolany storage.addCustomer(customer)
verify(storageMock).addCustomer(customer);
//ustawiamy zachowanie mocka, obiekt dodany wiec getAllCustomers
//powinien zwrocic liste z dodanym customerem
List<Customer> customers = new ArrayList<>();
customers.add(customer);
when(storageMock.getAllCustomers()).thenReturn(customers);
//faktyczny test, czy dodany customer zostal dodany do storage
//nie obchodzi nas co robi z nim storage, testujemy jedynie obecna klase
//dlatego testujemy tylko czy zostala wywolana odpowiednia metoda na
//skladowej storage
Assert.assertTrue(objUnderTests.isCustomerRegistered(customer));
}
}
Mocki można tworzyć i wskrzykiwać automatycznie (adnotacjami @Mock @InjectMocks), ale dla prostoty przykładu zrobiłem wszystko ręcznie.
To może na koniec powiem czemu mocki są takie popularne.
Wyobraź sobie, że mamy duży projekt. Testów jest tysiące. Puszczane są bardzo często (CI). Powiedzmy, że nie mockowaliśmy bazy danych tylko wszędzie korzystaliśmy z faktyczniej bazy danych. Setki klas w projekcie korzysta z tej jednej klasy obslugujacej baze danych. Nagle ktoś nieświadomie psuje klase obsługująca baze danych i wszystkie testy przestają przechodzić. Nikt nie wie co się stało. Panika. Do biznesmenów lecą raporty, że wszystkie testy są na czerwono i nie wytłumaczysz im, że ktoś nie zamockował, albo, że to tylko jedna klasa nawaliła a wszystko działa. Zamykają projekt. Zwalniają zespół. Troche lipa :D