W jaki sposób wykryte zostaną błędy pisarskie podczas tworzenia próbnych wersji w dynamicznym języku?

10

Problem występuje podczas wykonywania TDD. Po kilku zdaniach testowych zmieniają się typy zwracane przez niektóre klasy / moduły. W statycznym języku programowania, jeśli poprzedni próbny obiekt był używany w testach innej klasy i nie został zmodyfikowany w celu odzwierciedlenia zmiany typu, wystąpią błędy kompilacji.

Jednak w przypadku języków dynamicznych zmiana typów zwracanych znaków może nie zostać wykryta, a testy drugiej klasy będą nadal zaliczane. Jasne, że mogą istnieć testy integracyjne, które później powinny zakończyć się niepowodzeniem, ale testy jednostkowe zakończyłyby się błędem. Czy jest jakiś sposób, aby tego uniknąć?

Aktualizowanie za pomocą trywialnej próbki (w niektórych wymyślonych językach) ...

Wersja 1:

Calc = {
    doMultiply(x, y) {return x * y}
}
//.... more code ....

// On some faraway remote code on a different file
Rect = {
    computeArea(l, w) {return Calc.doMultipy(x*y)}
}

// test for Rect
testComputeArea() { 
    Calc = new Mock()
    Calc.expect(doMultiply, 2, 30) // where 2 is the arity
    assertEqual(30, computeArea)
}

Teraz w wersji 2:

// I change the return types. I also update the tests for Calc
Calc = {
    doMultiply(x, y) {return {result: (x * y), success:true}}
}

... Następnie Rect zgłasza wyjątek w czasie wykonywania, ale test nadal się powiedzie.

jvliwanag
źródło
1
Jak dotąd wydaje się, że brakuje odpowiedzi, że pytanie nie dotyczy testów obejmujących zmienione class X, ale testy class Yzależą od tego, Xa zatem są testowane na podstawie innego kontraktu niż to, z czym ma do czynienia w produkcji.
Bart van Ingen Schenau
Właśnie zadałem to pytanie SO , w odniesieniu do wstrzykiwania zależności. Patrz Przyczyna 1: Klasa zależna może zostać zmieniona w czasie wykonywania (pomyśl testowanie) . Oboje mamy takie samo nastawienie, ale brakuje nam świetnych wyjaśnień.
Scott Coates,
Ponownie czytam twoje pytanie, ale trochę się mylę z interpretacją. Czy możesz podać przykład?
Scott Coates,

Odpowiedzi:

2

W pewnym stopniu jest to tylko część kosztów prowadzenia działalności w dynamicznych językach. Otrzymujesz dużą elastyczność, znaną również jako „wystarczająca ilość liny do zawieszenia się”. Uważaj na to.

Dla mnie problem sugeruje użycie innych technik refaktoryzacji niż w przypadku języka o typie statycznym. W języku statycznym częściowo zastępujesz typy zwracane, abyś mógł „polegać na kompilatorze”, aby znaleźć miejsca, które mogą ulec uszkodzeniu. Jest to bezpieczne i prawdopodobnie bezpieczniejsze niż modyfikowanie typu zwrotu w miejscu.

W dynamicznym języku nie możesz tego zrobić, więc bezpieczniej jest zmodyfikować typ zwracanego miejsca niż go zastąpić. Być może zmodyfikujesz go, dodając do niego nową klasę dla klas, które tego potrzebują.

wzrost
źródło
2

W przypadku zmiany kodu, a jeszcze przejść testy, potem jest albo coś nie tak z badań (tj brakuje twierdzenie), czy kod nie faktycznie zmienić.

Co mam przez to na myśli? Twoje testy opisują umowy między częściami twojego kodu. Jeśli kontraktem jest „wartość zwracana musi być iterowalna”, wówczas zmiana wartości zwracanej z powiedzenia tablicy na listę nie jest w rzeczywistości zmianą umowy, a zatem niekoniecznie spowoduje niepowodzenie testu.

W celu uniknięcia brakujących twierdzeń, można użyć narzędzi, takich jak analiza pokrycia kodu (nie powie, jakie części kodu są testowane, ale to będzie powiedzieć, jakie części na pewno nie są testowane), cyclomatic złożoności i skomplikowania NPath (co daje dolną granicę ilości twierdzeń wymagany do osiągnięcia pełnej C1 i C2 pokrycia kodu) oraz mutacji testerów (który wprowadza mutacje w kodzie, takie jak toczenie truesię false, liczb ujemnych na dodatnie, przedmiotów do nullitd. i sprawdzić, czy powoduje, że testy kończą się niepowodzeniem).

Jörg W Mittag
źródło
+1 Coś nie tak z testami lub kodem tak naprawdę się nie zmieniło. Zajęło mi to trochę czasu, aby przyzwyczaić się po latach C ++ / Java. Umowa między częściami w dynamicznych językach kodu nie powinna być tym, CO jest zwracane, ale co zawiera ta zwracana rzecz i co może zrobić.
MrFox,
@suslik: W rzeczywistości nie chodzi tu o języki statyczne vs. dynamiczne, ale raczej o abstrakcję danych przy użyciu abstrakcyjnych typów danych vs. abstrakcyjną orientację obiektową. Zdolność jednego obiektu do symulacji innego obiektu, o ile zachowuje się nierozróżnialny (nawet jeśli są one całkowicie różnych typów i wystąpień zupełnie różnych klas) jest fundamentem całej idei OO. Albo inaczej: jeśli dwa obiekty zachowują się tak samo, są tego samego typu, niezależnie od tego, co mówią ich klasy. Mówiąc jeszcze inaczej: klasy nie są typami. Niestety, Java i C # źle to rozumieją.
Jörg W Mittag
Zakładając, że wyśmiewana jednostka jest w 100% objęta i nie brakuje żadnego stwierdzenia: co powiesz na bardziej subtelną zmianę, np. „Powinien zwracać null dla foo” na „powinien zwracać pusty ciąg dla foo”. Jeśli ktoś zmieni tę jednostkę i jej test, niektóre zużywające się jednostki mogą się zepsuć i z powodu kpiny jest to przezroczyste. (I nie: w momencie pisania lub używania wyśmiewanego modułu, zgodnie z „kontraktem”, nie trzeba traktować pustych ciągów jako wartości zwracanej, ponieważ ma ono zwracać niepuste ciągi lub tylko wartość zerową;)). (Tylko właściwe testy integracji obu modułów w interakcji mogłyby to wychwycić.)
spróbuj złapać w końcu