Archive

Posts Tagged ‘testowanie’

Spring Python i wprowadzenie do IoC (Inversion of Control)

December 22nd, 2009 Dariusz Suchojad Comments off

Jednym z feature’ów Spring Pythona jest kontener IoC, Inversion of Control, poniższy artykuł przedstawia konfigurację kontenera przy wykorzystaniu Spring Pythona w wersji 1.0.

Do czego może służyć Inversion of Control? Oczywiście do odwrócenia kontroli! :-) Cały trick polega na tym, żeby istniał pewien byt – właśnie kontener IoC – który zarządza zależnościami pomiędzy modułami aplikacji. Kontener taki bywa również określany mianem kontekstu aplikacji.

Klasy Pythona są bardzo dobre, moduły są dobre, pakiety, funkcje i metody także są dobre, kontener jednak pozwala w dodatkowy sposób podzielić aplikację na moduły, niezależnie od tego czy poszczególne moduły są pojedynczymi klasami, instancjami, funkcjami czy może po prostu stringami lub integerami.

Na czym polega odwrócenie? Na tym, że moduły aplikacji (nazywane dalej obiektami kontenera, kontekstu) nie tworzą bezpośrednio zależności pomiędzy sobą, np. nie importują siebie nawzajem. Pisząc więc klasę reprezentującą klienta, który wywołuje zdalną usługę serwera i jednocześnie uwierzytelnia się przed tym serwerem, nie importujemy w klasie klienckiej klasy implementującej uwierzytelnianie, a jedynie korzystamy z tej klasy  uwierzytelniającej – pozwalamy kontenerowi na to, żeby nam w runtimie wstrzyknął sam odpowiednia skonfigurowaną klasę, która zapewni uwierzytelnianie. Ostatecznie, klasa, która ma wywołać usługę serwera nie musi sobie zawracać głowy tym, skąd wziąć i jak skonfigurować uwierzytelnianie, prawda? Jej rolą, zadaniem i zmartwieniem jest wywołanie serwera, a nie związane z tym kwestie poboczne. Jeśli serwer kiedyś zmieni (a zawsze następuje to wcześniej niż można spodziewać się) sposób uwierzytelniania to nie będziemy musieli w ogóle dotykać klasy klienckiej – a ostatecznie celem jest to, żeby dobrze przetestowanego i spełniającego na produkcji swoją biznesową rolę kodu nie zmieniać (w każdym razie, nie zmieniać bez potrzeby).

Kluczowe w idei IoC jest to, że kontener konfigurowany jest deklaratywnie, jest to raczej statyczna konfiguracja niż miejsce, w który można poszaleć z pętlami czy wyjątkami. Ma to taką miłą zaletę, że kontener konfigurujemy z boku całej aplikacji, bez zmian w jej kodzie implementującym logikę biznesową.

Inną cechą kontekstu jest to, że obiekty definiowane w nim mogą być pobierane z niego jako singletone’y lub jako zwykłe instancje klas (tzw. prototypy).

IoC znakomicie też ułatwia testowanie aplikacji, całość składana jest z klocków, więc nie ma problemu z tym, żeby prawdziwe klocki, te, które faktycznie tworzą aplikację, zastępować mockami. Dzięki temu łatwo pisze się unittesty.

W wersji 1.0 Spring Pythona, IoC można skonfigurować korzystając przede wszystkim z dwóch składni:

- XMLConfig – pozwala na konfigurację kontenera w XML-u
- PythonConfig – umożliwiwa konfigurację kontenera w samym Pythonie

Od wersji 1.1 w Spring Pythonie dostępny będzie także YamlConfig, jak wynika z nazwy – IoC będzie można też konfigurować w YAML-u i jest to bardzo wygodne, choć jest to już sprawa na inny artykuł.

W dalszej części będę używał PythonConfiga, aczkolwiek dokładnie to samo można osiągnąć korzystając z XMLConfiga (a od wersji 1.1 także w YamlConfigu).

W porządku, załóżmy więc, że chcemy napisać aplikację, która wywoła usługę serwera zwracającą kursy walut. Trzeba więc utworzyć prosty serwer, który będzie zwracał kursy dla, powiedzmy, dwóch walut. Przed serwerem tym trzeba będzie też jakoś uwierzytelnić się.

