6

W naszym zespole programistów JavaScriptu zastosowaliśmy redux/reagujemy na styl pisania czystego kodu funkcjonalnego. Wydaje się jednak, że mamy problem z testowaniem naszego kodu przez jednostkę. Rozważmy następujący przykład:Jak przetestować drzewo czystych wywołań funkcji w izolacji?

function foo(data) { 
    return process({ 
     value: extractBar(data.prop1), 
     otherValue: extractBaz(data.prop2.someOtherProp) 
    }); 
} 

Ta funkcja połączenia zależy od wezwań do process, extractBar i extractBaz, z których każda może wywołać inne funkcje. Razem mogą wymagać nietrywialnej próby dla parametru data do skonstruowania do testowania.

Jeśli zaakceptujemy konieczność stworzenia takiego symulowanego obiektu i faktycznie zrobimy to w testach, szybko stwierdzimy, że mamy przypadki testowe, które są trudne do odczytania i utrzymania. Co więcej, najprawdopodobniej prowadzi to do testowania tego samego w kółko, ponieważ prawdopodobnie należy również napisać testy jednostkowe dla process, extractBar i extractBaz. Testowanie każdego możliwego przypadku krawędzi zaimplementowanego przez te funkcje za pomocą interfejsu foo jest niewygodne.


Mamy kilka rozwiązań na uwadze, ale tak naprawdę nie lubimy, ponieważ żaden z nich nie wydaje się być wzorem, który wcześniej widzieliśmy.

Rozwiązanie 1:

function foo(data, deps = defaultDeps) { 
    return deps.process({ 
     value: deps.extractBar(data.prop1), 
     otherValue: deps.extractBaz(data.prop2.someOtherProp) 
    }); 
} 

Rozwiązanie 2:

function foo(
    data, 
    processImpl = process, 
    extractBarImpl = extractBar, 
    extractBazImpl = extractBaz 
) { 
    return process({ 
     value: extractBar(data.prop1), 
     otherValue: extractBaz(data.prop2.someOtherProp) 
    }); 
} 

Rozwiązanie 2 zanieczyszcza foo metoda podpis bardzo szybko, jak liczba połączeń wzrasta funkcją zależną.

Rozwiązanie 3:

Wystarczy zaakceptować fakt, że foo jest skomplikowana operacja związek i przetestować go jako całość. Obowiązują wszystkie wady.


Proszę sugerować inne możliwości. Wyobrażam sobie, że jest to problem, który społeczność programowania funkcjonalnego musiała rozwiązać w taki czy inny sposób.

Odpowiedz

6

Prawdopodobnie nie potrzebujesz żadnego z rozważanych rozwiązań. Jedną z różnic między programowaniem funkcjonalnym a programowaniem imperatywnym jest to, że styl funkcjonalny powinien wytwarzać kod, który łatwiej jest zrozumieć. Nie tylko w sensie umysłowego "grania kompilatora" i symulowania tego, co stałoby się z danym zestawem danych wejściowych, ale także rozumowania na temat twojego kodu w bardziej matematycznym znaczeniu.

Na przykład celem testów jednostkowych jest sprawdzenie "wszystkiego, co może się zepsuć". Patrząc na pierwszy opublikowany fragment kodu, możemy zastanowić się nad funkcją i zapytać: "Jak ta funkcja może się zepsuć?" Jest to dość prosta funkcja, że ​​wcale nie musimy odtwarzać kompilatora. Możemy po prostu powiedzieć, że funkcja się zepsułaby, gdyby funkcja process() nie zwróciła poprawnej wartości dla danego zestawu danych wejściowych, tj. Jeśli zwróciło nieprawidłowy wynik lub zwróciło wyjątek. To z kolei oznacza, że ​​musimy również sprawdzić, czy extractBar() i extractBaz() zwróci poprawne wyniki, aby przekazać prawidłowe wartości do process().

Tak naprawdę, trzeba tylko sprawdzić, czy foo() rzuca nieoczekiwane wyjątki, bo wszystko co robi to wezwanie process() i powinno być testowanie process() we własnym zestawem testów jednostkowych. To samo z extractBar() i extractBaz(). Jeśli te dwie funkcje zwrócą poprawne wyniki po podaniu prawidłowych danych wejściowych, będą przekazywać poprawne wartości do process(), a jeśli process() wygeneruje poprawne wyniki po podaniu prawidłowych danych wejściowych, wówczas foo() również zwróci prawidłowe wyniki.

Możesz powiedzieć: "A co z argumentami? A co, jeśli wyciągnie nieprawidłową wartość ze struktury data?" Ale czy to naprawdę może pęknąć? Jeśli spojrzymy na funkcję, używamy notacji kropkowej JS, aby uzyskać dostęp do właściwości obiektu. Nie testujemy podstawowej funkcjonalności samego języka w naszych testach jednostkowych dla naszej aplikacji. Możemy po prostu spojrzeć na kod, powód, dla którego wyodrębnia on wartości na podstawie zakodowanego dostępu do właściwości obiektu i kontynuować nasze inne testy.

Nie oznacza to, że można po prostu wyrzucić testy jednostkowe, ale wielu doświadczonych programistów okaże się, że potrzebują o wiele mniej testów, ponieważ wystarczy przetestować rzeczy, które mogą się zepsuć, i programowanie funkcjonalne. redukuje liczbę łamliwych rzeczy, dzięki czemu możesz skupić się na testach na częściach, które naprawdę są zagrożone.

A tak na marginesie, jeśli pracujesz na złożonych danych i obawiasz się, że nawet w przypadku FP możesz mieć problem ze wszystkimi możliwymi permutacjami, możesz chcieć przyjrzeć się testom generatywnym. Myślę, że jest tam kilka bibliotek JS.