Czy używasz TDD? oraz pytania do TDD.

Czy używasz TDD? oraz pytania do TDD.
WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
1

@somekind:

Ten przykład jest dla mnie mocno średni, bo to, czy mogę zwrócić Result<X> czy nie, nie zależy od mojego doświadczenia ani wiedzy, tylko od tego, czego oczekują użytkownicy danej biblioteki

Część userów chce X, bo nie myśli o problemach które są pod spodem i ogólnie mają wywalone na obsługę błędów.

Cześć userów chce Result<X>, bo są świadomi problemów i chcą obsługiwać błędy.

Inni pewnie by woleli aby leciał wyjątek, no bo przecież tradycja tak nakazuje.

I teraz Ty masz zadecydować jak to rozegrać aby twoje API było jak najlepsze.

spike, którego wynikiem jest dokumentacja z opisem napotkanych problemów i ich rozwiązań. Tu piszę kod w dużej mierze do wyrzucenia, bez testów.
solution design
implementacja - tu już wiem, co i jak osiągnąć, mam zaprojektowane API, więc implementuję.

A po co?

Ja staram się robić 1 oraz 3 na raz, taki "best effort", i ewentualnie później nanoszę zmiany (redesign/refactor/optymalizacja), a na koniec testy i docsy.

A tak w ogóle, to ja nawet sobie nie wiem jak można implementować debuggerem podczas tworzenia biblioteki, i nie mieć testów wcześniej. Przecież biblioteki samej z siebie nie ma jak uruchomić, testy są najprostszym sposobem.

Pewnie wiele zależy jaką bibliotekę piszesz, ale generalnie bibliotekę można np. wywołać z aplikacji cli, albo jak chcesz, to nawet przyciskiem z GUI :)

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
WeiXiao napisał(a):

Ja staram się robić 1 oraz 3 na raz, taki "best effort", i ewentualnie później nanoszę zmiany (redesign/refactor/optymalizacja), a na koniec testy i docsy.

Bardzo chciałbym zobaczyć te testy :D

Coś mi mówi, ze one są tak dobre, że mógłbym wywalić 80% kodu i te testy nadal by przeszły.

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
1

@Riddle

Od kiedy kolejność pisania kodu i testów wpływa na pokrycie?

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
1
WeiXiao napisał(a):

@Riddle

Od kiedy kolejność pisania kodu i testów wpływa na pokrycie?

Od zawsze.

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
0

@Riddle

Sugerujesz że pisząc testy po napisaniu implementacji nie da się osiągnąć bardzo dobrego rezultatu?

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
WeiXiao napisał(a):

@Riddle

Sugerujesz że pisząc testy po napisaniu implementacji nie da się osiągnąć bardzo dobrego rezultatu?

Da się, ale jest to dużo trudniejsze, i z reguły wychodzi dużo, dużo rzadziej. Zajmuje też dużo więcej czasu, przez co jest nieopłacalne.

Poza tym, pisząc kod najpierw narażasz się na napisanie nietestowalnego kodu w dużo większym stopniu.

Jeśli chcesz to pokaż przykład swojego kodu i testów. Możliwe że napisałeś je bardzo dobrze od początku.

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
0

@Riddle

A zdajesz sobie sprawę że przy projektach gdzie koszty bugów są bardzo duże nierzadko jest tak, że

Jeden zespół developuje produkt (i swoje testy)

Drugi zespół sprawdza pierwszy poprzez stworzenie fake produktu i/lub testów, aby zweryfikować ten pierwszy?

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
WeiXiao napisał(a):

@Riddle

A zdajesz sobie sprawę że przy projektach gdzie koszty bugów są bardzo duże nierzadko jest tak, że

Jeden zespół developuje produkt (i swoje testy)

Drugi zespół sprawdza pierwszy poprzez stworzenie fake produktu i/lub testów, aby zweryfikować ten pierwszy?

Możesz sobie wytworzyć jakikolwiek randomowy case dookoła praktyk, produktów, klientów jaki chcesz. Możesz sobie wymyślić ile zespołów chcesz, ile QA'ów, ile produktów.

Kiedy taki scenariusz jest dany: napisanie testów przed implementacją ma praktycznie same zalety, i żadnych wad. Więc nie ma powodu, żeby tego nie robić. (oprócz tego że ludzie tego nie umieją albo są nie przywyknięci).

Nawet jeśli sobie wymyślisz scenariusz Drugi zespół sprawdza pierwszy poprzez stworzenie który zmniejsza potrzebę pisania testów; to nadal to nie jest argument przeciwko TDD. Bo nadal stosowanie TDD ma zalety.

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
1

