Valid XHTML 1.0 Transitional
Unit Testing, a tworzenie aplikacji w Pythonie.
Witam w tym artykule postaram się omówić zagadnienie unit testingu.

Co to w ogóle jest?
Unit testing jest to technika pisania i sprawdzania poprawności kodu, polegająca na pisaniu programu który testuje nasz program.

No dobra fajnie, ale po co mi to?
Korzyści wynikających z takiego programowania jest mnóstwo. Wypiszę kilka:
- Łatwiej jest pisać program kiedy wiemy dokładnie co ma robić.
- Podczas pisania podążamy po określonej ścieżce nie tracimy czasu na robienie czegoś co w tym momencie jest nie potrzebne.
- Podczas modyfikowania programu oszczędzamy bardzo dużo czasu który byśmy musieli przeznaczyć na normalne testowanie. Przykład piszesz moduł do przechowywania i weryfikacji użytkowników. Przychodzi taki moment, że musisz zmodyfikować strukturę bazy danych to ją modyfikujesz, odpalasz testy i wiesz czy ta zmiana nie miała negatywnego wpływu na działanie programu.
- Przy pracy zespołowej kiedy masz poprawić kogoś program albo coś dodać masz jego testy. Nie musisz się martwić, że jak coś zmienisz to program nie będzie spełniał założeń.
- Przy rozwijaniu programu pozwala wyłapać dużo więcej błędów i w dużo krótszym czasie, co nie jest możliwe przy normalnym testowaniu.

Podsumowując dzięki unit testom zaoszczędzamy dużo czasu i nerwów. Jeszcze więcej zalet i korzyści dostrzeżesz kiedy sam spróbujesz.

Zainteresowało Cię to? Jeśli tak to zapraszam do dalszej lektury.

Dobra wstęp mamy za sobą uff:D Teraz zajmijmy się sednem sprawy. Jak wygląda programowanie z wykorzystaniem unit testów.

Zaczynamy od określenia, założeń jakie ma spełniać program.
Później piszemy testy. Dobre napisanie testów zajmuje trochę czasu, ale jest kluczem do sukcesu.
Kiedy mamy już testy dopiero zaczynamy pisać program.

Teraz pewnie trochę Cię to dziwi. Zaraz weźmiemy się za pisanie przykładowego programu to wszystko powinno stać się jasne. Nim to nastąpi napisze jeszcze tylko jak to się ma w pytonie.

Python oferuje nam do tego specjalny moduł "unittest". Zaraz zobaczysz jak go używać.

Ok zaczynamy. Na potrzeby tego artykuły wymyśliłem prosty program w rzeczywistości jest on bezużyteczny, ale tutaj bardzo dobrze sprawdzi się w roli edukacyjnej.

Będzie to prosty program szyfrujący i deszyfrujący liczby. Nazwiemy go np mobdigcode.

Szyfrować będziemy każdą cyfrę ciągiem liter które znajdują się np. na klawiaturze komórki na klawiszu z tą cyfrą np.
2 to będzie abc
3 to będzie def
dla 1 damy ciąg "vobo" - od voice box
dla 0 damy ciąg "spac" - od spacji.

Jakie założenia musi spełniać nasz program.
1. Szyfrowanie i deszyfrowanie powinno być wykonane dla poprawnych danych, dla niepoprawnych powinien być błąd.
2. Funkcja deszyfrująca przyjmuję i zwraca tylko małe litery.

Dobra kiedy mamy ogólne założenia skupmy się nad szczegółami. Nasz program będzie posiadał dwie funkcje code i decode.

Funkcja code:
Powinna zwrócić zaszyfrowany ciąg dla danego ciągu cyfr.
Powinna zwrócić zaszyfrowany ciąg składający się z małych liter.
Powinna zwrócić błąd jeśli ciąg nie składa się z cyfr.
Powinna zwrócić błąd jeśli wejście jest puste.

