Pisanie testów jednostkowych - kilka pytań początkującego

1

Jestem w takim momencie mojej nauki, że (wydaje mi się) są mi już potrzebne testy jednostkowe. Projekty zaczynają mieć kilka klas, myślę o ich rozwoju, dodawaniu nowych featureów, coś o abstrakcji też (ale ciągle denerwuje mnie istnienie interfejsów, z których notabene nie wiem jak korzystać poprawnie).

I mam tu parę pytań:

  1. Kiedy pisanie testów powinno mieć miejsce? [Przed/W trakcie/Po napisaniu kodu]
  2. Czy testy powinny sprawdzać zachowanie konkretnych metod czy części działań?
    • Jeżeli mają sprawdzać zachowania konkretnych metod to czy testy powinny być napisane przed stworzeniem kodu wykonawczego? Jeśli tak to jak tego dokonać?
  3. Czy testy jednostkowe powinny sprawdzać zmiany w plikach?
    • Jeżeli tak to jak przykładowo wyglądałby test takiej metody?
    public static Optional<List<String>> loadDataFromFile(File file) {
        	List<String> rawFileContent = new ArrayList<>();
            try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
    
        		String line;
    
            	while ((line = reader.readLine()) != null ) {
                    	rawFileContent.add(line);
            	}
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
           return Optional.ofNullable(rawFileContent);
        }
    
  4. Czy testy jednostkowe są miarą jakości kodu?
  5. Czy testy powinny sprawdzać lokalną obsługę wyjątków, tj. czy kod znajdujący się w klauzuli catch jest wykonywany poprawnie? (Nie korzystam z przekształceń checked na runtime, nie wiem jak to się obsługuje i czy jest lepsze)
  6. Czy testy powinny sprawdzać zachowania klas reprezentujące pewne dane? Przykład poniżej
package game;

public class Cell {

    private CellState state;

    public Cell(CellState state) {
        this.state = state;
    }

    public Cell setAlive() {
        state = CellState.Alive;
        return this;
    }

    public Cell setDead() {
        state = CellState.Dead;
        return this;
    }

    public boolean isDead() {
        return this.state.equals(CellState.Dead);
    }

    public boolean isAlive() {
        return this.state.equals(CellState.Alive);
    }

    public CellState getState() {
        return state;
    }

    @Override
    public boolean equals(Object obj) {
       Cell argument = (Cell) obj;
       return argument.getStateAsString().equals(this.getStateAsString());
    }

    public String getStateAsString() {
        return state.toString();
    }

    @Override
    public String toString() {
        return state.toString();
    }
}
  1. Czy testy wymagające stworzenia skomplikowanego przykładu (np. tablicy do gry z konkretnym rozstawieniem pionków/jednostek, etc.) ciągle są testami jednostkowymi?
  2. Czy używanie interfejsów wpływa na łatwość pisania testów/ich jakość/czytelność?

Pewnie w trakcie dyskusji będę miał jeszcze jakieś pytania, liczę na waszą wyrozumiałość ;)

1

Ło panie, poruszyłeś ciężki temat i będzie pewno niezła dyskusja (znając @jarekr000000)

1)Ja piszę po
2)Są różne rodzaje testów. Jeśli tworzymy klasy z logiką to wtedy piszemy testy metod, np. mamy dane które przekazywane są do metody, ona je jakoś filtruje, mapuje, są jakieś ify w zależności od nich sprawdza robi coś dalej. Są też testy integracyjne np. w Springu MVC gdzie "wysyłamy zapytanie" i mamy te wszystkie transakcje itp
3)Ależ oczywiście że można. Ja to robiłem zazwyczaj tak że dodawałem do resourców plik testowy i uruchamiałem ze ścieżką do niego. Tyle że Twój przykładowy kod jest de facto w bibliotece Apache Commons
5)Tzn?
6)Raczej na DTO to nie ma sensu, chyba że jakiś skomplikowany equals albo coś w tym stylu

1

subiektywnie i krotko:

  1. kiedy wygodnie
  2. powinny sprawdzac zachowania
  3. nie powinny - jesli juz to np zawartosc jakiegos in-memory strumienia (ktory zastepuje ci prawdziwy plik w tescie). przykladowo twoja metoda moglaby przyjmowac abstrakcyjny strumien ktory mockujesz w tescie.
  4. zwykle tak ale czasem nie bo to zalezy od doswiadczenia ludzi ktorzy te testy pisza
  5. nie, strata czasu i brak profitow
  6. j.w.
  7. wtedy sie mowi ze to sa testy integracyjne, ale tak szczerze to nomenklatura ma male znaczenie - wazna jest uzytecznosc
  8. moze byc wygodnie mockowac interfejsy po to zeby jakos reprezentowac zaleznosci jak np. i/o czy timery ale poza tym to nie powinno miec wiekszego znaczenia
