Zanurkuj w Pythonie/Programowanie funkcyjne - wszystko razem
Dowiedzieliście się już wystarczająco dużo, by móc odczytać pierwszych siedem linii kodu z przykładu podanego na początku rozdziału, w którym odczytywane są pliki w katalogu a następnie importowane wybrane spośród nich moduły.
Przykład 16.16. Funkcja regressionTest
def regressionTest():
path = os.path.abspath(os.path.dirname(sys.argv[0]))
files = os.listdir(path)
test = re.compile("test\.py$", re.IGNORECASE)
files = filter(test.search, files)
filenameToModuleName = lambda f: os.path.splitext(f)[0]
moduleNames = map(filenameToModuleName, files)
modules = map(__import__, moduleNames)
load = unittest.defaultTestLoader.loadTestsFromModule
return unittest.TestSuite(map(load, modules))
Spójrzmy na ten kod w sposób interaktywny, linia po linii. Załóżmy, że katalogiem bieżącym jest c:\diveintopython\py, w którym znajdują się przykłady dołączone do tej książki, z omawianym w tym rozdziale skryptem włącznie. Jak widzieliśmy w podrozdziale 16.2 Znajdowanie ścieżki, nazwa katalogu, w którym znajduje się skrypt, trafi do zmiennej path, spróbujmy więc tę część zakodować na sztywno, po czym dopiero przejść dalej.
Przykład 16.17. Krok 1: Pobieranie wszystkich plików
>>> import sys, os, re, unittest >>> path = r'c:\diveintopython\py' >>> files = os.listdir(path) >>> files #(1) ['BaseHTMLProcessor.py', 'LICENSE.txt', 'apihelper.py', 'apihelpertest.py', 'argecho.py', 'autosize.py', 'builddialectexamples.py', 'dialect.py', 'fileinfo.py', 'fullpath.py', 'kgptest.py', 'makerealworddoc.py', 'odbchelper.py', 'odbchelpertest.py', 'parsephone.py', 'piglatin.py', 'plural.py', 'pluraltest.py', 'pyfontify.py', 'regression.py', 'roman.py', 'romantest.py', 'uncurly.py', 'unicode2koi8r.py', 'urllister.py', 'kgp', 'plural', 'roman', 'colorize.py']
- files jest listą nazw wszystkich plików i katalogów znajdujących się w katalogu, z którego pochodzi skrypt. (Jeśli wcześniej uruchamialiście już jakieś przykłady, na liście możecie również zauważyć pliki .pyc)
Przykład 16.18. Krok 2: Filtrowanie w celu znalezienia interesujących plików
>>> test = re.compile("test\.py$", re.IGNORECASE) #(1) >>> files = filter(test.search, files) #(2) >>> files #(3) ['apihelpertest.py', 'kgptest.py', 'odbchelpertest.py', 'pluraltest.py', 'romantest.py']
- Wyrażenie regularne zostanie dopasowane do każdego napisu zakończonego na test.py. Zauważcie, że kropka musi zostać poprzedzona sekwencją unikową; w wyrażeniach regularnych kropka oznacza "dopasuj dowolny znak", jednak nam zależy na dosłownym dopasowaniu znaku kropki.
- Skompilowane wyrażenie regularne działa jak funkcja, a więc możemy jej użyć do przefiltrowania długiej listy nazw plików i katalogów, dzięki czemu uzyskamy listę nazw, do których zostało dopasowane wyrażenie.
- Otrzymaliśmy więc listę skryptów będących testami jednostkowymi, ponieważ tylko one mają nazwę JAKASNAZWAtest.py.
Przykład 16.19. Krok 3: Odwzorowanie nazw plików na nazwy modułów
>>> filenameToModuleName = lambda f: os.path.splitext(f)[0] #(1) >>> filenameToModuleName('romantest.py') #(2) 'romantest' >>> filenameToModuleName('odchelpertest.py') 'odbchelpertest' >>> moduleNames = map(filenameToModuleName, files) #(3) >>> moduleNames #(4) ['apihelpertest', 'kgptest', 'odbchelpertest', 'pluraltest', 'romantest']
- Jak widzieliśmy w podrozdziale 4.7 Wyrażenia lambda, lambda pozwala na szybkie zdefiniowanie jednolinijkowych funkcji w locie. Tutaj funkcja lambda pobiera nazwę pliku wraz z rozszerzeniem i zwraca część nazwy bez rozszerzenia, używając do tego funkcji os.path.splitext z biblioteki standardowej, którą poznaliśmy w przykładzie 6.17 "Rozdzielanie ścieżek" w podrozdziale Praca z katalogami.
- filenameToModuleName jest funkcją. W porównaniu ze zwykłymi funkcjami, które tworzy się przy pomocy instrukcji def, w funkcjach lambda nie ma niczego magicznego. Możemy wywołać filenameToModuleName jak każdą inną funkcję, a robi ona dokładnie to, czego potrzebujemy: odcina rozszerzenie z napisu przekazanego jej w parametrze wejściowym.
- Tutaj możemy wywołać tę funkcję na każdej nazwie pliku znajdującej się na liście plików będących testami jednostkowymi. Używamy do tego funkcji map.
- W wyniku otrzymujemy to, czego oczekiwaliśmy: listę nazw modułów.
Przykład 16.20. Krok 4: Odwzorowanie nazw modułów na moduły
>>> modules = map(__import__, moduleNames) #(1) >>> modules #(2) [<module 'apihelpertest' from 'apihelpertest.py'>, <module 'kgptest' from 'kgptest.py'>, <module 'odbchelpertest' from 'odbchelpertest.py'>, <module 'pluraltest' from 'pluraltest.py'>, <module 'romantest' from 'romantest.py'>] >>> modules[-1] #(3) <module 'romantest' from 'romantest.py'>
- Jak widzieliśmy w podrozdziale 16.6 Dynamiczne importowanie modułów, w celu odwzorowania listy nazw modułów (napisów) na właściwe moduły (obiekty wywoływalne, do których można mieć dostęp jak do jakichkolwiek innych modułów), można użyć funkcji map oraz __import__.
- modules to teraz lista modułów, do których można mieć taki sam dostęp, jak do jakichkolwiek innych modułów.
- Ostatnim modułem na liście jest moduł romantest, tak jak gdybyśmy napisali: import romantest
Przykład 16.21. Krok 5: Ładowanie modułów do zestawu testów
>>> load = unittest.defaultTestLoader.loadTestsFromModule >>> map(load, modules) #(1) [<unittest.TestSuite tests=[ <unittest.TestSuite tests=[<apihelpertest.BadInput testMethod=testNoObject>]>, <unittest.TestSuite tests=[<apihelpertest.KnownValues testMethod=testApiHelper>]>, <unittest.TestSuite tests=[ <apihelpertest.ParamChecks testMethod=testCollapse>, <apihelpertest.ParamChecks testMethod=testSpacing>]>, ... ] ] >>> unittest.TestSuite(map(load, modules)) #(2)
- To są prawdziwe obiekty modułów. Nie tylko mamy do nich dostęp taki, jak do innych modułów i możemy tworzyć instancje klas oraz wywoływać funkcje, mamy również możliwość introspekcji (wglądu) w moduł, której możemy użyć przede wszystkim do tego, aby dowiedzieć się, jakie klasy i funkcje dany moduł posiada. To właśnie robi metoda loadTestsFromModule: dokonuje introspekcji, a następnie dla każdego modułu zwraca obiekt unittest.TestSuite. Każdy taki obiekt zawiera listę obiektów unittest.TestSuite, po jednym dla każdej klasy dziedziczącej po TestCase zdefiniowanej w module. Każdy z obiektów na tej liście zawiera z kolei listę metod testowych zdefiniowanych w klasie testowej.
- Na końcu umieszczamy listę obiektów TestSuite wewnątrz jednego, dużego zestawu testów. Moduł unittest nie ma problemów z przechodzeniem po drzewie zestawów testowych zagnieżdżonych w zestawach testowych; dotrze on do każdej metody testowej i ją wywoła, sprawdzając, czy przeszła, czy nie, a następnie przejdzie do kolejnej metody.
Moduł unittest zwykle przeprowadza za nas proces introspekcji. Czy pamiętacie magiczną funkcję unittest.main(), którą wywoływały poszczególne moduły, aby odpalić wszystkie znajdujące się w nich testy? Metoda unittest.main() w rzeczywistości tworzy instancję klasy unittest.TestProgram, która z kolei tworzy instancję unittest.defaultTestLoader, służącą do załadowania modułu, z którego została wywołana. (Skąd jednak ma referencję do modułu, z którego została wywołana, jeśli nie dostała jej od nas? Otóż dzięki równie magicznemu poleceniu __import__('__main__'), które dynamicznie importuje wykonywany właśnie moduł. Mógłbym napisać całą książkę na temat trików i technik używanych w module unittest, ale chyba bym jej nie skończył.
Przykład 16.22. Krok 6: Powiedzieć modułowi unittest, aby użył naszego zestawu testowego
if __name__ == "__main__":
unittest.main(defaultTest="regressionTest") #(1)
- Zamiast pozwalać modułowi unittest wykonać całą magię za nas, większość zrobiliśmy sami. Utworzyliśmy funkcję (regressionTest), która sama importuje moduły i woła unittest.defaultTestLoader, a następnie utworzyliśmy duży zestaw testów. Teraz potrzebujemy jedynie, aby unittest, zamiast szukać testów i budować zestaw testowy w standardowy sposób, uruchomił funkcję regressionTest, która zwróci gotwy do użycia obiekt testSuite.