Serwer będzie napisany w Pyro, bez użycia Spring Pythona, a poniżej jest jego kod źródłowy:

# -*- coding: utf-8 -*-

# server.py

import Pyro.core as pyro

class ExchangeRates(pyro.ObjBase):
    """ Minimalny serwer zwracający, dla wybranych walut, kursy wymiany złotego.
    """
    def get_exchange_rate(self, user, password, currency):
        if user != "foo" and password != "bar":
            return "FORBIDDEN"

        if currency == "EUR":
            return "4.1959"
        elif currency == "CHF":
            return "2.8080"
        else:
            return "UNRECOGNIZED_CURRENCY"

def run_server():
    """ Startuje serwer kursów walut."
    """
    pyro.initServer()
    daemon = pyro.Daemon()
    uri = daemon.connect(ExchangeRates(), "exchange-rates")

    print "Server started, port: %s" % daemon.port

    daemon.requestLoop()

if __name__ == "__main__":
    run_server()

I taki serwer spokojnie nam wystarczy, przejdźmy teraz do klienta, który będzie korzystał ze Spring Pythona. Tak się szczęśliwie składa, że Spring Python wspiera także korzystanie z Pyro, więc z tego też skorzystamy.

Pierwsza wersja klienta:

# -*- coding: utf-8 -*-

# client1.py

from springpython.remoting.pyro import PyroProxyFactory

class Client(object):
    def __init__(self, url, user, password):
        self.url = url
        self.user = user
        self.password = password

        self.proxy = PyroProxyFactory()
        self.proxy.service_url = self.url

    def get_exchange_rate(self, currency):
        return self.proxy.get_exchange_rate(self.user, self.password, currency)

client = Client("PYROLOC://127.0.0.1:7766/exchange-rates", "foo", "bar")
print client.get_exchange_rate("EUR")

Uruchomienie jej zakończy się sukcesem

$ python client1.py
Pyro Client Initialized. Using Pyro V3.7
4.1959
$

Użyjmy teraz IoC do skonfigurowania klienta, na początek przenieśmy tam po prostu wszystkie zmienne, które na pierwszy rzut oka wydają się warte przeniesienia, czyli URL usługi, usera i hasło.

Kontener korzystający ze składni Pythona to zwykła klasa dziedzicząca po springpython.config.PythonConfig. Spring Python zarządza tylko tymi obiektami, które przypisany mają dekorator springpython.config.Object. Pierwsza wersja kontekstu może wyglądać tak:

# -*- coding: utf-8 -*-

# ctx1.py

from springpython.config import Object
from springpython.config import PythonConfig

class ClientContext(PythonConfig):

    @Object
    def url(self):                      
        return "PYROLOC://127.0.0.1:7766/exchange-rates"

    @Object    
    def user(self):
        return "foo"

    @Object
    def password(self):
        return "bar"

a klient korzystający z kontekstu w ctx1.py ma postać:

# -*- coding: utf-8 -*-

# client2.py

from springpython.context import ApplicationContext
from springpython.remoting.pyro import PyroProxyFactory

from ctx1 import ClientContext

class Client(object):
    def __init__(self, url, user, password):
        self.url = url
        self.user = user
        self.password = password

        self.proxy = PyroProxyFactory()
        self.proxy.service_url = self.url

    def get_exchange_rate(self, currency):
        return self.proxy.get_exchange_rate(self.user, self.password, currency)

ctx = ApplicationContext(ClientContext())

url = ctx.get_object("url")
user = ctx.get_object("user")
password = ctx.get_object("password")

client = Client(url, user, password)
print client.get_exchange_rate("EUR")

Tworzymy więc kontekst aplikacji poprzez powołanie nowej instancji klasy springpython.context.ApplicationContext, której przekazujemy instancję naszej konfiguracji z ctx1.py. Do obiektów zarządzanych przez kontekst mamy dostęp przez metodę .get_object(nazwa_obiektu) – ‘nazwa_obiektu’ jest taka sama jak nazwa metody zdefiniowana w ctx1.py, wywołanie tej metody zwróci obiekt z kontenera. Warto zwrócić uwagę na to, że w Spring Pythonie zawsze używamy metody .get_object do pobierania obiektów z kontenera, niezależnie od tego, czy (tak jak tutaj) korzystamy z PythonConfiga, z XMLConfiga czy też – od Spring Pythona 1.1 – z YamlConfiga.

