• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Czy powinienem to testować - testy jednostkowe JUnit

VPS Starter Arubacloud
0 głosów
972 wizyt
pytanie zadane 2 sierpnia 2018 w Java przez must Bywalec (2,980 p.)

Cześć. Postanowiłem się pouczyć trochę o testach jednostkowych i oczywiście kilka z nich napisać, ale tutaj pojawia się pytanie. Jakiego typu metody powinienem testować?

Na przykładzie, mam taką klasę:

package CarRental;

import java.sql.SQLException;
import java.util.InputMismatchException;
import java.util.Scanner;

import CarRental.DataGetter.ClientDataGetter;
import CarRental.DataGetter.WorkerDataGetter;


public class CarRentalEngine {

    private int option;
    private Scanner input = new Scanner(System.in);
    private CarRentalOptions carRentalOptions = new CarRentalOptions();
    private ClientDataGetter clientDataGetter = new ClientDataGetter();
    private WorkerDataGetter workerDataGetter = new WorkerDataGetter();

    CarRentalEngine() throws SQLException {
    }

    void startCarRental() throws SQLException {
        System.out.println("Who are you?\n1. Customer\n2. Worker");
        try {
            switch (input.nextInt()) {
                case 1:
                    executeClientCase();
                    break;
                case 2:
                    executeWorkerCase();
                    break;
            }
        } catch (InputMismatchException e) {
            System.err.println("Your input is wrong!");
        }
    }


    void executeOptionsForClient(int option) throws SQLException {
        switch (option) {
            case 1:
                carRentalOptions.rentACar(clientDataGetter.rentACar(input));
                break;
            case 2:
                carRentalOptions.returnACar(clientDataGetter.returnACar(input));
                break;
            case 3:
                carRentalOptions.populateTableRent(clientDataGetter.populateTableRent(input));
                break;
            case 4:
                carRentalOptions.populateTableViewCars(clientDataGetter.populateTableViewCars(input));
                break;
            case 5:
                break;
        }
    }


    void executeOptionsForWorker(int option) throws SQLException {
        switch (option) {
            case 1:
                carRentalOptions.populateTableViewClients(input);
                break;
            case 2:
                carRentalOptions.populateTableViewCars(clientDataGetter.populateTableViewCars(input));
                break;
            case 3:
                carRentalOptions.makeCarAvailable(workerDataGetter.makeCarAavailable(input));
                break;
            case 4:
                carRentalOptions.makeCarUnavailable(workerDataGetter.makeCarUnavailable(input));
                break;
            case 5:
                carRentalOptions.createNewCar(workerDataGetter.createCar(input));
            case 6:
                break;
        }
    }


    void executeClientCase() throws SQLException {
        System.out.println("1. Have you inputted your data before?\nN/Y: ");
        if (input.next().toUpperCase().equals("N")) {
            carRentalOptions.createNewCustomer(clientDataGetter.createClient(input));
            System.out.println("Now you have your unique number clinet, use it where it is required!");
        } else {
            do {
                System.out.println("What do you want to do?");
                System.out.println("1. Rent a car\n2. Return a car\n3. Populate rented cars\n4. Populate cars\n5. Quit");
                option = input.nextInt();
                executeOptionsForClient(option);
            }
            while (option != 5);
        }
    }

    void executeWorkerCase() throws SQLException {
        do {
            System.out.println("What do you want to do?");
            System.out.println("1. Populate clients\n2. Populate cars\n3. Make car available\n4. Make car unavailable\n5. Insert new car\n6. Quit");
            option = input.nextInt();
            executeOptionsForWorker(option);
        }
        while (option != 6);
    }
}

Jak przetestować metody: 

executeWorkerCase, executeClientCase, executeOptionsForWorker, startCarRental

Tak samą mam taką klasę:

package CarRental;

import CarRental.Model.Car;
import CarRental.Model.Client;
import CarRental.Model.RentingACar;
import DB.DataBase;

import java.sql.SQLException;
import java.util.Scanner;


public class CarRentalOptions {
    private DataBase dataBase = new DataBase();

    CarRentalOptions() throws SQLException {
    }

    void createNewCustomer(Client client) throws SQLException {
        dataBase.insertNewCustomer(client);

        System.out.println("Client added successfully!");
    }