@Riddle:

Kiedyś wrzucałem jakiś projekt na 4p - z tego co widzę było tam trochę testów, więc pomimo tego że było to ze 2 lata temu, to możesz tam zerknąć (poza RegexCodeGeneration, bo to był taki eksperymentalny feature)

https://github.com/xd-loler/YetAnotherStringMatcher

Zapraszam do wywalenia 80% kodu i pokazania że testy dalej przechodzą ;)

Kiedy taki scenariusz jest dany: napisanie testów przed implementacją ma praktycznie same zalety, i żadnych wad.

Napisanie testów, czy robienie tej całej ceremonii "TDD"?

  • Add a test
  • Run all tests. The new test should fail for expected reasons
  • Write the simplest code that passes the new test

To moim zdaniem jest bullshit ^

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
WeiXiao napisał(a):

@Riddle:

Kiedyś wrzucałem jakiś projekt na 4p - z tego co widzę było tam trochę testów, więc pomimo tego że było to ze 2 lata temu, to możesz tam zerknąć (poza RegexCodeGeneration, bo to był taki eksperymentalny feature)

https://github.com/xd-loler/YetAnotherStringMatcher

Zapraszam do wywalenia 80% kodu i pokazania że testy dalej przechodzą ;)

Pomijając RegexCodeGeneration, masz dwa pliki z testami które kolejno mają 37, 6, czyli łącznie 43 testy. Dodatkowo, masz 20 klas z kodem które mają pomiędzy 50-100 linijek.

Myślę, że gdybym się uparł to mógłbym wywalić 80% tego kodu, i testy nadal by przeszły.

Można powiedzieć sporo dobrego o tym kodzie, tzn. krótkie pliki, krótkie funkcje, nazwy takie poł na pół, brak statycznych metod, abstrakcje nie są specjalnie przedmuchane. Mało komentarzy oprócz miejsc, gdzie są konieczne, dobre użycie elementów funkcyjnych.

Ale są też wady, np niektóre funkcje mają 20+ linijek, ToString() wydają się bezsensowne, niepotrzebne dziedziczenie, niepotrzebny else po return, no i oczywiście słabe testy.

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
0

@Riddle

Myślę, że gdybym się uparł to mógłbym wywalić 80% tego kodu, i testy nadal by przeszły.

To czekam.

Oczywiście nie mówimy o "wywaleniu" kodu a'la zamiana

Kopiuj
if (a)
{
  return A;
}
else
{
  return B;
}

na

Kopiuj
return a ? A : B;

tylko faktycznym wywaleniu kodu i zmianie zachowania pokazującym brak pokrycia?

No bo wywalić nowe linie, komentarze, albo w ogóle upchnąć ile się da w jedną linijkę to każdy może :D

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
1
WeiXiao napisał(a):

@Riddle

Myślę, że gdybym się uparł to mógłbym wywalić 80% tego kodu, i testy nadal by przeszły.

To czekam.

Oczywiście nie mówimy o "wywaleniu" kodu a'la zamiana
[...]
tylko faktycznym wywaleniu kodu i zmianie zachowania pokazującym brak pokrycia?

Nie, mam na myśli to że są miejsca w Twoim kodzie w którym mógłbym dosłownie wywalić cały kod, i wstawić return true; i Twoje testy przejdą. Spróbuj. Wejdź do tego kodu, znajdź losowe miejsce i usuń logikę i zobacz czy jakieś testy się wywalą.

Swoją drogą, teraz masz szansę spróbować TDD.

  1. Usuń cały kod z Core/, wtedy wszystkie testy się nawet nie skompilują.
  2. Zaczynasz z napisanymi testami
  3. Dodaj klasy, ale TYLKO i wyłącznie te, konieczne do tego żeby testy się skompilowały. Za pewne nie przejdą. Jeśli jakaś klasa "byłoby fajnie jakby była", ale testy się kompilują bez niej - to nie dodawaj jej.
  4. Potem zobacz pierwszy test, i zastanów się jakie minimum logiki sprawi że ten jeden test przejdzie. Jeśli masz chętkę napisać jakąś dodatkową logikę, ale bez niej test nadal przechodzi to nie dopisuj jej.
  5. Potem zobacz drugi test, i zrób to samo.
  6. Powtórz to samo dla wszystkich innych testów.