Funkcja decode:
Powinna zwrócić rozszyfrowany ciąg dla poprawnego wejścia.
Powinna zwrócić ciąg składający się z cyfr.
Powinna zwrócić błąd dla niepoprawnego wejścia np. "acbdfe"
Powinna zwrócić błąd kiedy wejście nie jest tekstem.
Powinna zwrócić błąd kiedy w wejściu występują duże litery.
Powinna zwrócić błąd jeśli wejście jest puste.

Kiedy już mamy określone jak nasz program ma działać zaczynamy pisać testy.
Poniżej znajduję się cały program testujący.

mobdigcodetest.py
#!/usr/bin/python
# -*- coding: iso-8859-2 -*-

"""Unit test for mobdigcode.py"""

import mobdigcode
import unittest

#1
class ZnaneWartosci(unittest.TestCase):
#2
	znaneWartosci = (
		("0", "spac"),
		("1", "vobo"),
		("2", "abc"),
		("3", "def"),
		("4", "ghi"),
		("5", "jkl"),
		("6", "mno"),
		("7", "pqrs"),
		("8", "tuv"),
		("9", "wxyz"),
		("10", "vobospac"),
		("55", "jkljkl"),
		("111", "vobovobovobo"),
		("666", "mnomnomno"),
		("12345", "voboabcdefghijkl"),
	)

#3
	def testCode(self):
		"""code powinna zwrócić poprawną wartość dla poprawnego wejścia"""
		for liczba, szyfr in self.znaneWartosci:
#4
			wynik = mobdigcode.code(liczba)
#5
			self.assertEqual(szyfr, wynik)
#6
	def testDecode(self):
		"""decode powinna zwrócić poprawną wartość dla poprawnego wejścia"""
		for liczba, szyfr in self.znaneWartosci:
			wynik = mobdigcode.decode(szyfr)
			self.assertEqual(liczba, wynik)

#7
class CodeZleWejscie(unittest.TestCase):
	def testNonDigits(self):
		"""code powinna zwrócić błąd jeśli wejście nie jest cyframi"""
#8
		self.assertRaises(mobdigcode.BadInput, mobdigcode.code, "abcdefghijklmnopqrstuvxyz")
	
	def testBlank(self):
		"""code powinna zwrócić błąd jeśli wejście jest puste"""
		self.assertRaises(mobdigcode.BadInput, mobdigcode.code, "")

#9
class DecodeZleWejscie(unittest.TestCase):
	def testInvalidString(self):
		"""decode powinna zwrócić błąd jeśli wejście jest nie poprawne"""
		self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "acbdfe")
	
	def testDigits(self):
		"""decode powinna zwrócić błąd jeśli wejście zawiera cyfry"""
		self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "abc3def")

	def testBlank(self):
		"""decode powinna zwrócić błąd jeśli wejście jest puste"""
		self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "")

#10
class TestRownowaznosci(unittest.TestCase):
	def testSanity(self):
		"""decode(code(s)) == s dla wszystkich s"""
		for liczba in range(0, 5000):
			liczba = str(liczba)
			szyfr = mobdigcode.code(liczba)
			wynik = mobdigcode.decode(szyfr)
			self.assertEqual(liczba, wynik)

#11
class TestWielkosciLiter(unittest.TestCase):
	def testUpperCase(self):
		"""decode powinna zwrócić błąd jeśli wejście zawiera duże litery"""
		self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "ABCDEF")
	
	def testCodeCase(self):
		"""code powinna zwrócić ciąg małych liter"""
		for liczba in range(0,5000):
			szyfr = mobdigcode.code(liczba)
			self.assertEqual(szyfr, szyfr.lower())

if __name__ == "__main__":
	unittest.main()

Jak powinny wyglądać poprawne testy:
- powinny się wykonywać samodzielnie bez ingerencji człowieka (człowiek podczas testowania nie podaje wartości z klawiatury)
- powinny działać niezależnie od tego czy funkcja wykona się dobrze, czy zwróci błąd bez ingerencji człowieka

Dobra teraz komentujemy skomentujmy te testy.
#1 W naszych klasach testujących dziedziczymy klasę unittest.TestCase która dodaje nam dużo przydatnych funkcji.