    void createNewCar(Car car) throws SQLException {
        dataBase.insertNewCar(car);

        System.out.println("Car added successfully!");
    }

    void makeCarUnavailable(Car car) throws SQLException {
        dataBase.makeCarUnavailable(car);
    }

    void makeCarAvailable(Car car) throws SQLException {
        dataBase.makeCarAvailable(car);
    }

    void rentACar(RentingACar rentingACar) throws SQLException {
        dataBase.rentACar(rentingACar);
    }

    void populateTableViewCars(Car car) throws SQLException {
        dataBase.populateTableViewCars(car);
    }

    void populateTableRent(Client client) throws SQLException {
        dataBase.populateTableRent(client);
    }

    void populateTableViewClients(Scanner input) throws SQLException {
        dataBase.populateTableViewClients();
    }

    void returnACar(Car car) throws SQLException {
        dataBase.returnACar(car);
    }
}

Tutaj też nie wiem czy powinienem to testować.

 

5 odpowiedzi

+1 głos
odpowiedź 4 sierpnia 2018 przez RafalS VIP (122,820 p.)
wybrane 4 sierpnia 2018 przez must
 
Najlepsza

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

komentarz 4 sierpnia 2018 przez must Bywalec (2,980 p.)
hej, dzięki za odpowiedź! W wolnej chwili zerknę na pewno, ale jest to pierwsza konkretna odpowiedź, której oczekiwałem. Dzięki!.
komentarz 5 sierpnia 2018 przez RafalS VIP (122,820 p.)
Generalnie polecam też zobaczyć jakieś przykłady mockowania, bo ja jedynie zarysowałem o co z tym chodzi, ale Mockito ma wiele innych funkcjonalności. Przeważnie korzysta się z kilku najważniejszych, ale dobrze wiedzieć jakie są możliwości.

Polecam przykłady z oficjalnego tutoriala mockito

http://static.javadoc.io/org.mockito/mockito-core/2.20.0/org/mockito/Mockito.html
komentarz 11 sierpnia 2018 przez must Bywalec (2,980 p.)

Ogarnąłem temat mocków, mniej więcej ogarniam temat.

Mimo to podtrzymuje dalej pytania:

1) Jak dość do tego by zwracało mi listę klientów jako liste nie tekst

2) 

public boolean isCustomerRegistered(Customer customer){
        return storage.getAllCustomers().contains(customer);
    }

Ta klasa mi nie pasuje w CarRentalOptions, bo ma to się troche nijak do tego co aktualnei zbudowałem. Czy nie miało by to działać właśnie na zasadzie stuba? Czyli w osobnej całkowicie klasie?

3) Jak rozegrać ten pakunek DataGetter i przykładową metodę createClient


public Client createClient(Scanner input) {
        Client client = new Client();
        client.setClientNumber(rand.nextInt(999));
 
        System.out.print("name: ");
        client.setName(input.next());
        System.out.print("surname: ");
        client.setSurname(input.next());
        System.out.print("city: ");
        client.setCity(input.next());
        System.out.print("house number: ");
        client.setHouseNumber(input.nextInt());
        System.out.print("street: ");
        client.setStreet(input.next());
        System.out.print("pesel number: ");
        client.setPeselNumber(input.nextLong());
        System.out.print("rent date: ");
        client.setRentDate(input.next());
        System.out.println("Your client number is: " + client.getClientNumber());
 
        return client;
    }

 

komentarz 12 sierpnia 2018 przez must Bywalec (2,980 p.)

Chyba sobie odpowiedziałem na 1)

Zrobiłem coś takiego:

@Override
    public List<Client> getAllCustomers() throws SQLException {
        List<Client> listOfClients = new ArrayList<Client>();

        String sql = "SELECT * FROM `client`";
        result = statement.executeQuery(sql);

        while (result.next()) {
            Client client = new Client();
            client.setName(result.getString("namee"));
            client.setSurname(result.getString("surname"));
            client.setStreet(result.getString("street"));
            client.setPeselNumber(result.getLong("peselNumber"));
            client.setRentDate(result.getString("rentDate"));
            client.setCity(result.getString("city"));
            client.setHouseNumber(result.getInt("houseNumber"));
            client.setClientNumber(result.getInt("clientNumber"));

            listOfClients.add(client);
        }

        return listOfClients;
    }