Zostanie Ci kod, który jest w 100% przetestowany. Zobaczysz też wtedy jaka ilość usuniętego kodu faktycznie była nieprzetestowana, skoro nie jest konieczna do tego żeby testy przeszły.

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
0

Wziąłem pierwszy lepszy projekt który miałem wystawiony na świat pisany ze 2 lata temu, nawet nie pamiętając w jakim jest stanie
bo zasugerowałeś że lekką ręką wywalisz z niego 80% kodu i testy będą nadal przechodzić

Jest tam:

Get-ChildItem -Filter "*.cs" -Recurse | Get-Content | Measure-Object -line

Lines Words Characters Property


821

800 linijek - daj znać jak zejdziesz poniżej tych 200 z testami na zielono.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
1
WeiXiao napisał(a):

Wziąłem pierwszy lepszy projekt który miałem wystawiony na świat pisany ze 2 lata temu, nawet nie pamiętając w jakim jest stanie
bo zasugerowałeś że lekką ręką wywalisz z niego 80% kodu i testy będą nadal przechodzić

Jest tam:

Get-ChildItem -Filter "*.cs" -Recurse | Get-Content | Measure-Object -line

Lines Words Characters Property


821

800 linijek - daj znać jak zejdziesz poniżej tych 200 z testami na zielono.

Dobrze. Niech Ci będzie.

  1. Zrobiłem clone'a repo,
  2. Usunąłem wszystkie pliki z Core/, było 100 błędów kompilacji.
  3. Dodałem klasy Matcher, Result które mają metody oraz same return this;, plus 3 puste klasy (puste tzn samo class Something {})
  4. Przechodzi 28 testów, 15 nadal failuje. Czyli grubo ponad połowa testów przechodzi z "głupią" implementacją, taką żeby tylko się skompilowało.

screenshot-20231004005040.png

Myślę że napiszę jeszcze z 20 linijek i przejdą wszystkie.

Jak sobie tak piszę, to praktycznie żaden test nie wymaga używania regexpów. Pierwszy który używa ich faktycznie to Test031_Extract. Pozostałe da się zaimplementować zwykłą logiką i return this;, co jeszcze bardziej pokazuje jak bardzo te testy są słabe.

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
0

Myślę że napiszę jeszcze z 20 linijek i przejdą wszystkie.

Minęło 15min, a 20 linijek nadal nie zostało napisanych :P

PS: Nie zapomnij wrzucić kodu

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
3

Tak jak mówiłem.

Wywaliłem większość plików:

screenshot-20231004014758.png

Oraz większośc logiki. Niektóre metody mają return this;. Wszystkie testy przechodzą. Kod oczywiście można zrefaktorować, nie jest to clean code - to co zrobiłem miało za zadanie pokazać po prostu jak słabe są testy które napisałeś. A są słabe na tyle, że jak mówiłem, można wyrzucić znaczną część Twojego kodu, a testy nadal przechodzą.

Gdyby testy były dobre, to nie mógłbym usunąć ani jednej linijki, tak żeby testy zaczęły failować. Mógłbym jedynie refaktorować (np if na ?: jak zauważyłeś).

Kod tutaj: https://github.com/danon/YetAnotherStringMatcher/commits/master

Oczywiście wiele funkcjonalności zostało usuniętych, kod jest mniejszy. Żeby poprawnie dodać te funkcjonalności z powrotem, powinieneś napisać więcej testów.

Zauważyłem tez że niektóre testy są redundantne. Tzn. nie testują niczego nowego, czego poprzednie testy nie przetestowały - i możnaby je usunąć.

somekind
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Wrocław
0
WeiXiao napisał(a):

Część userów chce X, bo nie myśli o problemach które są pod spodem i ogólnie mają wywalone na obsługę błędów.

Cześć userów chce Result<X>, bo są świadomi problemów i chcą obsługiwać błędy.

Inni pewnie by woleli aby leciał wyjątek, no bo przecież tradycja tak nakazuje.

I teraz Ty masz zadecydować jak to rozegrać aby twoje API było jak najlepsze.

Decyzja ta jest dość oczywista - w zależności od tego, kto i do czego będzie danej biblioteki używał. Zazwyczaj nie mogę zmuszać nikogo do używania resultów, więc biblioteka będzie rzucała wyjątki.
Czasami będą po prostu dwie biblioteki, albo jedna z dwoma API.

spike, którego wynikiem jest dokumentacja z opisem napotkanych problemów i ich rozwiązań. Tu piszę kod w dużej mierze do wyrzucenia, bez testów.
solution design
implementacja - tu już wiem, co i jak osiągnąć, mam zaprojektowane API, więc implementuję.