#2 Lista liczb i odpowiadających im szyfrów. Wiadome nie dajemy tutaj wszystkich możliwości bo to jest nie możliwe, dajemy tylko przykładowe i takie o których myślimy, że mogą sprawiać problemy. Tutaj dajemy tylko poprawne wartości.

#3 Każdy test jest metodą która nie przyjmuję i nie zwraca żadnych wartości. Jeśli metoda zakończy się sukcesem test jest zaliczony, jeśli zakończy się wyjątkiem test jest nie zaliczony.

#4 Tutaj odwołujemy się do funkcji code. Nie jest ona jeszcze napisana.

#5 Sprawdzamy, czy po podaniu poprawnej wartości funkcja zwróci poprawny szyfr. Do porównywania wartości używamy metody assertEqual(parram1, parram2)

#6 Analogiczny test dla funkcji decode

Nie wystarczy sprawdzać dla dobrego wejścia. Musimy sprawdzać również dla złego.

#7 Klasa w której umieścimy metody do sprawdzania złego wejścia dla funkcji code.

#8 Sprawdzamy, czy podając wejście nie będące cyframi, funkcja zwróci nam błąd.

#9 Analogicznie, dla funkcji decode.

#10 Teraz sprawdzamy równoważność czyli czy decode(code(s)) == s

#11 Tutaj sprawdzamy wejście i wyjście funkcji pod względem wielkich liter.

O czym należy pamiętać.
- Zawsze testujemy program dla poprawnego wejścia.
- Zawsze testujemy program dla niepoprawnego.
- Gdy jest to możliwe testujemy równoważność. Np. przy funkcja typu fromKelvin(toKelvin(n)) == n, rot13(rot13(s)) == s etc.

Dobra testy mamy napisane. Teraz przechodzimy do pisania programu.

Pierwsza faza. Piszemy tylko szkielet programu.

mobdigcode.py
#!/usr/bin/python
# -*- coding: iso-8859-2 -*-

# Definicja wyjątków
#1
class MobdigcodeError(Exception):
	pass

#2
class BadInput(MobdigcodeError):
	pass

def code(s):
	pass

def decode(s):
	pass

#1 Definicja twojej własnej kalsy wyjątków w której możesz zdefiniować jak mają wyglądać wyjątki według własnych preferencji, jest to zalecane, ale nie wymagane.

#2 Klasa która będzie zwracała wyjątki przy złych wejściach.

W pierwszej fazie piszemy tylko definicje klas i funkcji nie wypelniamy ich kodem stąd te wszystkie pass.

Najwyższa pora odpalić nasz pierwszy test.
Uruchamiamy przez python mobdigcodetest.py -v
Tutaj output:
code powinna zwrócić błąd jeśli wejście jest puste ... FAIL
code powinna zwrócić błąd jeśli wejście nie jest cyframi ... FAIL
decode powinna zwrócić błąd jeśli wejście jest puste ... FAIL
decode powinna zwrócić błąd jeśli wejście zawiera cyfry ... FAIL
decode powinna zwrócić błąd jeśli wejście jest nie poprawne ... FAIL
decode(code(s)) == s dla wszystkich s ... FAIL
code powinna zwrócić ciąg małych liter ... ERROR
decode powinna zwrócić błąd jeśli wejście zawiera duże litery ... FAIL
code powinna zwrócić poprawną wartość dla poprawnego wejścia ... FAIL
decode powinna zwrócić poprawną wartość dla poprawnego wejścia ... FAIL

======================================================================
ERROR: code powinna zwrócić ciąg małych liter
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 91, in testCodeCase
    self.assertEqual(szyfr, szyfr.lower())
AttributeError: 'NoneType' object has no attribute 'lower'

======================================================================
FAIL: code powinna zwrócić błąd jeśli wejście jest puste
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 55, in testBlank
    self.assertRaises(mobdigcode.BadInput, mobdigcode.code, "")
AssertionError: BadInput not raised

