Zanurkuj w Pythonie/Testowanie niepoprawnych przypadków

Testowanie funkcji w sytuacji, w której na wejściu pojawiają się wyłącznie poprawne wartości, nie jest wystarczające; należy dodatkowo sprawdzić, że funkcja kończy się niepowodzeniem, gdy otrzymuje ona niepoprawne dane wejściowe. Nie może to być jednak dowolne niepowodzenie; musi być ono dokładnie takie, jakiego się spodziewamy.

Przypomnijmy sobie pozostałe wymagania dotyczące funkcji toRoman:

2. Funkcja toRoman powinna kończyć się niepowodzeniem, gdy przekazana jest jej wartość spoza przedziału od 1 do 3999.
3. Funkcja toRoman powinna kończyć się niepowodzeniem, gdy przekazana jest jej wartość nie będąca liczbą całkowitą.

W języku Python funkcje kończą się niepowodzeniem wówczas, gdy rzucają wyjątki. W module unittest znajdują się natomiast metody, dzięki którym można wykryć, czy funkcja, otrzymawszy niepoprawne dane wejściowe, rzuca odpowiedni wyjątek:

Przykład 13.3. Testowanie niepoprawnych danych wejściowych do funkcji toRoman

class ToRomanBadInput(unittest.TestCase):                            
    def testTooLarge(self):                                          
        """toRoman should fail with large input"""                   
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, 4000)        #(1)

    def testZero(self):                                              
        """toRoman should fail with 0 input"""                       
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, 0)           #(2)

    def testNegative(self):                                          
        """toRoman should fail with negative input"""                
        self.assertRaises(roman.OutOfRangeError, roman.toRoman, -1)  

    def testNonInteger(self):                                        
        """toRoman should fail with non-integer input"""             
        self.assertRaises(roman.NotIntegerError, roman.toRoman, 0.5)         #(3)


  1. Klasa TestCase z modułu unittest udostępnia metodę assertRaises, która przyjmuje następujące argumenty: wyjątek, którego się spodziewamy, funkcję, którą testujemy oraz argumenty, które mają być przekazane do funkcji (jeśli testowana funkcja przyjmuje więcej niż jeden argument, należy je wszystkie przekazać po kolei do funkcji assertRaises, która przekaże je w tej właśnie kolejności do testowanej funkcji). Zwróćcie baczną uwagę na to, co tutaj robimy: zamiast ręcznego wywoływania funkcji i sprawdzania, czy został rzucony wyjątek odpowiedniego typu (poprzez otoczenie wywołania blokiem try...except), używamy funkcji assertRaises, która robi to wszystko za nas. Wszystko, co należy zrobić, to przekazać typ wyjątku (roman.OutOfRangeError), funkcję (toRoman) oraz jej argument (4000), a assertRaises zajmie się wywołaniem toRoman z przekazanym parametrem oraz sprawdzeniem, czy rzucony wyjątek to rzeczywiście roman.OutOfRangeError. (Zauważmy również, że do funkcji assertRaises przekazujemy funkcję toRoman jako parametr; nie wywołujemy jej ani nie przekazujemy jej nazwy w postaci napisu. Czy wspominałem ostatnio, jak bardzo przydatne jest to, że w języku Python wszystko jest obiektem, włączając w to funkcje i wyjątki?)
  2. Oprócz przetestowania wartości zbyt dużych należy też przetstować wartości zbyt małe. Pamiętajmy, że w zapisie rzymskim nie można wyrazić ani wartości 0, ani liczb ujemnych, więc dla każdej z tych sytuacji mamy przypadek testowy (testZero i testNegative). W funkcji testZero sprawdzamy, czy toRoman rzuca wyjątek roman.OutOfRangeError, gdy jest wywołana z wartością 0; jeśli nie rzuci tego wyjątku (zarówno z powodu zwrócenia pewnej wartości jak i rzucenia jakiegoś innego wyjątku), test powinien zakończyć się niepowodzeniem.
  3. Wymaganie #3 określa, że toRoman nie może przyjąć jako danych wejściowych liczb niecałkowitych, więc tutaj upewniamy się, że dla wartości 0.5 toRoman rzuci wyjątek roman.NotIntegerError. Jeśli toRoman nie rzuci takiego wyjątku, test ten powinien zakończyć się niepowodzeniem.

Kolejne dwa wymagania są podobne do pierwszych trzech, przy czym odnoszą się one do funkcji fromRoman zamiast toRoman:

  1. Funkcja fromRoman powinna przyjmować napis będący poprawną liczbą w zapisie rzymskim i zwracać liczbę całkowitą, którą ten napis reprezentuje.
  2. Funkcja fromRoman powinna zakończyć się niepowodzeniem, gdy otrzyma na wejściu napis nie będący poprawną liczbą w zapisie rzymskim.

Wymaganie #4 jest obsługiwane w podobny sposób, jak wymaganie #1, poprzez iterowanie po zestawie znanych wartości i testowanie każdej z nich. Wymaganie #5 jest z kolei obsługiwane podobnie, jak wymagania #2 i #3, poprzez testowanie serii niepoprawnych ciągów wejściowych i sprawdzanie, czy fromRoman rzuca odpowiedni wyjątek.

Przykład 13.4. Testowanie niepoprawnych danych wejściowych do funkcji fromRoman

class FromRomanBadInput(unittest.TestCase):                                      
    def testTooManyRepeatedNumerals(self):                                       
        """fromRoman should fail with too many repeated numerals"""              
        for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):             
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)   #(1)

    def testRepeatedPairs(self):                                                 
        """fromRoman should fail with repeated pairs of numerals"""              
        for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):               
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)

    def testMalformedAntecedent(self):                                           
        """fromRoman should fail with malformed antecedents"""                   
        for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
                  'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):                       
            self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, s)
  1. Nie ma tu nic nowego do powiedzenia: wzorzec postępowania jest dokładnie taki sam jak w przypadku testowania niepoprawnego wejścia do funkcji toRoman. Zaznaczę tylko, że mamy teraz nieco inny wyjątek: roman.InvalidRomanNumeralError. Okazało się więc, że potrzebujemy trzech określonych przez nas wyjątków, które powinny zostać zdefiniowane w roman.py (wraz z roman.OutOfRangeError i roman.NotIntegerError). Kiedy już zajmiemy się implementacją roman.py w dalszej części tego rozdziału, dowiesz się, jak definiować własne wyjątki.