A po co?

Jak to po co?
Żeby było wiadome, co jest do zrobienia, i dlaczego należy to zrobić w ten, a nie inny sposób. Nie jestem jednym programistą w firmie.

Ja staram się robić 1 oraz 3 na raz, taki "best effort", i ewentualnie później nanoszę zmiany (redesign/refactor/optymalizacja), a na koniec testy i docsy.

Ok. A jaki jest roczny przychód tego biznesu?

Pewnie wiele zależy jaką bibliotekę piszesz, ale generalnie bibliotekę można np. wywołać z aplikacji cli, albo jak chcesz, to nawet przyciskiem z GUI :)

Tylko wtedy trzeba mieć aplikację CLI albo GUI, a więc trzeba stracić czas, żeby ją napisać.

SZ
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 52
0

Czy po zmianie implementacji API, use caseow (ale nie kontraktu) nie macie testów do wyrzucenia w sytuacji wysokiego pokrycia. ( bo rozumiem że takie 80-100% macie tylko jak testujecie szczegóły implementacyjne).

SE
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 321
0
Riddle napisał(a):

Nie, mam na myśli to że są miejsca w Twoim kodzie w którym mógłbym dosłownie wywalić cały kod, i wstawić return true; i Twoje testy przejdą. Spróbuj. Wejdź do tego kodu, znajdź losowe miejsce i usuń logikę i zobacz czy jakieś testy się wywalą.

Tylko czy to jest jakikolwiek wyznacznik? Moglbym miec zamockowane testy i po wywaleniu zaden test Ci nie przejdzie :P

@Riddle: Sory, skrot myslowy. Mam na mysli, ze masz syfiasty kod, ktory musi miec wstrzykniete mocki podczas testowania. W zacytowanej sytuacji nie moglbys wywalic zadnej logiki, bo test sie wywali przez brak wywolan mockow :P

SE
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 321
1

@szmeterling:
Wlasnie zastanawiam sie nad czyms podobnym. Skoro:

Riddle napisał(a):

Gdyby testy były dobre, to nie mógłbym usunąć ani jednej linijki, tak żeby testy zaczęły failować. Mógłbym jedynie refaktorować (np if na ?: jak zauważyłeś).

To przeciez w takiej sytuacji mamy kod zabetonowy testami na miare mockow. Bardziej mnie interesuje co ma sie wydarzyc, a nie jak.

TU
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 122
0

@Seken: czyli napisałeś sobie early return warunku gdy możesz przewidzieć, że nie ma rozwiązania, ale ktoś wywalił ten early return i algorytm niepotrzebnie wykonał wszystkie obliczenia, mimo że funkcja w obu przypadkach tak samo działa.

Jeśli zrobisz bardziej granularne testy to wtedy jak się okaże, że jakiś algorytm jest bardziej optymalny od tego co napisałeś to wszystkie testy do śmietnika, bo zabetonowany cały algorytm na jeden możliwy sposób wykonania.

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
0

@Riddle

Tak jak mówiłem.
Wywaliłem większość plików:

Przerobiłeś kod, który... ręcznie robi regexy na... regexa

i wyszło ci że jest mniej kodu?

:(

To tak jakbyś napisał biblioteke do jsonów i napisał że możesz wywalić 80% jej kodu poprzez wywołanie pod spodem innej :D

przecież to nie jest wywalenie kodu, a refaktor (czy tam trytytkowanie).

Jasne, pokazałeś że testy które tam są pokrywają tylko jakieś happy-pathe i są luki, ale nadal nie wywaliłeś kodu, a po prostu przerzuciłeś go do innej libki (prawdziwego regexa)

Kopiuj
public Matcher NoMore()
{
    if (this.regexp.EndsWith("$"))
    {
        this.invalid = true;
    }
    this.regexp += "$";
    return this;
}

public Matcher MatchAnything()
{
    this.regexp += ".*";
    return this;
}

public Matcher MatchAnyOf(string parcel, string taxi)
{
    this.regexp = "(" + parcel + "|" + taxi + ")";
    return this;
}

public Matcher ThenExtractAs(string name)
{
    this.regexp += "(?<" + name + ">.+)";
    return this;
}

Oraz większośc logiki. Niektóre metody mają return this;. Wszystkie testy przechodzą. Kod oczywiście można zrefaktorować, nie jest to clean code - to co zrobiłem miało za zadanie pokazać po prostu jak słabe są testy które napisałeś. A są słabe na tyle, że jak mówiłem, można wyrzucić znaczną część Twojego kodu, a testy nadal przechodzą.

Ja się zgadzam że pokrycie tam jest słabe, bo brakuje pokrycia poza happy-pathami, ale to że można podmienić implementacje na inną (real regex) raczej o tym nie świadczy :)

KamilAdam
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Silesia/Marki
  • Postów: 5550
0
somekind napisał(a):

Ten przykład jest dla mnie mocno średni, bo to, czy mogę zwrócić Result<X> czy nie, nie zależy od mojego doświadczenia ani wiedzy, tylko od tego, czego oczekują użytkownicy danej biblioteki, a to poniekąd wynika z tego, co oferuje język.

Powiedział bym to zależy. Np Rust jest mocno spójny w swoim podejściu (czego mocno mu zazdroszczę) i tam podręczniki mówią żeby zwracać Result ale w takiej Scali jest totalna anarchia i można zwracać: gołą wartość (i exception bokiem), Option (czyli bez opisu błędu), Either (odpowiednik Result), Try (Either gdzie błedem jest exception), Future (asynchroniczny Try), IO (asynchroniczny Either, ale tego nie ma w bibliotece standardowej, z tego powodu użytkownicy biblioteki standardowej rzeźbili kiedyś Future[Either[Error, A]]). Bardziej egzotyczne, jak podejście, tagless final pominę (a jest genialne, ale możliwe chyba tylko w Scali, Haskellu i Haskellopodobnych :( .A czemu jest genialna? bo pozwala pisać kod który jednoczesnie zwraca Either i IO w zależności co potrzebujemy)

to poniekąd wynika z tego, co oferuje język

Wystarczy że język oferuje generyki na jakimkolwiek sensownym poziomie to można zaimplementować własny Result/Either, tak jak to robi vavr dla Javy. Jest to jedna klasa. Własna implementacja asynchronicznego IO to już większa zabawa. W Scali są pod to osobne biblioteki np cats-effect

zależy (...) od tego, czego oczekują użytkownicy

Tylko że jak użytkownik oczekuje wartości a dostał Option/Either/Try to wystarczy że wywoła sobie na tym .get() i już dostaje wartość. Oczywiście jak tej wartości nie ma to leci exception. To jak łątwo rozpakować Option/Either/Try było też powodem czemu ludzie w Javie mieli problemów z nauczeniem się poprawnego używania Optional, ale to już opowieść bardziej na TWF programowania, zresztą w WTF programowania jest tam dużo takiego kodu

jarekr000000
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: U krasnoludów - pod górą
  • Postów: 4712
1
WeiXiao napisał(a):

@somekind:

Ten przykład jest dla mnie mocno średni, bo to, czy mogę zwrócić Result<X> czy nie, nie zależy od mojego doświadczenia ani wiedzy, tylko od tego, czego oczekują użytkownicy danej biblioteki

Część userów chce X, bo nie myśli o problemach które są pod spodem i ogólnie mają wywalone na obsługę błędów.

Cześć userów chce Result<X>, bo są świadomi problemów i chcą obsługiwać błędy.

Inni pewnie by woleli aby leciał wyjątek, no bo przecież tradycja tak nakazuje.

I teraz Ty masz zadecydować jak to rozegrać aby twoje API było jak najlepsze.

Jak autor wybiera zwracanie X i rzucanie wyjątkiem (obsługiwalnym) to czuje się jakbym wszedł do toalety w restauracji, gdzie właściciel nie zamontował umywalki, bo i tak większość ludzi rąk nie myje, i preferuje okazjonalne rzucenie exceptionem (do przodu, albo do tyłu) kilka dni po wizycie.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
WeiXiao napisał(a):

@Riddle

Tak jak mówiłem.
Wywaliłem większość plików:

Przerobiłeś kod, który... ręcznie robi regexy na... regexa

i wyszło ci że jest mniej kodu?

Napisałem kod który przechodzi testy, tak jak się w TDD powinno robić.

Jeśli masz jakieś dodatkowe założenia, to powinieneś napisać test pod to.

Poza tym, nawet jakbym przepisał regexpy na Func<>, żeby było "tak jak chcesz", to nadal to nie zmienia faktu jak wiele kodu można było wywalić, property, exceptiony, message, logikę przecież prawie całą.

Owszem, dodałem Trochę, absolutne minimum logiki w regexpach, (bo to było najprostsze); i mógłbym też dodać minimum logiki predykatowej, ale nadal efekt byłby podobny - masa kodu usunięta.

To tak jakbyś napisał biblioteke do jsonów i napisał że możesz wywalić 80% jej kodu poprzez wywołanie pod spodem innej :D

przecież to nie jest wywalenie kodu, a refaktor (czy tam trytytkowanie).

Jasne, pokazałeś że testy które tam są pokrywają tylko jakieś happy-pathe i są luki, ale nadal nie wywaliłeś kodu, a po prostu przerzuciłeś go do innej libki (prawdziwego regexa)

pokazałeś że testy które tam są pokrywają tylko jakieś happy-pathe i są luki. No i tylko o to mi chodziło, dziękuję, kurtyna.

Poza tym, @WeiXiao wiem jak działasz, wiem jak trollujesz ludzi - już sam się dałem z tym wykazaniem luk w testach w Twoim projekcie. Nawet jakbym teraz przerobił ten kod na predicate'y, to potem byś znalazł jeszcze jnedą rzecz i jeszcze jedną, i ciągnąłbyś mnie swój trollerski play, także nie dzięki. Sam to przerób na predicate'y. Masz przecież test suite, możesz to łatwo zrobić.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
Seken napisał(a):
Riddle napisał(a):

Nie, mam na myśli to że są miejsca w Twoim kodzie w którym mógłbym dosłownie wywalić cały kod, i wstawić return true; i Twoje testy przejdą. Spróbuj. Wejdź do tego kodu, znajdź losowe miejsce i usuń logikę i zobacz czy jakieś testy się wywalą.

Tylko czy to jest jakikolwiek wyznacznik? Moglbym miec zamockowane testy i po wywaleniu zaden test Ci nie przejdzie :P

@Riddle: Sory, skrot myslowy. Mam na mysli, ze masz syfiasty kod, ktory musi miec wstrzykniete mocki podczas testowania. W zacytowanej sytuacji nie moglbys wywalic zadnej logiki, bo test sie wywali przez brak wywolan mockow :P

@Seken: Okej, już wiem o co Ci chodzi. Odpowiadając na Twoje pytanie Tylko czy to jest jakikolwiek wyznacznik, to jest wyznacznik wystarczający, ale nie konieczny. Jeśli wywalę dużo kodu i testy przechodzą - wiem że są słabe.

Jeśli tak jak mówisz, tak łatwo się wywalić kodu nie da - nie oznacza to jeszcze że testy są dobre, może trzeba właśnie dodać calle do mocków. Ale same calle, nie implementację.

Ogólnie, jakoś testów można mierzyć tym ile kodu można zepsuć/wywalić, i one przejdą. To właściwie filozofia za mutation testing.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
1
Seken napisał(a):

@szmeterling:
Wlasnie zastanawiam sie nad czyms podobnym. Skoro:

Riddle napisał(a):

Gdyby testy były dobre, to nie mógłbym usunąć ani jednej linijki, tak żeby testy zaczęły failować. Mógłbym jedynie refaktorować (np if na ?: jak zauważyłeś).

To przeciez w takiej sytuacji mamy kod zabetonowy testami na miare mockow. Bardziej mnie interesuje co ma sie wydarzyc, a nie jak.

Wysuwasz bardzo dobry temat: jak zapewnić, żeby dodanie buga (nawet najmniejszego) failowało testy, ale jednocześnie żeby wprowadzanie zmian w projekcie było bardzo szybkie oraz łatwe?

Testy są po to żeby wykrywać bugi. Dajmy przykład że wchodzę do projektu, i usuwam linijkę. Co się wtedy dzieje?

  • Jeśli aplikacja nie działa tak jak ma działać, to ewidentnie to jest bug. A skoro tak, to jakiś test powinien się wywalić. I mam na myśli tutaj dowolną linijkę w kodzie.
  • Po usunięciu linijki aplikacja nadal działa tak jak ma działać i nic się nie zepsuło, wszystko jest nadal tak jak ma być? Wtedy to nie jest bug, żaden test nie powinien sfailić, i widocznie linijka była niepotrzebna. Należy ją wywalić.

Ale z drugiej strony, wiele razy widzieliśmy takie testy, że chcemy zmienić nazwę funkcji, albo przenieść ją do innej klasy, i nie udało się to zrobić łatwo, bo wtedy były tak przyspawane do kodu, że malutka, nawet drobna zmiana powodowała fail masy testów. Coś co nazwałeś "kod zabetonowany testami".

Jak więc to pogodzić?

Trzeba być sprytnym.. Trzeba napisać testy które są czułe na zachowanie (jakie wartości są zwracane, jakie side effecty, jakie wartości parametrów, ile wywołań, jaki wyjątek), a jednocześnie całkowicie nieczułe na szczegóły (konkretna klasa, konkretna funkcja, konkretny moduł). To jak napisać takie testy to jest temat lat praktyk, czytania, szukania, doszkalania. Nie jest to prosta sprawa. To jest do zrobienia, tylko po prostu nie jest proste. (Dodatkowa miła konsekwencja jest taka, że jeśli mamy testy napisane w taki sposób, to one wymuszają loose-coupling. A skoro tak, to nasz kod naturalnie sam z siebie też jest wtedy loosly-coupled).

W ten sposób zapewnimy sobie testy które wykrywają bugi, a jednocześnie nie przeszkadzają w normalnej pracy.

  • Jeśli przegniesz w jedną stronę, to masz useless testy które nic nie testują.
  • Jeśli przegniesz w drugą, to masz zabetonowany kod i testy które przeszkadzają w pracy

Oczywiście, pewien poziom zabetonowania jest spodziewany - już wyjaśniam dlaczego. Czasem chcemy specjalnie dodać buga. A mówiąc dokładniej, chcemy dodać zmianę która jeszcze wczoraj byłaby bugiem, ale dzisiaj jest normalna (np wczoraj mieliśmy walidację np na wiek, nazwę, maila), a dzisiaj chcemy dodać zmianę która usuwa tą walidację. Więc oczywiście że jeśli ją usuniemy, to test sfailuje i to jest w porządku. Po prostu wtedy poprawiamy albo usuwamy taki test i jedziemy. Nie zawsze fail testu to jest coś złego, w tym wypadku to jest spodziewane. Oczywiście miejsc gdzie jest taki fail powinno być mało, żeby wprowadzenie takiej zmiany było łatwe.

Tutaj mamy kolejny problem: jak pogodzić ilośc testów. Z jednej strony powinno być testów na tyle dużo, żeby pokryć wszystkie zachowania i cechy aplikacji, ale z drugiej strony powinny być na tyle dobrze napisane, że wystąpienie jakiegoś buga nie powoduje nagle 100 faili, tylko kilka konkretnych testów które łatwo poprawić. Pisanie takich testów wymaga rozwagi i doświadczenia. Nie można ich pisać na pałę. To jest kolejny powód czemu testy pisać warto najpierw, kiedy jeszcze mamy zasoby mentale na to.

SZ
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 52
0

Jednym słowem refaktor, zmiana implementacji nie powinna łamać kontraktu - failować testu, zmiana funkcjonalności - modyfikacja api - pociąga za sobą modyfikację testów, nowe testy. A jak ze stosowaniem się do zasady, że do własnego api stosujemy testy sociable , a do cudzego api testy solitary.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
szmeterling napisał(a):

Jednym słowem refaktor, zmiana implementacji nie powinna łamać kontraktu - failować testu, zmiana funkcjonalności - modyfikacja api - pociąga za sobą modyfikację testów, nowe testy.

Tzn. tak i nie.

Na pewno zmiana zachowania powinna failować test. To że aplikacja teraz robi coś innego, potencjalnie niepożądanego.

Ale to że interfejs się zmienia, to jest dużo bardziej subtelna kwestia. Potrafię sobie wyobrazić scenariusze gdzie zmiana interfejsu jak najbardziej powinna failować testy, ale potrafię sobie też wyobrazić takie gdzie nie powinna. To dlatego że nawet interfejs może być szczegółem implementacyjnym. Większy temat.

szmeterling napisał(a):

A jak ze stosowaniem się do zasady, że do własnego api stosujemy testy sociable , a do cudzego api testy solitary.

Jak jak rozumiem testy sociable/solitary, to jest inna nazwa na testy whitebox/blackbox albo london/detroit school?

No to mogę Ci powiedzieć co ja osobiście o tym myślę; i moim zdaniem testy które bardzo polegają na mockach/stubach (czyli są solitary, według tej terminologii) cierpią na bardzo silną miskoncepcję, taką że "testy jednostkowe" powinny testować jednostkę, i że tą jednostką jest klasa. Z tego wyciągają błedny wniosek, że ponieważ mają testować jedną klasę, to znaczy że mogą używać tylko jednej klasy. Moim zdaniem takie podejście praktycznie nigdy nie jest dobre (z wyjątkiem jakiś bardzo hardcore'owych case'ów).

Pisanie testów pod jedną klasę ma sporo wad, np to że refaktor takiej klasy jest potem cięższy, i to że struktura testów przypomina potem strukturę kodu (co ogólnie nie jest dobre). Testy powinny ewoluować w jedną stronę, a kod w drugą. Dla przykładu, testy powinny być addytywne, tzn. każda kolejna funkcjonalność powinna być wartością dodawaną do testów. W kodzie już tak nie jest, kod powinien ewoluować bardziej jak drzewo - jeśli dochodzi nowa funkcjonalność, to nie powinna być adytywna, tylko raczej użyć istniejących elementów, i wydzielić wspólne części. Oczywiście z testów też można wydzielać rzeczy (fixturey, helpery, asercje, dsl'e), ale chodzi o to że dodanie nowych funkcjonalności powinno iść w parze z nowymi testami. Nie koniecznie to samo można powiedzieć o implementacji. Czasem nową funkcjonalność można dodać poprzez edycję lub nawet usunięcie istniejącej implementacji, czego nie można powiedzieć o testach. Dodając nową funkcjonalność nie edytujesz albo nie usuwasz poprzednich testów (chyba że są niekompatybilne). Także testy i kod nie powinny być do siebie podobne. Robiąc testy które wyglądają User <-> UserTest, Order <-> OrderTest własnie do tego doprowadzisz i to nie jest dobre. Dodatkowo, całkowity refaktor czy revamp implementacji powinien być możliwy, i nie powinno to sprawić że testy też mają się zmienić.

Dodam tutaj od razu - oczywiście, możesz mieć testy które testują funkcjonalność usera i funkcjonalność order, ale to że akurat teraz te funkcjonalności są w klasie User i Order, nie znaczy że testy mają być pod te klasy w których akurat teraz ta funkcjonalność jest. Testy powinny być pod zachowanie (co kod/aplikacja robi), a nie pod strukturę (gdzie kod teraz jest, w jakiej klasie).

WeiXiao
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 5227
2

@Riddle

Poza tym, @WeiXiao wiem jak działasz, wiem jak trollujesz ludzi - już sam się dałem z tym wykazaniem luk w testach w Twoim projekcie.
to potem byś znalazł jeszcze jnedą rzecz i jeszcze jedną, i ciągnąłbyś mnie swój trollerski play

Tak, ja jestem trollem.

Przypominam że napisałeś że wywalisz 80% mojego kodu i testy będą przechodziły,
A co zrobiłeś? zrefaktorowałeś kod i zmieniłeś implementacje?

W życiu by mi do głowy nie przyszło, że w dyskusji nt. testów i ich skuteczności ktoś pisząc że wywali kod będzie tak naprawdę miał ma myśli że znajdę krótszą implementacje, bo jest to absolutnie bez sensu, bo jest to wręcz truizm że da się ten kod skrócić, a w szczególności wołając bibliotekę która robi to samo pod spodem.

Tak samo wykorzystywanie wiedzy nt. implementacji testów, do tego aby napisać implementacje, to też niezłe.
Może pójdźmy krok dalej - pisząc w ten sposób implementacje, testy napisane w każdy sposób (TDD czy też bez) będą się pokazywać na zielono

Kopiuj
if (Environment.StackTrace.Contains("Test001_SamplePhoneNumbers"))
{
    return new CheckResult(true, 5);
}

Jest niespójność w tym co piszesz, bo piszesz

Trzeba napisać testy które są czułe na zachowanie (jakie wartości są zwracane, jakie side effecty, jakie wartości parametrów, ile wywołań, jaki wyjątek), a jednocześnie całkowicie nieczułe na szczegóły (konkretna klasa, konkretna funkcja, konkretny moduł).

a chwilę wcześniej pisałeś:

to nadal to nie zmienia faktu jak wiele kodu można było wywalić, property, exceptiony, message, logikę przecież prawie całą.

No tak, jeżeli wywalisz propertisy np.

Kopiuj
public IOperation NextOperation { get; set; }

/// <summary>
/// User friendly name of this requirement.
/// </summary>
string Name { get; }

Gdzie NextOperation który był związany z implementacją mojego algorytmu (a'la linked list), i zmienisz algorytm na taki, który z tego nie korzysta,
to oczywiście test przejdzie, bo te propertisy to są szczegóły implementacji, których test nie powinien dotykać. W przeciwnym wypadku twój refaktor by nie zadziałał, bo byłby przywiązany do mojej impl.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.