======================================================================
FAIL: code powinna zwrócić błąd jeśli wejście nie jest cyframi
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 51, in testNonDigits
    self.assertRaises(mobdigcode.BadInput, mobdigcode.code, "abcdefghijklmnopqrstuvxyz")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście jest puste
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 69, in testBlank
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście zawiera cyfry
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 65, in testDigits
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "abc3def")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście jest nie poprawne
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 61, in testInvalidString
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "acbdfe")
AssertionError: BadInput not raised

======================================================================
FAIL: decode(code(s)) == s dla wszystkich s
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 79, in testSanity
    self.assertEqual(liczba, wynik)
AssertionError: '0' != None

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście zawiera duże litery
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 85, in testUpperCase
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "ABCDEF")
AssertionError: BadInput not raised

======================================================================
FAIL: code powinna zwrócić poprawną wartość dla poprawnego wejścia
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 37, in testCode
    self.assertEqual(szyfr, wynik)
AssertionError: 'spac' != None

======================================================================
FAIL: decode powinna zwrócić poprawną wartość dla poprawnego wejścia
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 44, in testDecode
    self.assertEqual(liczba, wynik)
AssertionError: '0' != None

----------------------------------------------------------------------
Ran 10 tests in 0.003s

FAILED (failures=9, errors=1)

Jak widzimy wszystkie testy nie powiodły się i o to chodziło. Testy muszą być tak napisane, żeby w tej fazie się nie powiodły.

Przejdźmy do drugiej fazy czyli kodzenia funkcji code.

mobdigcode.py
#!/usr/bin/python
# -*- coding: iso-8859-2 -*-

import re

# Definicja wyjątków
class MobdigcodeError(Exception):
	pass

class BadInput(MobdigcodeError):
	pass

def code(s):
	s = str(s)
	s = re.sub("0", "spac", s)
	s = re.sub("1", "vobo", s)
	s = re.sub("2", "abc", s)
	s = re.sub("3", "def", s)
	s = re.sub("4", "ghi", s)
	s = re.sub("5", "jkl", s)
	s = re.sub("6", "mno", s)
	s = re.sub("7", "pqrs", s)
	s = re.sub("8", "tuv", s)
	s = re.sub("9", "wxyz", s)

	return s

def decode(s):
	pass

W drugiej fazie skupiamy sie na jednej funkcji.

Output testu:
code powinna zwrócić błąd jeśli wejście jest puste ... FAIL
code powinna zwrócić błąd jeśli wejście nie jest cyframi ... FAIL
decode powinna zwrócić błąd jeśli wejście jest puste ... FAIL
decode powinna zwrócić błąd jeśli wejście zawiera cyfry ... FAIL
decode powinna zwrócić błąd jeśli wejście jest nie poprawne ... FAIL
decode(code(s)) == s dla wszystkich s ... FAIL
code powinna zwrócić ciąg małych liter ... ok
decode powinna zwrócić błąd jeśli wejście zawiera duże litery ... FAIL
code powinna zwrócić poprawną wartość dla poprawnego wejścia ... ok
decode powinna zwrócić poprawną wartość dla poprawnego wejścia ... FAIL

======================================================================
FAIL: code powinna zwrócić błąd jeśli wejście jest puste
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 55, in testBlank
    self.assertRaises(mobdigcode.BadInput, mobdigcode.code, "")
AssertionError: BadInput not raised

======================================================================
FAIL: code powinna zwrócić błąd jeśli wejście nie jest cyframi
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 51, in testNonDigits
    self.assertRaises(mobdigcode.BadInput, mobdigcode.code, "abcdefghijklmnopqrstuvxyz")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście jest puste
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 69, in testBlank
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście zawiera cyfry
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 65, in testDigits
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "abc3def")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście jest nie poprawne
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 61, in testInvalidString
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "acbdfe")
AssertionError: BadInput not raised

======================================================================
FAIL: decode(code(s)) == s dla wszystkich s
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 79, in testSanity
    self.assertEqual(liczba, wynik)
AssertionError: '0' != None

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście zawiera duże litery
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 85, in testUpperCase
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "ABCDEF")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić poprawną wartość dla poprawnego wejścia
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 44, in testDecode
    self.assertEqual(liczba, wynik)
AssertionError: '0' != None