Mamy więc kontekst, w którym trzymana jest podstawowa konfiguracja. Domyślne użycie dekoratora @Object tak jak powyżej oznacza, że dany obiekt jest singletonem. Możliwe jest także podanie explicite, że obiekt ma być singletonem; można także podać, że obiekt ma być prototypem, czyli przy każdym jego pobraniu z użyciem .get_object będzie zwracana jego nowa instancja.

Pójdźmy teraz dalej i zdejmijmy z klasy Client odpowiedzialność za tworzenie proxy do wywoływania serwera – przede wszystkim, klient ma serwer przez proxy wywoływać, a nie pamiętać o tworzeniu proxy (niech się tym zajmie kontekst), po drugie, w przykładzie tworzymy tylko jednego klienta, ale w rzeczywistości instancji klasy Client możemy tworzyć wiele i nie ma sensu tworzyć nowego proxy dla każdej instancji, niech proxy będzie singletonem. Przy okazji przeniesiemy też do kontekstu definicję klienta, i będzie ona prototypem, przy każdym pobraniu klienta z kontekstu będziemy otrzymywali nową instancję klasy Client.

Tak może wyglądać kontekst ctx2.py

# -*- coding: utf-8 -*-

# ctx2.py

from springpython.remoting.pyro import PyroProxyFactory
from springpython.config import scope, Object, PythonConfig

from client3 import Client

class ClientContext(PythonConfig):

    @Object
    def url(self):                      
        return "PYROLOC://127.0.0.1:7766/exchange-rates"

    @Object    
    def user(self):
        return "foo"

    @Object
    def password(self):
        return "bar"

    @Object
    def proxy(self):
        proxy = PyroProxyFactory()
        proxy.service_url = self.url()

        return proxy

    @Object(scope.PROTOTYPE)        
    def client(self):
        client = Client()
        client.proxy = self.proxy()
        client.user = self.user()
        client.password = self.password()

        return client

Z kolei client3.py mocno zmniejszył się

# -*- coding: utf-8 -*-

# client3.py

class Client(object):
    def __init__(self, proxy=None, user=None, password=None):

        self.proxy = proxy
        self.user = user
        self.password = password

    def get_exchange_rate(self, currency):
        return self.proxy.get_exchange_rate(self.user, self.password, currency)

Potrzebujemy teraz czegoś, co uruchomi całość, np. app1.py:

# -*- coding: utf-8 -*-

# app1.py

from springpython.context import ApplicationContext

from ctx2 import ClientContext

ctx = ApplicationContext(ClientContext())

client = ctx.get_object("client")
print client.get_exchange_rate("EUR")

Co teraz osiągnęliśmy? Oddzieliliśmy od siebie komponenty. Klient nie wie -  i dobrze, skoro nie musi tego wiedzieć – skąd bierze się proxy, ważne, że może je wywołać. Nie wie nawet jakiego rodzaju jest to proxy, akurat teraz jest to Pyro, ale kiedyś może być to komunikacja z serwerem przy użyciu innej technologii. Proxy także nie wie skąd bierze się jego konfiguracja, akurat teraz URL jest zapisany bezpośrednio w kontekście, ale kto wie, może za miesiąc będzie pobierany z LDAP-a czy z innego źródła danych – ponownie, nie powinno być zmartwieniem proxy skąd wziąć URL, URL powinien być dla proxy po prostu dostępny, a jego udostępnieniem zajmuje się kontekst. Komponenty zajmują się jedynie wykonywaniem swojej pracy, a nie zadaniami konfiguracyjnymi, nie są ze sobą powiązane zależnościami w kodzie. Można je więc spokojnie rozdzielić, osobno dystrybuować, osobno developować, i nie przejmować się tym w jaki sposób zostaną w przyszłości użyte – ważne jest tylko to, że dobrze spełniają swoje biznesowe zadania.

Sprawdźmy teraz w jaki sposób IoC ułatwia nam napisanie unittestów – nie możemy oczywiście zakładać, że będziemy mieli zawsze dostęp do serwera (zresztą, o to chodzi w unittestach, żeby testować pojedyncze unity, w różnych warunkach izolacji od otoczenia), napiszemy więc fake’owe proxy, który będzie nam symulowało prawdziwy serwer i wstrzykniemy je do kontekstu w miejsce prawdziwego proxy.