1

Odnośnie wyjątków to jak masz tylko loggera to trudno to testować, a co innego jeśli masz kod który w catchu spowoduje zwrócenie innego obiektu
np.

   public Result sendData(Request requst){
     try{
        //sending data
        return Result.Success;
    }catch(SendindDataException ex){
      logger.error(ex);
      return Result.Failure;
     }
  }
2

Kiedy pisanie testów powinno mieć miejsce? [Przed/W trakcie/Po napisaniu kodu]

Są różne szkoły i różne sytuacje.

Czy testy jednostkowe są miarą jakości kodu?

Testy jednostkowe służą do ratowania d**y, żeby ci się nic nie rozpier... przy okazji refaktoringu czy wprowadzania nowych ficzerów.

Czy testy jednostkowe powinny sprawdzać zmiany w plikach?

Unikałbym tego, chociaż jeśli piszesz coś, co faktycznie ma wczytać dane z pliku albo zapisać coś do pliku - to jednak warto byłoby to przetestować (jednak ogólnie, nie tylko w testach, ale i w implementacji unikałbym kontaktu z plikami - trochę jak @katelx napisała: przykladowo twoja metoda moglaby przyjmowac abstrakcyjny strumien ktory mockujesz w tescie..).

Jeśli masz klasę powiedzmy User, która reprezentuje użytkownika, to z jakiej paki ma ona wiedzieć cokolwiek o systemie plików? To raczej nie powinno należeć do jej odpowiedzialności. No bo jeśli ileś takich klas narobisz, które odwołują się do systemu plików, a potem pojawi się nowe wymaganie biznesowe - wieloplatformowość. Np. apka jest na Windowsy, trzeba będzie ją dostosować do Linuxa i Maka. Na każdym z systemów ścieżki do plików będą trochę inne. I będziesz musiał zmieniać w kilku klasach to samo. Albo np. będzie wymaganie, żeby dorobić ładowanie przez plik z FTP. I będziesz musiał w każdej klasie dorobić metodę loadDataFromFtpFile... Koszmarna duplikacja kodu..

Tak więc uważam, że w ogóle wiedza o systemie plików powinna być zabrana z klasy, której podstawowa funkcjonalność jest inna (np. klasa User do obsługi użytkownika). Takie coś powinno być raczej ładowane w osobnej klasie do ładowania plików. - wtedy ładowałbyś w tej klasie pliki, a inne klasy przyjmowałyby już strumień albo tablicę albo obiekt z danymi, czy jakąkolwiek inną strukturę danych, którą masz w języku).

1

Najłatwiej (i pewnie najbliżej prawdy) odpowiedzieć "to zależy", ale to chyba najgorsze co można powiedzieć początkującemu. To nie jest trywialny temat i zapewne bez jakiejś książki, np. https://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627 się nie obejdzie, bo tutoriale z jakimiś string calculatorami nie mają za wiele wspólnego z pisaniem testów do prawdziwych programów.

  1. Pisanie testów przed kodem produkcyjnym nie jest łatwe, ale ma masę zalet: ułatwia stworzenie czystego, testowalnego kodu, zabezpiecza przed stworzeniem testu który nigdy się nie wysypie, utrudnia odkładanie testów na później (a później nigdy nie nadchodzi) i wiele innych, polecam poczytać Wujka Boba.
  2. Poza najtrywialniejszymi funkcjami raczej nie będziesz w stanie pokryć wszystkich przypadków jednym testem. Jak poprzednicy, najlepiej skupić się na testowaniu zachowań/use case'ów.
  3. Myślę że wolałbym znaleźć jakąś bibliotekę (commonsy chyba każdy ma w projekcie) żeby uniknąć pisania tak niskopoziomowego kodu, a w swoim kodzie operować już na otrzymanych danych które można o wiele łatwiej przygotować lub zamockować.
  4. Nie zawsze, ale brak testów przeważnie jest miarą braku jakości. Główną zaletą testów jest to, że zabezpieczają podczas robienia refactoringu czy modyfikacji. Nigdy nie czułem się pewnie ruszając kod który nie był pokryty testami i parę razy zdarzyło mi się go zepsuć nieświadomie.
  5. Jeśli kod w catchu jest na tyle skomplikowany że chcesz go testować, to wydzieliłbym go do osobnej funkcji i testował jak każdą inną.
  6. Jeśli robisz coś bardziej skomplikowanego niż gettery i settery, np. obiekt sam zarządza modyfikacjami swojego stanu, tak jak na obiekt przystało, to napisałbym do tego testy.
  7. Tak, ale często można rozbić ten kod bardziej. Tutaj bardzo pomaga pisanie testów przed implementacją.
  8. W porównaniu do czego? Do duplikowania kodu, do dziedziczenia, do posiadania zwykłej prostej klasy? (patrzę na was, interfejsy serwisów z jedną implementacją) Tu ciężko coś powiedzieć jednoznacznie. Interfejsy mają swoje zastosowania ale nie warto ich nadużywać. https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it