I CarRentalOptions

void getAllCustomers() throws SQLException {
        for (int i = 0; i < storage.getAllCustomers().size(); i++) {
            System.out.println("Name: " + storage.getAllCustomers().get(i).getName()
                    + "\nSurname: " + storage.getAllCustomers().get(i).getSurname()
                    + "\nStreet: " + storage.getAllCustomers().get(i).getStreet()
                    + "\nHouse number: " + storage.getAllCustomers().get(i).getHouseNumber()
                    + "\nCity: " + storage.getAllCustomers().get(i).getCity()
                    + "\nPesel Number: " + storage.getAllCustomers().get(i).getPeselNumber()
                    + "\nRent Date: " + storage.getAllCustomers().get(i).getRentDate()
                    + "\nClient number: " + storage.getAllCustomers().get(i).getClientNumber());
            System.out.println("---------------------------");
        }
    }

Co myślisz?

komentarz 12 sierpnia 2018 przez must Bywalec (2,980 p.)

2)

W taki sposób test zrobiłem:

@Test
    void createNewCustomer() throws SQLException {
        CarRentalStorage carRentalStorageMock = mock(CarRentalStorage.class);

        CarRentalOptions carRentalOptions = new CarRentalOptions(carRentalStorageMock);
        Client client = new Client();

        carRentalOptions.createNewCustomer(client);

        List<Client> listOfClients = new ArrayList<Client>();
        listOfClients.add(client);

        verify(carRentalStorageMock).addClient(client);

        when(carRentalStorageMock.getAllCustomers()).thenReturn(listOfClients);

        assertEquals(1, listOfClients.size());
    }

na 3) Raczej już nie znajdę odpowiedzi :P

komentarz 16 sierpnia 2018 przez RafalS VIP (122,820 p.)
Nie za bardzo mam czas, żeby to analizować i nie będzie mnie na forum jakiś czas. Zadaj kolejne pytanie, na pewno ktoś pomoże :)
+2 głosów
odpowiedź 2 sierpnia 2018 przez marcin99b Szeryf (81,480 p.)
Kiedyś spotkałem się z zasadą że jeśli to nie jest punkt przez który przechodzi masa wartości biznesowej - przykładowo decyzja czy automatycznie zwrócić pieniądze za produkt, albo punkt gdzie przechodzi dużo danych

A ewentualna naprawa zajmie mniej niż 15 minut, bo ewentualnym błędem może być literówka albo szczegół (mamy prosty kod)
To nie warto tego testować, ponieważ nie opłaca się porównując straty do kosztów czasu programisty

Ale jeśli chodzi o własne aplikacje, po prostu rób to na wyczucie, co według ciebie nie jest pewne że zadziała -> testuj
Albo co zawsze musi działać w 100% -> testuj

Zamiast testów jednostkowych możesz też wspomagać się testami end to end, które co prawda nie pokażą ci dokładnie która metoda której klasy się wywala, przy jakich wartościach, z informacją dlaczego się wywala
Ale dzięki nim możesz sprawdzić czy pewien obszar -działa

Przykładowo - nie sprawdzisz która część kodu odpowiedzialna za rejestracje nie działa, ale jesteś w stanie sprawdzić czy ogólnie rejestracja działa lub nie

Pamiętaj tylko żeby pod testy end to end podpiąć inną baze niż tą "produkcyjną" (ConnectionString powinien być w jakimś zewnętrznym pliku, np json, a nie w skompilowanym kodzie, tak przynajmniej robimy w .net)

Jeszcze taka rada
Pamiętaj o wzorcu strategia i ogólnie o możliwości podmiany interfejsów (do tego dependency injection i te sprawy)
Pozwala na dużo prostsze testowanie jednostkowe, bo nie trzeba aż tak kombinować
Między innymi dlatego, że łatwo to mockować - prościej symulować zdarzenia na obiektach, w testach
1
komentarz 2 sierpnia 2018 przez Tomek Sochacki Ekspert (227,510 p.)

Albo co zawsze musi działać w 100% -> testuj 