# -*- coding: utf-8 -*-

# test_client.py

# stdlib
import unittest
from decimal import Decimal

# Spring Python
from springpython.config import Object
from springpython.context import ApplicationContext

# Nasza aplikacja
from ctx2 import ClientContext

class FakeProxy(object):
    """ Fake'owe proxy, używane w unittestach aplikacji klienckiej.
    """
    def get_exchange_rate(self, user, password, currency):
        return "4.3726"

class TestClientContext(ClientContext):
    """ Kontekst używany do testów aplikacji klienckiej.
    """

    @Object
    def proxy(self):
        return FakeProxy()

class TestClient(unittest.TestCase):

    def setUp(self):
        self.ctx = ApplicationContext(TestClientContext())

    def test_client_exchange_rate_decimal(self):
        client = self.ctx.get_object("client")
        rate = client.get_exchange_rate("EUR")

        # Załóżmy, że nie wiemy jaką dokładnie wartość zwraca serwer; wiemy
        # tylko, że jest to Decimal.
        rate_decimal = Decimal(rate)

        # Jeśli powyżej nie wystąpił wyjątek to znaczy, że wartość zwrócona
        # przez klienta była faktycznie możliwa do konwersji na typ Decimal,
        # czyli test powiódł się.

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

Na czym polega test? Na sprawdzeniu tego, czy klient zwraca wartości możliwe do skonwertowania na typ decimal.Decimal. Nie chcemy oczywiście uruchamiać do tego całego serwera, tworzymy więc subklasę kontekstu, w której metoda ‘proxy’ jest przeciążona, zwraca nasze fake’owe proxy, które w tym prostym przykładzie zwraca zawsze taką samą wartość – docelowo proxy to byłoby o wiele bardziej rozbudowane i symulowałoby faktyczną komunikację z serwerem, zwracałoby więcej kursów i obsługiwałoby uwierzytelnianie – ale dla potrzeb przykładu takie proste proxy w zupełności wystarczy do zilustrowania zasady, że wystarczy utworzyć testowy kontekst przeciążający te metody (czyli zwracający te obiekty), które są nam potrzebne w teście. Ponownie – trzeba zauważyć, że nie zmienialiśmy w ogóle kodu aplikacji, aplikacja jako taka nic nie wie, że tym razem jest poddawana testom i że proxy jest fake’owe.

Mam nadzieję, że to proste wprowadzenie zachęciło Was do korzystania z IoC i Spring Pythona. Nie jest to jednak koniec możliwości, ani Spring Pythona, ani IoC jako sposobu na modelowanie zależności komponentów aplikacji. Więcej informacji o Spring Pythonie można znaleźć tutaj, a dla osób zainteresowanych konfigurowaniem kontekstów w YAML-u jest dokumentacja do zbliżającej się wersji 1.1 Spring Pythona. Spring Python ma także swoje forum, listę mailingową i grupę na LinkedIn, można tam spokojnie wstąpić i zawsze ktoś pomoże lub doradzi :-)

Share

Kobiety jednak używają komputerów

March 5th, 2009 Dariusz Suchojad No comments

W razie gdyby ktoś nadal nie chciał uwierzyć – przypominam, że kobiety korzystają jednak z komputerów oraz Internetu i należy brać pod uwagę to, kto będzie odbiorcą oprogramowania, które tworzysz.

Z innych nowości – grają także w piłkę nożną, chodzą na wojny i fruwają w kosmos.

Dość smutne są obrazki, które widać na małych jak i dużych serwisach, ciągle powtarza się pytanie “Zapomniałeś hasło?” – czy tylko mężczyźni mogą zapomnieć swoje hasło? Czy kobiety nigdy nie zapominają, albo, gdy już zapomną, to nie mogą go zmienić?