1
  1. Testy są po to żeby Cię upewnić, że kod działa bez przeklikiwania po aplikacji. Jeżeli chcesz stosować TDD (które teoretycznie Cię spowalnia, ale efektem będzie lepszy kod gdy dobrze programujesz) to pisz przed kodem. Jeżeli jesteś początkującym i nie rozumiesz podstaw OOD to pisz po.

  2. Tak, test powinien sprawdzać pojedyncze warunki. Gdy test nie przechodzi to tylko z jednego powodu. Jeżeli wiesz jak metoda się będzie nazywać to nic więcej nie potrzebujesz, aby napisać test.

  3. Twój kod nie modyfikuje pliku, więc nie masz co sprawdzać. Natomiast, aby to przetestować, możesz w teście uzyskać File do pliku w resourceach i sprawdzić czy Lista linii będzie ok. Poczytaj o ResourceFile z JUnit.

  4. Tak, gdy testów nie ma to zakładamy, że kod nie działa i jest do 4 liter. Gdy są i pokrycie jest 80%-100% to nic nie możemy powiedzieć na temat kodu. Czyli testy mogą powiedzieć czy kod jest zły, ale nie powiedzą czy jest dobry.

  5. Tak, ale może w Twoim przypadku byłoby lepiej albo rzucić RuntimeException (mówiący - programisto napraw ten plik lub kod), a jeżeli plik nie jest krytyczny do działania to zwrócić pustą listę. I test powinien to weryfikować.

  6. Jeżeli ta klasa coś robi, a nie jest pojemnikiem na dane to tak. Ale powinna mieć osobne testy. A nie że kod wczytujący plik testuje inne klasy.

  7. Prawdopodobnie tak. Ogólnie jeżeli Twoje testy są długie to świadczy to, że kod jest fatalny.

  8. Tak. Raz, że możesz stworzyć interfejs i już pisać do niego testy przed jego implementacją (EDIT: Oczywiście gdy planujesz go stworzyć, inaczej stwórz pustą metodę po napisaniu testu). Dwa, że gdy twój kod korzysta z interfejsu, możesz łatwiej dostarczyć zaślepki np. bez Mockito.

Pamiętaj, że wysokie pokrycie nie zawsze dobrze jest osiągać testami do każdej klasy. Możesz wówczas zabetonować swój kod testami.
I każda zmiana w kodzie, będzie wymagała zmiany testów. Unikaj tego. Testuj kod z poziomu stabilnych klas, a to co wewnątrz samo się przetestuje przy okazji.

2

Jeżeli chcesz stosować TDD (które teoretycznie Cię spowalnia

Tylko teoretycznie. Praktycznie to często jak się pisze coś TDD to się oszczędza czas na developerce, ponieważ nie trzeba co chwila się przeklikiwać czy resetować serwera przy każdej zmianie, tylko się odpala testy, które trwają np. z sekundę.

.Jeżeli chcesz stosować TDD (...) to pisz przed kodem.

TDD (przynajmniej wg tego co pisze Wujek Bob, ale możliwe że są różne szkoły) to bardziej pisanie testów w trakcie kodowania (pisanie na zmianę najpierw kawałka testu, potemi kawałka implementacji i tak w kółko), a nie przed kodem.

Tak, gdy testów nie ma to zakładamy, że kod nie działa i jest do 4 liter. Gdy są i pokrycie jest 80%-100% to nic nie możemy powiedzieć na temat kodu.
Czyli testy mogą powiedzieć czy kod jest zły, ale nie powiedzą czy jest dobry.

Może raczej brak testów = kod słaby w utrzymaniu. Bo coś może być napisane nawet bardzo dobrze pod kątem implementacji, ale bez testów ciężko to będzie utrzymywać, rozwijać, dodawać nowe funkcje.

Pamiętaj, że wysokie pokrycie nie zawsze dobrze jest osiągać testami do każdej klasy. Możesz wówczas zabetonować swój kod testami.

Dokładnie. Najgorsze co może się przytracić, to zrobienie testów, które testują bezpośrednio każdą klasę i każdą metodę, i każdą strukturę danych - bo wtedy jakakolwiek zmiana w projekcie będzie wymagać zmiany testów (więc jakikolwiek refaktoring nie będzie możliwy). Fajnie Wujek Bob o tym pisze, że testy wcale nie muszą odzwierciedlać kodu produkcyjnego https://8thlight.com/blog/uncle-bob/2014/01/27/TheChickenOrTheRoad.html

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