A skoro jest w kodzie coś (funkcja, klasa itp.) co nie musi działać zawsze to po co w ogóle to tam jest...?

Dużo w podejściu do testowania zmienia przejście na system TDD...

komentarz 2 sierpnia 2018 przez marcin99b Szeryf (81,480 p.)
Zależy od interpretacji w sumie

Są elementy bardziej wartościowe i mniej
Na pewno więcej szkody wyrządzi uszkodzony kontener IoC, bez którego prawie cała aplikacja leży, niż problemy z aktualizacją miejscowości użytkownika, która jest informacją dla innych, a żadne operacje sie na tym nie wykonują
komentarz 3 sierpnia 2018 przez Secrus Nałogowiec (32,880 p.)
Przykład co nie musi działać w 100%:

Wysyłanie do użytkownika emaila powitalnego nie dającego mu żadnej informacji poza tym, że serwis się cieszy i dziękuje za rejestracje itp.
 

Co musi działać w 100%:

Przepływ informacji o nowym użytkowniku do bazy, szczególnie jeśli w aplikacji obracamy w jakiś sposób piniędzmi
komentarz 3 sierpnia 2018 przez Tomek Sochacki Ekspert (227,510 p.)

Przykład co nie musi działać w 100%:
Wysyłanie do użytkownika emaila powitalnego nie dającego mu żadnej informacji poza tym, że serwis się cieszy i dziękuje za rejestracje itp.

A to już zależy... jeśli Product Owner w Twoim projekcie uzna, że nie jest to faktycznie 100% obowiązkowa funkcjonalność to oki, ale raczej częściej takie właśnie rzeczy są dla "góry" ważniejsze niż niektóre tematy "pod spodem API"... 

No i to jest troszkę miejsce, gdzie czasami dochodzi do zgrzytu back-end - front... coś co dla back-endowca jest funkcjonalnością "nic nie robiącą" dla fronta i marketingu jest obowiązkową :)

Także zawsze trzeba szukać kompromisów...

komentarz 4 sierpnia 2018 przez marcin99b Szeryf (81,480 p.)
No właśnie, zawsze trzeba na to spojrzeć z poziomu "biznesu"

Jeśli koszty nie działania tego (np jedno z opcjonalnych pól tekstowych) są niższe niż koszty testowania tego i przewidywania przypadków dla których to może działać/nie działać
To finansowo nie opłaca się tego robić

A jeśli nie działanie czegoś sprawi, że 100% użytkowników nie będzie mogło używać 80% aplikacji, bo repozytorium ogłoszeń w aplikacji ogłoszeniowej przestanie działać
To znaczy, że koszty kazania programiście napisania testu są dużo niższe niż ewentualny błąd w kodzie

Sporo zależy też od skomplikowania kodu
Są sytuacje gdzie jakiś szczegół można zostawić bo na 99% działa a tworzenie nowego testu to dodatkowy czas pracy, w dodatku ten element jest z tych "mniej ważnych"
A są sytuacje gdzie jakiś element jest napisany kodem spaghetti i w sytuacji gdzie użytkownik zgłosi błąd, naprawa potencjalnie prostej funkcjonalności nie zajmie max 15 minut, tylko trochę (tu wstaw losową liczbę) więcej
+2 głosów
odpowiedź 2 sierpnia 2018 przez Ehlert Ekspert (212,630 p.)
Zacznijmy od tego że dla tego kodu nie powinieneś zaczynać od testów, ale od refactoru. Ta klasa robi wszystko: połączenie z bazą danych, scanner i ten zagadkowy switch. Polecam poczytać książki Flowera.
komentarz 2 sierpnia 2018 przez must Bywalec (2,980 p.)
Jest to w pewnym sensie silnik, czyli skupia wszystko inne.

Jaki zagadkowy switch?
komentarz 2 sierpnia 2018 przez Ehlert Ekspert (212,630 p.)
1, 2, 3. Skąd mam wiedzieć co to za wartości. Kompletnie nie wiadomo co reprezentują. Takie coś przyjemniej zamienia się na dobrze nazwane stałe.
komentarz 2 sierpnia 2018 przez must Bywalec (2,980 p.)
Reprezentują opcję, które są wyświetlane w terminalu podczas programu.