----------------------------------------------------------------------
Ran 10 tests in 0.272s

FAILED (failures=8)

Jak widzimy dla funkcji code już dwa testy są spełnione pozostał nam tylko test na błędny input. No to kodzimy.

mobdigcode.py
#!/usr/bin/python
# -*- coding: iso-8859-2 -*-

import re

# Definicja wyjątków
class MobdigcodeError(Exception):
	pass

class BadInput(MobdigcodeError):
	pass

def code(s):
	s = str(s)

#1
	if not re.search("^\d+$", s):
		raise BadInput, 'Złe wejście funkcja code przyjmuje tylko ciąg składający się z cyfr'	

	s = re.sub("0", "spac", s)
	s = re.sub("1", "vobo", s)
	s = re.sub("2", "abc", s)
	s = re.sub("3", "def", s)
	s = re.sub("4", "ghi", s)
	s = re.sub("5", "jkl", s)
	s = re.sub("6", "mno", s)
	s = re.sub("7", "pqrs", s)
	s = re.sub("8", "tuv", s)
	s = re.sub("9", "wxyz", s)

	return s

def decode(s):
	pass

#1 ten fragment dodaliśmy. Przepuszcza on tylko wyrażenia, które składają się z samych cyfr.
code powinna zwrócić błąd jeśli wejście jest puste ... ok
code powinna zwrócić błąd jeśli wejście nie jest cyframi ... ok
decode powinna zwrócić błąd jeśli wejście jest puste ... FAIL
decode powinna zwrócić błąd jeśli wejście zawiera cyfry ... FAIL
decode powinna zwrócić błąd jeśli wejście jest nie poprawne ... FAIL
decode(code(s)) == s dla wszystkich s ... FAIL
code powinna zwrócić ciąg małych liter ... ok
decode powinna zwrócić błąd jeśli wejście zawiera duże litery ... FAIL
code powinna zwrócić poprawną wartość dla poprawnego wejścia ... ok
decode powinna zwrócić poprawną wartość dla poprawnego wejścia ... FAIL

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście jest puste
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 69, in testBlank
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście zawiera cyfry
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 65, in testDigits
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "abc3def")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście jest nie poprawne
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 61, in testInvalidString
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "acbdfe")
AssertionError: BadInput not raised

======================================================================
FAIL: decode(code(s)) == s dla wszystkich s
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 79, in testSanity
    self.assertEqual(liczba, wynik)
AssertionError: '0' != None

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście zawiera duże litery
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 85, in testUpperCase
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "ABCDEF")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić poprawną wartość dla poprawnego wejścia
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 44, in testDecode
    self.assertEqual(liczba, wynik)
AssertionError: '0' != None

----------------------------------------------------------------------
Ran 10 tests in 0.308s

FAILED (failures=6)
Jak widzimy funkcja code już spełnia założenia. Widzimy, że teraz nie spełnia założeń tylko funkcja decode. Czyli teraz bierzemy się, za pisanie jej. Ważne jest, żeby dobrze interpretować testy, żebyśmy wiedzieli co kiedy mamy pisać.

mobdigcode.py
#!/usr/bin/python
# -*- coding: iso-8859-2 -*-

import re

# Definicja wyjątków
class MobdigcodeError(Exception):
	pass

class BadInput(MobdigcodeError):
	pass

def code(s):
	s = str(s)
	
	if not re.search("^\d+$", s):
		raise BadInput, 'Złe wejście funkcja code przyjmuje tylko ciąg składający się z cyfr'	

	s = re.sub("0", "spac", s)
	s = re.sub("1", "vobo", s)
	s = re.sub("2", "abc", s)
	s = re.sub("3", "def", s)
	s = re.sub("4", "ghi", s)
	s = re.sub("5", "jkl", s)
	s = re.sub("6", "mno", s)
	s = re.sub("7", "pqrs", s)
	s = re.sub("8", "tuv", s)
	s = re.sub("9", "wxyz", s)

	return s