Serwisy publiczne to jednak nic wielkiego w porównaniu z tym, co dzieje się w aplikacjach zamkniętych, w oprogramowaniu tworzonym dla wewnętrznego użytku przedsiębiorstw. Wydaje się, że bardzo mało osób bada to, kto właściwie będzie korzystał z tworzonego software’u. Warto sprawdzić czy kolejny pakiet oprogramowania finansowego, kadrowego, CRM czy wspierający inne procesy biznesowe będzie używany głównie przez mężczyzn, kobiety, czy może jednak pół na pół? Przecież zdarzają się środowiska tak zdominowane przez kobiety, że wyświetlanie na ekranie “Zalogowałeś się do systemu FOOBAR” zamiast “Witamy w systemie FOOBAR” wygląda po prostu na całkowity brak zainteresowania wrażeniami z używania systemu. Proponuję traktować takie teksty jako błąd w oprogramowaniu, powinny być to normalne błędy, zgłaszane do poprawek przed oddaniem systemu do użytku. Po oddaniu do użytku trzeba je traktować jako zwykłe błędy znalezione na produkcji, do poprawy w następnym releasie.

Nawet jeśli w wymaganiach biznesowych nikt na temat nic nie powiedział, to nie znaczy, że nie można samemu o tym pomyśleć, zadać stronie biznesowej pytanie, kto dokładnie docelowo będzie korzystał z systemu, a jeśli nawet strona biznesowa nie będzie wiedziała, albo nie zrozumie sprawy, to możecie sami napisać zamiast “Zapomniałeś hasło?” – “Nie pamiętasz hasła?”, zamiast “Czy jesteś pewny, że chcesz usunąć XYZ” – “Czy na pewno chcesz usunąć XYZ?” zamiast “Zostałeś wylogowany z systemu ABC” – “Życzymy Ci miłego dnia, kliknij tutaj, aby zalogować się ponownie”, a kobiety będą chętniej korzystały z Waszych
systemów :-)

Na marginesie, A list apart ma ciekawy artykuł i dyskusję o tym, żeby w ogóle nie pytać nikogo o to, czy chce na pewno usuwać cokolwiek, postulują, aby mieć zawsze undo. Ciężka sprawa zaimplementować undo w każdej sytuacji (po kilku tygodniach negocjacji z klientem ktoś klika “decyzja kredytowa negatywna” .. “niee, undo!, jednak p o z y t y w n a” ;-) ), ale na pewno warto o tym pomyśleć.

Share
Categories: Software Tags: , ,

110% pokrycia kodu

February 23rd, 2009 Dariusz Suchojad No comments

Cóż, zdarzają się ludzie, którzy wierzą, że pokrycie testami 100% kodu wystarczy,
aby stwierdzić, że w projekcie są prawidłowe testy.

Osiągnięcie takiego wyniku przy niebanalnym projekcie jest sprawą niełatwą
i czapki z głów przed tymi, którzy to osiągną, ale jednak pokrycie testami kodu mierzy
nic innego niż, cóż, pokrycie testami kodu…

Jeśli ktoś uważa, że pokrycie testami 100% kodu to wszystko co jest potrzebne,
to jest w sporym błędzie. To, że masz pewność, że wykonana została każda linijka
kodu, nie oznacza w ogóle, że kod spełnia wymagania i że zadziała w każdej
z przewidzianych w projekcie sytuacji. Pomijając już kwestie testowania rozmaitych
zakresów parametrów wejściowych lub interakcji z innymi częściami systemu,
może też zdarzyć się, że testami pokryty jest też kod, który w ogóle nie jest potrzebny.

Przytrafiło mi się, że przeglądałem pewien kawałek kodu i po jakimś czasie doszedłem
do wniosku, że część z tego kodu nie jest potrzebna, po prostu tego kodu nie powinno
w ogóle być. Nie robił niczego złego w tym sensie, że projektowana funkcjonalność
była realizowana, a od strony pokrycia kodu wszystko było w porządku, kod wykonywał
się. Szkopuł jednak w tym, że sporo linijek mogłem spokojnie usunąć i funkcjonalność
była nadal zachowana. Miałem więc zmierzone pokrycie kodu, który nie powinien
istnieć. Po pewnym czasie wyszło też, że kod ten wykonywał 4 niepotrzebne SELECT-y,
więc de facto był bugiem. A bugi się ubija :-) .

Wynikają z tego przynajmniej dwie kwestie

  • mierzenie pokrycia kodu to jeszcze nie wszystko co można zrobić
  • przeglądy kodu to bardzo dobra rzecz
Share
Categories: Software Tags: