Co dzieje się z testami metod, gdy metoda ta staje się prywatna po przeprojektowaniu w TDD?

29

Załóżmy, że zaczynam rozwijać grę RPG z postaciami atakującymi inne postacie i tym podobne rzeczy.

Stosując TDD, wykonuję kilka przypadków testowych w celu przetestowania logiki wewnątrz Character.receiveAttack(Int)metody. Coś takiego:

@Test
fun healthIsReducedWhenCharacterIsAttacked() {
    val c = Character(100) //arg is the health
    c.receiveAttack(50) //arg is the suffered attack damage
    assertThat(c.health, is(50));
}

Powiedz, że mam 10 metod testowania receiveAttackmetod. Teraz dodaję metodę Character.attack(Character)(która wywołuje receiveAttackmetodę) i po przetestowaniu jej przez niektóre cykle TDD podejmuję decyzję: Character.receiveAttack(Int)powinno być private.

Co dzieje się z poprzednimi 10 testami? Czy powinienem je usunąć? Czy powinienem zachować metodę public(nie sądzę)?

To pytanie nie dotyczy tego, jak przetestować prywatne metody, ale jak sobie z nimi poradzić po przeprojektowaniu podczas stosowania TDD

Zabijaka
źródło
2
Możliwy duplikat testowania metod prywatnych jako chronionych
komnata
10
Jeśli jest prywatny, nie testujesz go, to takie proste. Usuń i zrób taniec refactor
kayess
6
Prawdopodobnie walczę tutaj o ziarno. Ale generalnie za wszelką cenę unikam prywatnych metod. Wolę więcej testów niż mniej testów. Wiem, co ludzie myślą: „Co, więc nigdy nie masz żadnej funkcji, której nie chciałbyś udostępniać konsumentowi?”. Tak, mam mnóstwo rzeczy, których nie chcę wystawiać. Zamiast tego, gdy mam metodę prywatną, przekształcam ją na własną klasę i używam tej klasy z klasy oryginalnej. Nowa klasa może być oznaczona jako internallub odpowiednik twojego języka, aby nadal nie była narażona. W rzeczywistości odpowiedź Kevina Cline'a jest tego rodzaju podejściem.
user9993
3
@ user9993 wydaje się, że masz go wstecz. Jeśli ważne jest, aby mieć więcej testów, jedynym sposobem, aby upewnić się, że niczego nie przeoczyłeś, jest przeprowadzenie analizy zasięgu. A w przypadku narzędzi pokrycia nie ma znaczenia, czy metoda jest prywatna, publiczna czy cokolwiek innego. Mam nadzieję, że upublicznienie rzeczy w jakiś sposób zrekompensuje brak analizy zasięgu, obawiam się, że fałszywe poczucie bezpieczeństwa
oberwij
2
@gnat Ale nigdy nie mówiłem nic o „braku pokrycia”? Mój komentarz na temat „Wolę więcej testów niż mniej testów” powinien to uczynić oczywistym. Nie jestem pewien, do czego dokładnie zmierzacie, oczywiście testuję również kod, który wyodrębniłem. To o to chodzi.
user9993

Odpowiedzi:

52

W TDD testy służą jako wykonywalna dokumentacja twojego projektu. Twój projekt się zmienił, więc oczywiście twoja dokumentacja również musi!

Zauważ, że w TDD jedynym sposobem, w jaki attackmetoda mogła się pojawić, jest wynik pozytywnego wyniku testu. Co oznacza, attackże jest testowany przez inny test. Co oznacza, że pośrednio receiveAttack jest objęty attacktestami. Idealnie, każda zmiana receiveAttackpowinna przerwać przynajmniej jeden z attacktestów.

A jeśli nie, to funkcjonalność receiveAttacknie jest już potrzebna i nie powinna już istnieć!

Ponieważ receiveAttackjest już przetestowany attack, nie ma znaczenia, czy zachowasz testy. Jeśli środowisko testowe ułatwia testowanie metod prywatnych, a jeśli zdecydujesz się przetestować metody prywatne, możesz je zachować. Ale możesz je również usunąć bez utraty zasięgu testu i pewności siebie.

Jörg W Mittag
źródło
14
To dobra odpowiedź, z wyjątkiem: „Jeśli środowisko testowe ułatwia testowanie metod prywatnych, a jeśli zdecydujesz się przetestować metody prywatne, możesz je zachować”. Metody prywatne są szczegółami implementacyjnymi i nigdy nie powinny być nigdy bezpośrednio testowane.
David Arno
20
@DavidArno: Nie zgadzam się z tym rozumowaniem, że wewnętrzne elementy modułu nigdy nie powinny być testowane. Jednak elementy wewnętrzne modułu mogą być bardzo złożone, dlatego przeprowadzenie testów jednostkowych dla każdej funkcji wewnętrznej może być cenne. Testy jednostkowe służą do sprawdzania niezmienników elementu funkcjonalności, jeśli metoda prywatna ma niezmienniki (warunki wstępne / dodatkowe), wówczas test jednostkowy może być cenny.
Matthieu M.
8
z tego rozumowania elementy wewnętrzne modułu nigdy nie powinny być testowane ”. Te elementy wewnętrzne nigdy nie powinny być bezpośrednio testowane. Wszystkie testy powinny testować tylko publiczne interfejsy API. Jeśli element wewnętrzny jest nieosiągalny za pośrednictwem publicznego interfejsu API, usuń go, ponieważ nic nie robi.
David Arno
28
@DavidArno Zgodnie z tą logiką, jeśli budujesz plik wykonywalny (a nie bibliotekę), nie powinieneś mieć żadnych testów jednostkowych. - „Wywołania funkcji nie są częścią publicznego interfejsu API! Są tylko argumenty wiersza poleceń! Jeśli wewnętrzna funkcja twojego programu jest nieosiągalna przez argument wiersza poleceń, usuń ją, ponieważ nic nie robi.” - Chociaż funkcje prywatne nie są częścią publicznego interfejsu API klasy, są one częścią wewnętrznego interfejsu API klasy. I chociaż niekoniecznie musisz testować wewnętrzny interfejs API klasy, możesz, używając tej samej logiki do testowania wewnętrznego interfejsu API pliku wykonywalnego.
RM
7
@RM, jeśli miałbym utworzyć plik wykonywalny w sposób niemodularny, to musiałbym wybierać między kruchymi testami elementów wewnętrznych lub tylko testami integracyjnymi z wykorzystaniem plików wykonywalnych i we / wy środowiska wykonawczego. Dlatego zgodnie z moją logiką, a nie twoją wersją strawmana, stworzyłbym go w sposób modułowy (np. Poprzez zestaw bibliotek). Publiczne interfejsy API tych modułów można następnie przetestować w sposób niełamliwy.
David Arno
23

Jeśli metoda jest na tyle złożona, że ​​wymaga testów, powinna być publiczna w niektórych klasach. Refaktoryzujesz więc z:

public class X {
  private int complexity(...) {
    ...
  }
  public void somethingElse() {
    int c = complexity(...);
  }
}

do:

public class Complexity {
  public int calculate(...) {
    ...
  }
}

public class X {
  private Complexity complexity;
  public X(Complexity complexity) { // dependency injection happiness
    this.complexity = complexity;
  }

  public void something() {
    int c = complexity.calculate(...);
  }
}

Przenieś bieżący test X.complexity na ComplexityTest. Następnie napisz X. coś przez wyśmiewanie się ze złożoności.

Z mojego doświadczenia wynika, że ​​refaktoryzacja w kierunku mniejszych klas i krótszych metod przynosi ogromne korzyści. Są łatwiejsze do zrozumienia, łatwiejsze do przetestowania, a w końcu są wykorzystywane więcej niż można by się spodziewać.

Kevin Cline
źródło
Twoja odpowiedź wyjaśnia znacznie jaśniej pomysł, który próbowałem wyjaśnić w moim komentarzu do pytania OP. Niezła odpowiedź.
user9993
3
Dzięki za odpowiedź. W rzeczywistości metoda receiveAttack jest dość prosta ( this.health = this.health - attackDamage). Być może wyodrębnienie go do innej klasy jest na razie rozwiązaniem nadinżynieryjnym.
Héctor
1
Jest to zdecydowanie przesada dla OP - chce jechać do sklepu, nie lecieć na księżyc - ale dobre rozwiązanie w ogólnym przypadku.
Jeśli funkcja jest tak prosta, być może jest to nadinżynieria, że ​​w ogóle definiuje się ją jako funkcję.
David K
1
dziś może to być przesada, ale za 6 miesięcy, gdy w tym kodzie pojawi się mnóstwo zmian, korzyści będą oczywiste. I w każdym przyzwoitym IDE z pewnością wyodrębnienie jakiegoś kodu do oddzielnej klasy powinno być co najwyżej kilkoma naciśnięciami klawiszy, co nie jest zbyt skomplikowanym rozwiązaniem, biorąc pod uwagę, że w binarnym środowisku uruchomieniowym wszystko i tak sprowadza się do tego samego.
Stephen Byrne,
6

Powiedzmy, że mam 10 metod testujących metodę receiveAttack. Teraz dodaję metodę Character.attack (Character) (która wywołuje metodę receiveAttack) i po kilku testach TDD testuję ją, podejmuję decyzję: Character.receiveAttack (Int) powinien być prywatny.

Należy pamiętać o tym, że podejmujesz decyzję o usunięciu metody z interfejsu API . Sugerują to uprzejmości związane z kompatybilnością wsteczną

  1. Jeśli nie musisz go usuwać, pozostaw go w interfejsie API
  2. Jeśli nie musisz go jeszcze usuwać , oznacz go jako przestarzały i, jeśli to możliwe, udokumentuj datę końca życia
  3. Jeśli musisz go usunąć, oznacza to poważną zmianę wersji

Testy są usuwane / lub zastępowane, gdy interfejs API nie obsługuje już tej metody. W tym momencie metoda prywatna jest szczegółem implementacji, który powinien być w stanie refaktoryzować.

W tym momencie powracasz do standardowego pytania, czy Twój zestaw testów powinien bezpośrednio uzyskiwać dostęp do implementacji, czy raczej wchodzić w interakcje wyłącznie za pośrednictwem publicznego interfejsu API. Prywatna metoda jest czymś, co powinniśmy być w stanie zastąpić bez przeszkadzania pakietowi testów . Spodziewałbym się więc, że testy się odejdą - albo przejdzie na emeryturę, albo przejdzie wraz z implementacją do osobno testowalnego komponentu.

VoiceOfUnreason
źródło
3
Odstąpienie nie zawsze stanowi problem. Od pytania: „Powiedzmy, że zaczynam się rozwijać ...” jeśli oprogramowanie nie zostało jeszcze wydane, wycofanie nie jest problemem. Ponadto: „gra fabularna” oznacza, że nie jest to biblioteka wielokrotnego użytku, ale oprogramowanie binarne skierowane do użytkowników końcowych. Podczas gdy niektóre oprogramowanie użytkownika końcowego ma publiczny interfejs API (np. MS Office), większość nie. Nawet oprogramowanie, które nie mają publicznego API ma tylko jego część odsłoniętą dla wtyczek, skryptów (np gier z rozszerzeniem LUA) lub inne funkcje. Warto jednak przywołać pomysł na ogólny przypadek opisany przez PO.