def decode(s):
	s = str(s)

	s = re.sub("spac", "0", s)
	s = re.sub("vobo", "1", s)
	s = re.sub("abc", "2", s)
	s = re.sub("def", "3", s)
	s = re.sub("ghi", "4", s)
	s = re.sub("jkl", "5", s)
	s = re.sub("mno", "6", s)
	s = re.sub("pqrs", "7", s)
	s = re.sub("tuv", "8", s)
	s = re.sub("wxyz", "9", s)

	return s

Test:
code powinna zwrócić błąd jeśli wejście jest puste ... ok
code powinna zwrócić błąd jeśli wejście nie jest cyframi ... ok
decode powinna zwrócić błąd jeśli wejście jest puste ... FAIL
decode powinna zwrócić błąd jeśli wejście zawiera cyfry ... FAIL
decode powinna zwrócić błąd jeśli wejście jest nie poprawne ... FAIL
decode(code(s)) == s dla wszystkich s ... ok
code powinna zwrócić ciąg małych liter ... ok
decode powinna zwrócić błąd jeśli wejście zawiera duże litery ... FAIL
code powinna zwrócić poprawną wartość dla poprawnego wejścia ... ok
decode powinna zwrócić poprawną wartość dla poprawnego wejścia ... ok

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście jest puste
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 69, in testBlank
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście zawiera cyfry
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 65, in testDigits
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "abc3def")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście jest nie poprawne
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 61, in testInvalidString
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "acbdfe")
AssertionError: BadInput not raised

======================================================================
FAIL: decode powinna zwrócić błąd jeśli wejście zawiera duże litery
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mobdigcodetest.py", line 85, in testUpperCase
    self.assertRaises(mobdigcode.BadInput, mobdigcode.decode, "ABCDEF")
AssertionError: BadInput not raised

----------------------------------------------------------------------
Ran 10 tests in 0.857s

FAILED (failures=4)
Jak widzimy funkcja decode działa musimy tylko zrobić wyjątki dla błędnych wejść.

mobdigcode.py
#!/usr/bin/python
# -*- coding: iso-8859-2 -*-

import re

# Definicja wyjątków
class MobdigcodeError(Exception):
	pass

class BadInput(MobdigcodeError):
	pass

def code(s):
	s = str(s)
	
	if not re.search("^\d+$", s):
		raise BadInput, 'Złe wejście funkcja code przyjmuje tylko ciąg składający się z cyfr'	

	s = re.sub("0", "spac", s)
	s = re.sub("1", "vobo", s)
	s = re.sub("2", "abc", s)
	s = re.sub("3", "def", s)
	s = re.sub("4", "ghi", s)
	s = re.sub("5", "jkl", s)
	s = re.sub("6", "mno", s)
	s = re.sub("7", "pqrs", s)
	s = re.sub("8", "tuv", s)
	s = re.sub("9", "wxyz", s)

	return s

def decode(s):
	s = str(s)

#1
	if not re.search("^(spac|vobo|abc|def|ghi|jkl|mno|pqrs|tuv|wxyz)+$",s):
		raise BadInput, 'Złe wejście funkcja decode przyjmuje tylko poprawny szyfr składający się z małych liter'

	s = re.sub("spac", "0", s)
	s = re.sub("vobo", "1", s)
	s = re.sub("abc", "2", s)
	s = re.sub("def", "3", s)
	s = re.sub("ghi", "4", s)
	s = re.sub("jkl", "5", s)
	s = re.sub("mno", "6", s)
	s = re.sub("pqrs", "7", s)
	s = re.sub("tuv", "8", s)
	s = re.sub("wxyz", "9", s)

	return s

#1 regexpy odpowiedzialny za sprawdzanie poprawności wejścia.

Test:
code powinna zwrócić błąd jeśli wejście jest puste ... ok
code powinna zwrócić błąd jeśli wejście nie jest cyframi ... ok
decode powinna zwrócić błąd jeśli wejście jest puste ... ok
decode powinna zwrócić błąd jeśli wejście zawiera cyfry ... ok
decode powinna zwrócić błąd jeśli wejście jest nie poprawne ... ok
decode(code(s)) == s dla wszystkich s ... ok
code powinna zwrócić ciąg małych liter ... ok
decode powinna zwrócić błąd jeśli wejście zawiera duże litery ... ok
code powinna zwrócić poprawną wartość dla poprawnego wejścia ... ok
decode powinna zwrócić poprawną wartość dla poprawnego wejścia ... ok