Nie bawiłem się tutaj w żadne MVC itp z tego względu, że nie ogarniam tematu i nie będę się tam pchać na siłe.

Tak jak już wspomniałem silnik skupia całą logikę, dlatego raczej na pewno nię będę refaktorować tego kodu.

Mimo to, potrzebuje pomocy z testami, a nie co reprezntuje 1,2,3..
komentarz 2 sierpnia 2018 przez Ehlert Ekspert (212,630 p.)
Testy jednostkowe wymagają trzymania się solidu. Mile widziane jest też typowanie interfejsami. Jeśli  nie trzymasz się tego i wg Ciebie ta implementacja jest ok, to musisz się liczyć z tym, że nikt Ci nie pomoże. Takiego kodu nie testuje się jednostkowo.
komentarz 2 sierpnia 2018 przez must Bywalec (2,980 p.)
Jest ok, bo nie mam kompletnie innego pomysłu jak można porozdzielać te metody na inne jeszcze klasy. Już na ten moment starałem się wszystko rozdzielać tematycznie do klas i myślałem, ze rezultat jest dobry.

Wrzuciłem na code review i nic mi o tym złego nie napisali.
komentarz 2 sierpnia 2018 przez RafalS VIP (122,820 p.)

Podbijam. Czesto duzym benefitem z pisania testow jest wymuszenie pisania testowalnego, czytaj dobrego kodu.

Ten tutaj trzeba zrefactorowac jeali chcemy napisac do niego testy jednostkowe (testujace pojedyncza funkcjonalność). 

Piszesz ze klasa musi skupiac duzo logiki bo jest silnikiem. Ale to ze musi skupiac nie oznacza ze musi calosc logiki implementować. Niech klasy implemenetuja interfejsy i beda wstrzykiwane w konstruktorze. Wtedy napisanie testow bedzie latwe i przyjemne.

komentarz 2 sierpnia 2018 przez must Bywalec (2,980 p.)

A nie wyodrębniłem właśnie oddzielnych klas?

private CarRentalOptions carRentalOptions = new CarRentalOptions();
private ClientDataGetter clientDataGetter = new ClientDataGetter();
private WorkerDataGetter workerDataGetter = new WorkerDataGetter();

Poza tym, wg mnie każda metoda odpowiada za pojedynczą funkcjonalność, więc jak to mogę jeszcze zmienić?

Co więcej  piszesz "Wtedy napisanie testow bedzie latwe i przyjemne.", ale to że stworzę nową klasę gdzie powrzucam jakieś tam metody nie sprawi, że te metody się zmienią, będą takie same, tylko że w innych klasach.

1
komentarz 2 sierpnia 2018 przez RafalS VIP (122,820 p.)

Ostatni komentarz był sugerowany poprzednią dyskusją. Przeoczenie, pisałem z autobusu :D

Problemem Twoich klas jest niemożliwość zamockowania składowych.

private CarRentalOptions carRentalOptions = new CarRentalOptions();

taka inicjalizacja w ciele sprawia, że silnik jest nierozerwalnie połączony z CarRentalOptions, przez co cieżko to przetestować.

Rozwiązaniem jest stworzenie interfejsu (cięzko mi o nazwe bo nie do końca ogarniam ten kod), ale bardzo dobry przykład jest tutaj:

private DataBase dataBase = new DataBase();

Twoja klasa jest nierozerwalnie połączona ze specyficzną bazą danych zaimplementowana w postaci klasy DataBase.

Gdybyś zrobił interfejs w stylu CarRentalDataStorage i klasa DataBase (tak na marginesie beznadziejna nazwa, bo mówi jedynie tyle ze to baza danych, mimo ze prawdopodobnie nie jest generyczna baza danych, tylko baza danych sql do przechowywania car rental data) to mógłbyś przetestować tę klasę odzielnie a w tescie CarRentalOptions skorzystał byś z mocka lub stuba dla bazy danych tak żeby przetestować jedynie funkcjonalności - stworzył byś "głupią" implementacje interfejsu CarRentalDataStorage (stuba), która by robiła rzeczy w stylu return true, return 10, tą implementacje wstrzyknąłbyś w konstruktorze i wtedy jesteś w stanie bardzo dobrze przetestować funkcjonalności klasy CarRentalOptions, gdyż nie polega ona na klasie DataBase.

PS zazwyczaj lepiej jest skorzystać z jakiejś biblioteki do mockowania zamiast pisać w całości tą "głupią" implementacje. 

Na pierwszy rzut oka uzależnianie klasy od Scanner(System.in) też nie jest najlepszym pomysłem. Mimo, że da się podmienić na moment strumień system.in:

https://stackoverflow.com/questions/1647907/junit-how-to-simulate-system-in-testing

 

2
komentarz 2 sierpnia 2018 przez RafalS VIP (122,820 p.)
Programowanie pod interfejsy pozoliłoby Ci też dobrze odzielić interfejs publiczny Twoich klas od metod prywatnych, co do których testowania zdania są podzielone - wg mnie nie powinno się ich testować.

Np executeClientCase() brzmi jak metoda która powinna być totalnie prywatna i test nie powinien być do niej tylko do metody publicznej która z niej korzysta.
1
komentarz 2 sierpnia 2018 przez RafalS VIP (122,820 p.)
Pisanie testów jedostkowych jest wg mnie bardzo powtarzalne, dlatego polecam znaleźć jakiś projekt na githubie, który ma napisane testy, popatrzeć na to i od razu Ci się rozjaśni o czym wszyscy mówimy z tymi mockami i dependency injection.
komentarz 2 sierpnia 2018 przez must Bywalec (2,980 p.)

I była prywatna, wszystkie był prywatne oprócz 

startCarRental

ale nie mogłem ich przetestować dlatego usunąłem private.

Właśnie oglądam jakieś tutoriale o tym Mockito. Ale czytając to co napisałes, to jest to dla mnie czarna magia i chyba porywam się z motyką na słońce :P

Dependency Injection, kolejny mój problem, oglądałem masę filmików, dalej nie potrafię tego zaimplementować w tej wypożyczalni.

1
komentarz 3 sierpnia 2018 przez RafalS VIP (122,820 p.)
Napisze Ci to / znajde dobre przyklady jak tylko na miał czas
komentarz 3 sierpnia 2018 przez must Bywalec (2,980 p.)
Dzięki.
1
komentarz 4 sierpnia 2018 przez RafalS VIP (122,820 p.)
Napisałem coś o dependency injection i mockach w odpowiedzi, rzuć okiem.
0 głosów
odpowiedź 2 sierpnia 2018 przez must Bywalec (2,980 p.)
Czyli co testować - wszystko. :D

A jeżeli chodzi o to, jak przetestować metody które podałem w pytaniu?
–1 głos
odpowiedź 2 sierpnia 2018 przez Mateusz51 Nałogowiec (28,180 p.)
Na początku możesz testować dosłownie wszystko. Idąc z czasem zdobędziesz doświadczenie mówiące Ci co jest warto testować a co nie.

Tak na szybko. Nie warto testować get i set oraz mały sens ma testowanie połączeń z systemami zewnętrznymi, kłóci się to trochę z założeniami testów jednostkowych.
komentarz 2 sierpnia 2018 przez must Bywalec (2,980 p.)
Czyli mam nie testować baz danych?

Czy w 2 klasach, które wrzuciłem powinienem przetestować wszystkie metody?

Jak mam metodę executeWorkerCase  w klasie CarRentalEngine, to co powinienem dokładnie przetestować i jak to zrobić, bo nawet nie wiem co mam wpisać w google.
komentarz 2 sierpnia 2018 przez Tomek Sochacki Ekspert (227,510 p.)

 Nie warto testować get i set 

No tu bym dyskutował... jeśli jest tego niewiele to jaki problem napisać 1-2 dodatkowe testy? A jeśli korzystamy w jakieś klasie nadmiernie z get/set to zastanowiłbym się nad tym, czy na pewno wszystko w klasie jest ok...

Moim zdaniem nie warto oszczędzać na testach, bo gdy wejdą kolejny programiści to wszystko może szybko runąć...

komentarz 2 sierpnia 2018 przez Mateusz51 Nałogowiec (28,180 p.)
A jaki jest sesn pisania testu który sprawdza czy ustawia się zmienna? Jest to strata czasu programisty i zaciemnianie kodu. Złe testy są gorsze od żadnych.