----------------------------------------------------------------------
Ran 10 tests in 0.893s

OK

No i wszystko działa:D Zamykamy programowanie pierwszej wersji. Jak widzisz testy prowadziły nas za rączkę co kiedy mamy zrobić.
Teraz nadchodzi czas na dalsze rozwijanie programu. Rozwijanie? Czynienie go bardziej wydajnym, estetyczniejszym dodaj swoje własne epitety.

Nasz program działa, ale specjalnie do celów edukacyjny napisałem go nie do końca dobrze. Te wyrażenia regularne jedne pod drugimi nie dość, że są nie estetyczne to jeszcze działają wolno. Ulepszy zatem nasz program.

mobdigcode.py
#!/usr/bin/python
# -*- coding: iso-8859-2 -*-

import re

# Definicja wyjątków
class MobdigcodeError(Exception):
	pass

class BadInput(MobdigcodeError):
	pass

codeMap = (
	("0", "spac"),
	("1", "vobo"),
	("2", "abc"),
	("3", "def"),
	("4", "ghi"),
	("5", "jkl"),
	("6", "mno"),
	("7", "pqrs"),
	("8", "tuv"),
	("9", "wxyz"),
)

def code(s):
	s = str(s)
	
	if not re.search("^\d+$", s):
		raise BadInput, 'Złe wejście funkcja code przyjmuje tylko ciąg składający się z cyfr'	

	for liczba, szyfr in codeMap:
		s = s.replace(liczba, szyfr)

	return s

def decode(s):
	s = str(s)

	if not re.search("^(spac|vobo|abc|def|ghi|jkl|mno|pqrs|tuv|wxyz)+$",s):
		raise BadInput, 'Złe wejście funkcja decode przyjmuje tylko poprawny szyfr składający się z małych liter'

	for liczba, szyfr in codeMap:
		s = s.replace(szyfr, liczba)

	return s

Test:
code powinna zwrócić błąd jeśli wejście jest puste ... ok
code powinna zwrócić błąd jeśli wejście nie jest cyframi ... ok
decode powinna zwrócić błąd jeśli wejście jest puste ... ok
decode powinna zwrócić błąd jeśli wejście zawiera cyfry ... ok
decode powinna zwrócić błąd jeśli wejście jest nie poprawne ... ok
decode(code(s)) == s dla wszystkich s ... ok
code powinna zwrócić ciąg małych liter ... ok
decode powinna zwrócić błąd jeśli wejście zawiera duże litery ... ok
code powinna zwrócić poprawną wartość dla poprawnego wejścia ... ok
decode powinna zwrócić poprawną wartość dla poprawnego wejścia ... ok

----------------------------------------------------------------------
Ran 10 tests in 0.265s

OK

0.893s, a 0.265s to jest różnica 29.68% na tak prostym skrypcie, jednak w tym momencie nie to było najważniejsze, chciałem Ci pokazać, jakie to jest genialne. Modyfikujesz kod odpalasz testy i już wiesz czy twoje zmiany nie wpłynęły negatywnie na program.

Jeśli na tak prostym przykładzie widać zastosowanie unit testingu to co dopiero przy dużych projektach.

Podsumowując:
- robienie testów pomaga nam podczas pisania programu
- robienie testów pomaga nam przy rozwijaniu programu
- robienie testów pomaga nam wychwycić dużo więcej błędów
- robienie testów pomaga w pracy zespołowej
To by było na tyle z mojej strony. Mam nadzieję, że ten mały artykuł choć trochę Ci się przydał. Opinię, uwagi, spostrzeżenia proszę wysyłać na karql.pl@gmail.com

Pozdrawiam.
autor: Mateusz 'Karql' Karkula


[1] [2] [3] [4] [5] [6] [7] [8] [9] [10]