Get i set można przetestować globanie testując cały proces.
1
komentarz 2 sierpnia 2018 przez Tomek Sochacki Ekspert (227,510 p.)
a co, jeśli jakiś setter czy getter wykonuje dodatkowo jakąś logikę? Jak już upierać się przy nietestowaniu tego to obstawiałbym za brakiem jakiejkolwiek dodatkowej logiki w tych metodach...
komentarz 2 sierpnia 2018 przez Mateusz51 Nałogowiec (28,180 p.)
No zakładam że nie ma tam logiki
komentarz 2 sierpnia 2018 przez Tomek Sochacki Ekspert (227,510 p.)

No zakładam że

Gdy nad projektem pracuje kilka osób to oki... ale gdy w zespole są rotacje ludzi iprojekt ciągnie się przez długi czas to uważałbym z takimi założeniami... :)

Ale oczywiście w małych projektach, krótkich itp. można podjąć takie założenia, ale powinny one być poparte np. ogólymi zasadami pisania kodu danej apki i wyłapywane w code review, bo inaczej może być kiedyś problemik :)

Wszystko zalezy więc od przypadku, ale warto chociaż analizować takie tematy.

komentarz 2 sierpnia 2018 przez Mateusz51 Nałogowiec (28,180 p.)
Nie chce się kłócić. Ale jak ktoś umieszcza logike w setterach to raczej testy mu nie pomogą
1
komentarz 2 sierpnia 2018 przez Aisekai Nałogowiec (42,190 p.)
edycja 2 sierpnia 2018 przez Aisekai
Logika w setterach ... Czemu nie wolno umieszczac? Według mnie nie ma w tym nic zlego, a wręcz przeciwnie - mocno ułatwia pisanie kodu. Czemu?

-Pierwsza sytuacja gdy Ty piszesz kod i musisz w wielu miejscach ustawic jakieś pole jakas wartością. Duzo prościej i bezpieczniej jest gdy klasa sama sobą zarzadza.  

-Druga sytuacja, gdy ktoś pisze kod to może nie wiedzieć, ze musiała coś wykonac przed użyciem settera i ustawi np hasło na bardzo słabe.

-Wedlug mnie, takie podejście jest troche sprzeczne z OOP i pozbywajac się logiki z klas reprezentujących jakies dane, możesz doprowadzic do anemicznych klas.

Edit: Używanie setterow i getterow, jeżeli framework nie narzuca ich utworzenia, w których sie zakłada ze nie bedzie żadnej logiki mija sie z celem. Trzymanie logiki w setterach jest o tyle lepsze, że:

Załóżmy taką sytuację, że nie ma potrzeby walidowania jakiegoś pola, więc na chwilę obecną nie ma walidacji w całej aplikacji dla tego pola. Po jakims czasoe weszła ustawa, że jednak musi mieć specyficzny format. Dużo łatwiej w jednym miejscu dodac walidację niz szukać po całej aplikacji gdzie ustawiasz to pole.
1
komentarz 2 sierpnia 2018 przez Aisekai Nałogowiec (42,190 p.)
A co do testowania polaczenia z baza danych: ma to sens. Tylko nie sa to testy jednostkowe, a testy integracyjne.

Podobne pytania

0 głosów
1 odpowiedź 357 wizyt
pytanie zadane 4 maja 2021 w Java przez janyczek Początkujący (360 p.)
0 głosów
0 odpowiedzi 313 wizyt
pytanie zadane 24 listopada 2019 w Java przez JuniorPL Użytkownik (770 p.)
0 głosów
2 odpowiedzi 339 wizyt
pytanie zadane 11 stycznia 2016 w Java przez natrov Gaduła (3,970 p.)

92,451 zapytań

141,261 odpowiedzi

319,073 komentarzy

61,853 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Akademia Sekuraka 2024 zapewnia dostęp do minimum 15 szkoleń online z bezpieczeństwa IT oraz dostęp także do materiałów z edycji Sekurak Academy z roku 2023!

Przy zakupie możecie skorzystać z kodu: pasja-akademia - użyjcie go w koszyku, a uzyskacie rabat -30% na bilety w wersji "Standard"! Więcej informacji na temat akademii 2024 znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...