Škatulkování programátorů

Když se člověk podívá na nabídky práce, často narazí na škatulkování. Vlastně téměř pokaždé. Jednou to je Pythonista, jindy JavaScriptář, pak zase PHPčkář, C++kař, databázista a tak dále. K tomu nějaká nutná praxe. Máme to i u nás v práci.

Osobně se mi dělení nelíbí. Naopak mi přijde super mít mix lidí. Už jsem o tom nepřímo psal. Mix ve smyslu, kdy každý má jiný background. Jinou školu. Jinak přemýšlí. Má rád něco jiného. Je úplně v pořádku, když jeden zbožňuje Javu, jiný Go, další Haskell či třeba LISP. To neznamená, že všichni nemůžeme pracovat v Pythonu a přinášet super nápady odjinud.

Moc by se mi líbilo, kdyby se hledali „šikovní programátoři™“ a nijak se nedělili ani po náboru. Tým dostane několik úkolů a ať si tým úkoly rozebere sám dle libosti. Chceš teď chvíli řešit HTML5 a CSS3, aby sis osahal, co se změnilo od dob IE šestky? Chceš omrknout novinku React a ES6? Chceš zkusit ladit výkon databáze, či aplikace celkově? Chceš se naučit Python? Chceš si zkusit konfigurovat servery? Jasně, běž do toho!

Ano, někomu bez daných zkušeností to bude trvat o něco déle. Ale zase je do toho zapálen! Dělat neustále to samé člověka unavuje, až se mu do práce vůbec nechce. Pak tu je možnost párovat. Ten, kdo umí, může dělat nováčkovi v dané technologii pozorovatele a tím ho naučit. Stále to je o něco časově náročnější, ale jedná se o investici do budoucna. A když se jedná o něco, co neumí nikdo, někdo už určitě bude mít chuť se na danou věc mrknout.

Samozřejmě tím nechci říct, že všichni by měli být full-stack vývojáři. Jde mi o to, aby programátoři nebyli svázáni a mohli si vybrat úkoly dle libosti. Od client-side, přes server-side, přes mobilní aplikace až po vše, co tým vyvíjí. Pokud chce někdo zůstat u toho, co umí nejlépe, a všem tak vyhovuje, nemá smysl nijak měnit.

Takhle jsem to měl v hlavě a započal nedávno experiment. Zatím to vypadá na pozitivní reakce. Takže už nehledáme Pythonistu. Ani JavaScriptáře. Ale šikovného programátora, kterému nevadí v takovém prostředí programovat. Zbytek už naučíme. Chceš pracovat s námi? :-)

Django vs. Flask, Rails vs. Sinatra

Výběr frameworku je vždy náročný úkol. V Pythonu jsou nejznámější webové frameworky Django a Flask, který ale vybrat? Vyzkoušeny mám oba pro velké projekty za různých okolností a nemám výherce. Každý je dobrý k něčemu jinému. V Ruby je podobná volba Rails nebo Sinatra. To, že nemám výherce, ale neznamená, že nevím, po kterém sáhnout. Naopak dnes už to vím přesně. Popíšu na dvou případech:

1) Když jsem začínal BOObook.cz, neměl jsem nic a potřeboval jsem rychle něco mít. Sáhl jsem po Djangu, připravil projekt, a ještě tu noc jsem měl základní kostru se šablonami, formuláři, modelem, databází, sessionou a administrací. Mohl jsem se zaměřit na featury a jakmile něco nevyhovovalo, postupně měnit za svůj kód. Například vyměnit/upravit administraci, vlastní servírování souborů chráněné přihlášením a podobně.

2) Když jsme si v práci řekli, že je na čase přejít na open source framework (z našeho interního z dob, kdy ještě nic takového neexistovalo), sáhli jsme po Flasku. Měli jsme totiž vše a Flask nemá nic. Díky tomu nám dovolil ho použít ihned a postupně přecházet na nové šablony, formuláře, modely, session atp. Byl to sice komplikovanější proces, než začít paralelně na zelené louce, jenže přepsat celou aplikaci by trvalo roky. Nyní máme přechod za sebou a funguje nám jak nová, tak stará, ještě nepřepsaná, část aplikace.

Co to tedy znamená?

Django je all-in-one balíček. Od routingu, přes modely a formuláře až po šablony. Samozřejmě nechybí spoustu šikovných či dokonce potřebných utilit. Tedy nainstalujete, přečtete si dokumentaci, první ukázku a můžete se zaměřit na vaši aplikaci.

Flask je naopak vlastně jen routing a vše se musí poskládat z dalších knihoven dle potřeby. Často Jinja, WTForms, SQLAlchemy a další. Tedy nainstalujete, přečtete si dokumentaci a hledáte best practices, jak komponenty poskládat dohromady pro vaši potřebu.

Z toho vyplývá mé doporučení: pokud nemusíte použít nějaké vlastní komponenty, je lepší sáhnout po Djangu. Díky tomu se lze zaměřit na vývoj webové aplikace a neřešit, jak validovat vstupní data, jak si držet sessionu, jak přistupovat k databázi a dalším zdrojům, … S jednou výjimkou – Django je kanón na vrabce, takže nemá smysl se ho držet pro opravdu miniaturní webovou stránku.

Co Rails vs. Sinatra?

Tam to vidím stejně. S tím, že Django se jmenuje Rails a Flask je Sinatra.

Jediný rozdíl vidím ve výjimce. Chtěl jsem se podívat, co zajímavého nabízí Ruby, a tak ho využil pro jedno API k mému novému pet projektu. Jelikož se jedná o SPA a veškerý kód je v JavaScriptu, sáhl jsem po Sinatře. Což byla asi chyba, jelikož jako ve světe JavaScriptu často lidi radí, jak problém vyřešit s jQuery, ve světe Ruby naopak radí, jak problém vyřešit s Rails. Je pak těžké najít čisté Ruby. Na druhou stranu s Rails bych nevěděl, co znamená čisté Ruby. :-)

#programmerhumor

FizzBuzz

FizzBuzz zná zřejmě každý programátor. Není to nic těžkého, implementace může vypadat třeba takto:

for index in range(1, 20):
    if index % 15 == 0:
        print 'FizzBuzz'
    elif index % 3 == 0:
        print 'Fizz'
    elif index % 5 == 0:
        print 'Buzz'
    else:
        print index

Ale to není sranda. Sice je to úloha na rychlé vyřazení špatných programátorů, ale paradoxně selhávají i schopní, protože se snaží na první dobrou napsat řešení nějak chytře (rozuměj one-linově). A nepovede se. Třeba:

for index in range(1, 20):
    print 'Fizz' * int(index % 3 == 0) + 'Buzz' * int(index % 5 == 0) or index

Ale to stále není sranda. Třeba takový Javista by mohl namítnout, že v tom je málo tříd…

class FizzBuzzNumber(int):
    def __str__(self):
        return (self.fizz + self.buzz) or super(FizzBuzzNumber, self).__str__()
    
    @property
    def fizz(self):
        return 'Fizz' if self.is_fizz else ''
    
    @property
    def buzz(self):
        return 'Buzz' if self.is_buzz else ''
    
    @property
    def is_fizz(self):
        return self % 3 == 0
    
    @property
    def is_buzz(self):
        return self % 5 == 0


for index in range(1, 20):
    print FizzBuzzNumber(index)

Mně to přišlo ale stále málo crazy. Když už mám třídu, proč si nevytvořit dynamicky třídu pro každé číslo. Když to jde, že?

class FizzBuzzNumberType(type):
    _class_cache = {}

    def __new__(mcs, name, bases, attributes):
        return mcs.create_instance

    @classmethod
    def create_instance(cls, number):
        return cls.create_or_get_class(number)(number)

    @classmethod
    def create_or_get_class(cls, number):
        if number not in cls._class_cache:
            cls._class_cache[number] = cls.create_class(number)
        return cls._class_cache[number]

    @classmethod
    def create_class(cls, number):
        printable_number = cls.get_printable_number(number)
        attributes = {
            '__str__': lambda self: printable_number,
        }
        new_cls = type.__new__(FizzBuzzNumberType, 'FizzBuzzNumber({})'.format(number), (int,), attributes)
        return new_cls

    @staticmethod
    def get_printable_number(number):
        return 'Fizz' * int(number % 3 == 0) + 'Buzz' * int(number % 5 == 0) or str(number)


class FizzBuzzNumber(int):
    __metaclass__ = FizzBuzzNumberType


for index in range(1, 20):
    print FizzBuzzNumber(index)

Tím sranda nemusí končit. Můžeme klidně pokračovat generováním kódu z XML. Aneb můžeme programovat aniž bychom museli měnit kód!

Ale to už je fakt za hranicí šílenosti. Pojďme zkusit něco jiného. Co třeba CSS?

.fizzbuzz {
    counter-increment: index;
}
.fizzbuzz:nth-of-type(n)::before{
    content: counter(index);
}
.fizzbuzz:nth-of-type(5n)::before{
    content: "";
}
.fizzbuzz:nth-of-type(3n)::before{
    content: "fizz";
}
.fizzbuzz:nth-of-type(5n)::after{
    content: "buzz";
}

Kam se hrabe JavaScript. :-)

Přemýšlel jsem, v čem ještě by šlo implementovat FizzBuzz. Vzpomněl jsem si na hlášku: Když se někteří lidé setkají s problémem, pomyslí si: „Já vím! Použiji regulární výrazy.“ V tom okamžiku mají problémy dva.

seq 1 19 | sed -r '3~3 s/[0-9]*/Fizz/; 5~5 s/[0-9]*$/Buzz/'

A taky že ano. První řešení je velice jednoduché. Tak ještě jednou bez počítání řádků!

seq 1 19 | sed -r 's/^([0369]|[258][0369]*[147]|[147]([0369]|[147][0369]*[258])*[258]|[258][0369]*[258]([0369]|[147][0369]*[258])*[258]|[147]([0369]|[147][0369]*[258])*[147][0369]*[147]|[258][0369]*[258]([0369]|[147][0369]*[258])*[147][0369]*[147])*$/Fizz\1/; s/^Fizz[0-9]*[05]$/FizzBuzz/; s/^Fizz[0-9]*$/Fizz/; s/^[0-9]*[05]$/Buzz/'

Ufff. Šlo by to napsat s menším počtem kroků, ale jeden konečný automat mi pro dnešek stačil.

Co vy, máte taky nějaký crazy FizzBuzz?

P.S.: Pro silnější nátury jsem narazil na oblíbený Brainfuck. A pro ověření si můžete udělat vlastní interpreter. :-)

Testování webových aplikací Seleniem

O tomto tématu jsem už něco napsal. Například jak nainstalovat a začít či nějaké best practices. Dneska chci napsat o tom, jak vlastně testujeme u nás, co jsem už párkrát školil.

Ukázky budou v Pythonu. Knihovna pro komunikaci se Seleniem je WebDriver Wrapper, kterou jsem sepsal pro jednodušší ovládání prohlížeče. Aby se nemuselo myslet na všechny ty detaily. A samotné testy budou ukázány s pytestem, který jsem si v nedávné době oblíbil.

Pohyb po webu

Takový základ na webu je přesun mezi stránkami. Selenium nabízí metodu get. Nevýhoda je, že se musí pokaždé předávat absolutní cesta. Proto jsem přidal motodu go_to, díky které stačí předat relativní cesta. Například:

driver.go_to('http://seznam.cz')
driver.go_to('prihlaseni')

Musí se ale myslet na to, že někde se musí začít s absolutní. Nejvhodnější hned po vytvoření driveru. Aby to bylo ještě jednodušší, lze předávat parametry jako slovník!

driver.go_to('search', {'q': 'Selenium'})

Což přejde na URL http://seznam.cz/search?q=Selenium.

Často padá dotaz, zda lze stránky testovat na status kód. Nejde. Protože to prý k tomuto toolu nepatří. Samozřejmě teoreticky mají pravdu, prakticky se mýlí. Kluci ze Zboží si s tím poradili tak, že nastartovali proxy, přes kterou ženou všechny požadavky, a zkoumají veškeré HTTP kódy a hlavičky tam. Výhoda je jasná, mohou jednoduše monitorovat veškerý provoz, i třeba JavaScriptové volání.

Pokud není potřeba takovýto kanón, přidal jsem do wrapperu metodu download_url. Buď předáte URL, kterou stáhnout, nebo se stáhne aktuální stránka. Vlastně je to samé, jako například volání request.get(url), jen s tím rozdílem, že to za vás pořeší sušenky. Podobně jsem přidal metodu download_file, která je dostupná nad odkazem či formulářem. Například si představte formulář a při jeho odeslání se stáhne požadovaný soubor. Jak ověřit jeho obsah, status kód a hlavičky? Tak, že ze stránky vytáhnete, zda je formulář POST či GET, všechny jeho formulářové políčka, cookie ve stránce a složíte patřičný request. Nebo…

form = driver.get_elm('form_id')
res = form.download_file()
assert res.status_code == 200

Co vše se v odpovědi nachází, viz dokumentace.

Formuláře

Tím se dostáváme k dalšímu důležitému aspektu a to je vyplňování formulářů. S čistým Seleniem je taková akce utrpením. Jelikož naše aplikace je, dalo by se říct, jedním obrovským formulářem, byla to první věc, kterou jsme si zjednodušili. Jak se vyplňuje například formulář v Djangu či WTForms? Předáním slovníku z requestu. Proč to nezkusit tedy i pro testování webovek…

form = driver.get_elm('form_id')
form.fill_out_and_submit({
    'string': u'Nějaký text',
    'number': 42,
    'checkbox': True,
    'radio': 'value',
    'select': 'value',
    'multiselect': ['value1', 'value5'],
    'file': '/tmp/file.txt',
})

Vyhnuli jsme se tedy různým způsobům vyplňováním na pozadí. Klíč je vždy name formulářového políčka a hodnota jak byste ji reprezentovali v Pythonu. O zbytek se postará wrapper za vás.

Například se v poslední době rozmáhá Bootstrap a jiné knihovny sloužící nejen programátorům vytvářet rychle pohledný web. Problém je, že všude je moderní klasický checkbox skryt a přestylován k obrazu svému. Tím poté po volání metody click vyskočí výjimka o skrytém elementu a tudíž na něj „uživatel“ nemůže kliknout. Musí se tedy klikat místo toho na label kolem checkboxu. Jelikož je to rozšířená praktika, wrapper si s tímto popere. Stejně tak u radio tlačítek.

Dále bývá problém při onchange událostech. Takové události se spustí po opuštění políčka. Takže pokud budeme mít navázaný JavaScript na změnu políčka a v testu na tuto změnu budeme čekat… nedočkáme se, pokud explicitně políčko neopustíme. Například stiskem klávesy tabulátor.

from selenium.webdriver.common.keys import Keys

input = driver.get_elm('input_id')
elm.send_keys('text' + Keys.TAB)

Což pro jistotu wrapper dělá automaticky.

Běžně, díky metodě fill_out, není potřeba obsluhovat selecty ručně, ale stejně takové případy občas nastanou. Normálně byste buď obsluhu museli řešit komplikovaně klikáním, nebo si vytvořit instanci Select:

select = driver.get_elm('select_id')
select = selenium.webdriver.support.select.Select(select)
select.deselect_all()

S wrapperem není potřeba. Select je automaticky Select a můžete volat jeho metody.

select = driver.get_elm('select_id')
select.deselect_all()

Práce s elementy

Možná se divíte, co to stále používám za metodu get_elm. Je to zkratka pro metody find_element_by_*. Tedy původně jsem ji napsal kvůli výjimkám. Selenium totiž umí povědět, zda je něco špatně, ale detailů se nedočkáte. Když nám padaly testy na NoSuchElementException, neměli jsme ponětí, co je špatně. A když jsme měli ponětí (z tracebacku bylo jasné, žádná dynamika), nevěděli jsme na jaké stránce.

Jde o to, že chyba NoSuchElement nemusí nutně znamenat chybějící element. Požadovaný element může na své stránce být, ale problém nastal při přechodu na požadovanou stránku. Například odkaz byl špatný. Takové věci se bez patřičných informací blbě ladí a proto jsem si vytvořil vlastní metodu, která přidává potřebné informace.

>>> driver.get_elm('resultCount')
NoSuchElementException: Message: u'No element <* id=resultCount> found at http://www.seznam.cz'

Hned je vidět, že chybí element s ID resultCount na homepage Seznamu. Tam opravdu daný element není a tudíž problém bude někde jinde.

Jakmile jsem vyřešil, aby takové výjimky házely i metody find_element_by_*, přemýšlel jsem o zrušení. Ale lenost vyhrála. Lenost psaní dlouhých testů. Za prvé první parametr je ID, což se používá nejčastěji a tudíž kratší zápis; co je ale důležitější je kombinace s parent_* atributy.

elm = driver.find_element_by_id('id')
elms = elm.find_elements_by_tag_name('a')
# =>
elms = driver.get_elms(parent_id='id', tag_name='a')

Asi to nezní moc jako důvod, proč by to tu mělo zůstávat. Zajímavé to začne být v kombinaci s dalšími metodami. Například možnost nemuset získat element a poté na něj kliknout, ale rovnou kliknout.

elm = driver.get_elm('id')
elm.click()
# =>
driver.click('id')

Nebo při řešení slavné chyby StaleElementException. Přihlašte se, kdo se s ní setkal! Je velice jednoduché na ni narazit, především, pokud máte tu čest s JavaScriptem. Jakmile vám JavaScript pod rukama začně mazat a znovu vytvářet elementy, staré reference přestanou existovat. (Více jsem o tom už psal.)

Při kliknutí na klasický odkaz Selenium počká na načtení stránky. Při kliknutí na cokoliv jiného, co má na starost JavaScript, Selenium nečeká. Neví totiž, zda má čekat. A na co vlastně. Stejně jako uživatel by nevěděl na co má čekat. Takové akce si musíte zařídit explicitně. time.sleep rozhodně nechcete, a všude dávat ručně cyklus taky ne. Selenium nabízí WebDriverWait, já nabízím jednoduší možnost:

elm = driver.wait_for_element('id')

Občas je potřeba opak – počkat, až se něco skryje. Například nějaké nastavení v modálním okně apod.

elm = driver.wait_for_element_hide('id')

Testování

Pravá sranda nastává až se samotným testováním. Jak si vytvořit driver? Jak se případně přihlásit do aplikace? Co testovat? Jak neustále hlídat nečekané chybové hlášky? Či dokonce chybové stránky? …

Samotné Selenium s ničím takovým nepomáhá a proto jsem do wrapperu přidal i podportu pro snadné testování. Pro pytest stačí vytvořit fixturu s názvem _driver, která vytváří samotný driver a specifikuje jeho scope.

Wrapper obsahuje fixturu driver, který se využívá v testech a definuje nějaké chování, o které se nemusíte starat. Například test může kdykoliv otevřít novou záložku, třeba i omylem, a v každém testu byste museli řešit jejich zavření a v dalším testu pokračovat v hlavním okně.

@pytest.yield_fixture(scope='session')
def _driver():
    driver = Chrome()
    yield driver
    driver.quit()

V případě, že potřebujete před/po každé testovací funkcí driver nějak resetovat, můžete postupovat standardně.

@pytest.yield_fixture(scope='session')
def session_driver():
    driver = Chrome()
    yield driver
    driver.quit()


@pytest.fixture(scope='function')
def _driver(session_driver):
    reset_driver(session_driver)  
    return session_driver

Fixtura driver se nestará pouze o aktivní tab, ale také o detekci chybových hlášek a stránek. Automaticky se hledají CSS třídy error a pokud se nalezne, test skončí chybou. Dále se hledá CSS třída error-page pro chybové stránky jako 500, 404, 403, …

Tedy jakákoliv chybová hláška či stránka je špatně. Nemusíte ručně psát testování chybových hlášek a test sám selže.

def test_save(driver):
    driver.fill_out_and_submit({'username': 'michael', 'password': 'abcd'})

# ErrorMessagesException: Unexpected error messages "Password is not correct".

Samozřejmě ale chceme testovat aplikaci i při selhání. Ověřit, že se uživatel dozví potřebné informace. Na to tu existují dekorátory…

@expected_error_messages('Password is not correct')
def test_save(driver):
    driver.fill_out_and_submit({'username': 'michael', 'password': 'abcd'})

# OK.

Test naopak selže, pokud na stránce nalezne jiné (znamená i žádné) chybové hlášky.

Někdy může nastat situace, kdy se může zobrazit chybová hláška, nebo taky nemusí. Ale to nám nevadí. Třeba že některý server není dostupný, ale pro daný test nás tato informace nezajímá. Pak lze použít allowed_error_messages. Či dokonce allowed_any_error_message pokud je nám text chyby úplně jedno.

Podobně jsou dekorátory pro stránky: expected_error_page a allowed_error_pages. Či dokonce pro informační hlášky typu „úspěšně uloženo“: expected_info_messages a allowed_info_messages. Více v dokumentaci.

Aby toho nebylo málo, wrapper kontroluje taky chyby v JS na stránce. Aby tato kontrola mohla fungovat, je potřeba přidat do stránky následující JS kód:

<script>
    window.jsErrors = [];
    window.onerror = function(errorMessage) {
        window.jsErrors[window.jsErrors.length] = errorMessage;
    }
<script>

Teď ale zní otázka – co když mám chybové hlášky definováno jinak?

driver = Chrome()

# Lepsi nastavit na instanci, takze pri zmene prohlizece neni potreba menit tento kod.
driver.__class__.get_error_page = your_method
driver.__class__.get_error_traceback = your_method
driver.__class__.get_error_messages = your_method
driver.__class__.get_info_messages = your_method

Ještě se zmíním o metodě break_point, díky které se kód zastaví a čeká na signál k pokračování (enter v konzoli). Tím můžete snadno test pozastavit a podívat se do Chrome konzole a podobně.

Pro UnitTesty fungují dekorátory stejně. Pouze použijte připravený WebdriverTestCase.

Další drobnosti

To není vše. Ještě je zde několik detailů. Například metoda contains_text, vylepšená switch_to_window a další pro práci s taby, alerty a dalšími detaily. Projděte dokumentaci.


A to je vše. Máte nějaké dotazy či nápady na vylepšení? Sem s nimi!

Co se mi líbí na pytestu

Stručně jsem tu už o pytestu psal v poznámkách z konference PyCON UK. Od té doby jsme v našem týmu pytest nasadili na několik komponent a testuje se nám parádně. Proč?

Asserty

Není potřeba psát více, než je potřeba. Python už assert v sobě má.

def test_eq():
    assert 1 == 2

Spustíme…

$ py.test test.py
=========================== test session starts ===========================
platform linux2 -- Python 2.7.3 -- py-1.4.25 -- pytest-2.6.3
collected 1 items

test.py F

================================= FAILURES ================================
--------------------------------- test_eq ---------------------------------

    def test_eq():
>   	assert 1 == 2
E    assert 1 == 2

test.py:6: AssertionError
========================= 1 failed in 0.01 seconds =======================

Super! Tímto samozřejmě pytest nekončí. Například balík unittest dodává při assertu různé pomocné hlášky. Tady začíná trochu magie. Python sám o sobě nic víc při použití assertu neřekne, proto pytest udělá menší analýzu kódu a vyplivne podrobný rozbor, třeba i s diffem.

def test_eq():
    a = 'aa'
    assert a == 'aab'
    def test_eq():
    	a = 'aa'
>   	assert a == 'aab'
E    assert 'aa' == 'aab'
E      - aa
E      + aab
E      ?   +

Poradí si i s instancemi a vnořenými instrukcemi. Co ale další typy, třeba assertIn, assertIsInstance, …?

    def test_in():
>   	assert 42 in range(10)
E    assert 42 in [0, 1, 2, 3, 4, 5, ...]
E     +  where [0, 1, 2, 3, 4, 5, ...] = range(10)
    def test_isinstance():
>   	assert isinstance(datetime.date.today(), datetime.datetime)
E    assert isinstance(datetime.date(2015, 2, 14), )
E     +  where datetime.date(2015, 2, 14) = ()
E     +    where  = .today
E     +      where  = datetime.date
E     +  and    = datetime.datetime

Tady je vidět, že občas pytest trochu spamuje. V tomto případě je nám jedno, jakou konkrétní hodnotu datum obsahuje a pokud by se hodilo, stačí vědět datum, nikoliv kde všude v paměti se vše nachází. Ono je to ale takhle lepší, jak zjistíme později. Hlavně pokud se to děje samo s kódem, který musíme tak jako tak napsat, proč ne?

Jak jsou na tom očekávané výjimky?

def test_zerodivision():
    with pytest.raises(ZeroDivisionError) as excinfo:
        1/0
    assert excinfo.value.args[0] == 'integer division or modulo by zero'

Co třeba almostEqual?

Tato metoda a jí podobné chybí. Ale nikdo nezakazuje použití jiných balíků, třeba nose.

from nose.tools import assert_almost_equals

def test():
    assert_almost_equals(1.234, 1.2345, places=4)

Pytest s obyčejným assertem dokáže vše, co je potřeba, aniž byste přišli o jakoukoliv informaci. Dokonce vám dovolí rozšířit assert o vaše informace. Takže při volání assert s vašimi instancemi si můžete přidat potřebná data. Více v dokumentaci.

class Foo:
    def __init__(self, val):
        self.val = val

def pytest_assertrepr_compare(op, left, right):
    if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
        return [
            'Comparing Foo instances:',
            '   vals: %s != %s' % (left.val, right.val)
        ]

Výstup pak vypadá například takto:

    def test_compare():
        f1 = Foo(1)
        f2 = Foo(2)
>       assert f1 == f2
E       assert Comparing Foo instances:
E            vals: 1 != 2

Fixtury

Test se však neskládá jen z testu. Pro test je potřeba, především pro integrační test, předpřipravit data, konexe a další závislosti. Pytest tuto nutnost řeší od začátku bravurním způsobem. Pravděpodobně největší páka, proč jsem vynaložil energii unittest u nás vyměnit právě za pytest.

Klasické unittesty mají metody setUp a tearDown, kterými lze předpřipravit prostředí pro testy, či po sobě uklidit. Problém začne, jakmile taková příprava nění jednoduchá. Například pokud chcete otestovat založení objednávky v e-shopu. K tomu potřebujete minimálně testovací produkt a testovacího uživatele. Takový produkt může být nějakého typu, který potřebuje nakonfigurovat. U uživatele zase potřebujete vyplněné fakturační údaje; ověřit, že se vygeneruje dobře faktura. Poměrně hodně věcí pro jeden setUp.

Pytest na to jde jinak. „Magickým“ dependency injection.

def test_order(user, product):
    # test ordering of product by user

Kde pytest získá uživatele a produkt?

Vy mu je nadefinujete.

@pytest.fixture
def user():
    return create_some_user_here()

@pytest.fixture
def product():
    return create_some_product_here()

Dobře. Ale pro tyto fixtury je potřeba databáze…

@pytest.fixture(scope='function')
def db():
    return connect_to_database()

@pytest.fixture
def user(db):
    return create_some_user_here(db)

OK. Chtěl bych, aby test končil rollbackem a nic po sobě nenechal.

@pytest.yield_fixture(scope='function')
def db():
    db = connect_to_database()
    db.begin()
    yield db
    db.rollback()
    db.close()

Za pozornost stojí parametr scope. Tímto parametrem se určuje, kde všude se má fixture cachovat. Pokud řeknu „function“, říkám tím, aby ve všech fixturách, které potřebuje jeden test, byla stejná instance databáze. Tím mohu pracovat v jedné transakci a rollbacknout. Pokud bych neurčil scope, každá fixture by měla jiné spojení s databází a nešlo by využít transakcí.

Tohle je na fixturách moc hezké. Mohou záviset sami na sobě a příprava i čistka jsou na jednom místě vedle sebe. Plus fixtury mohou být sdílené přes všechny testy a vytvářejí se pouze, pokud jsou opravdu potřeba daným testem. Osobně mi to přijde skvělé a mnohem přehlednější.

Zní dobře. Je možné se připojit k databázi jednou a pro každý test jen začít transakci?

@pytest.yield_fixture(scope='session')
def db_connection():
    db = connect_to_database()
    yield db
    db.close()

@pytest.yield_fixture(scope='function')
def db(db_connection):
    db_connection.begin()
    yield db_connection
    db_connection.rollback()

Fixtury lze skládat jak je libo. Jediné pravidlo je, že nelze, logicky, z většího scope záviset na menším. Tedy například ve fixture db_connection nelze záviset například na fixture se scope „function“. Protože by se pak tato fixture musela v db_connection nacachovat pro všechny testy, což by mohlo mít nežádoucí následky.

Mimochodem v reportu testu uvidíte, jaká instance fixtury do testu vstupovala. To je proč se hodí vidět i ty adresy instancí v paměti. Lze pak jednoduše zkontroloval, zda chybu nezpůsobuje špatná konfigurace fixtur.

Parametry

Super. Chtěl bych vyzkoušet otestování s dvěma typy uživatelů – zaregistrovaný a jednorázový nákup.

@pytest.fixture(params=['registred', 'unregistred'])
def user(request, db):
    return create_some_user_here(db, request.param)

Takhle málo stačí a všechny testy, kde se využívá fixture user, se provedou dvakrát. V reportu testu je to opět hezky vidět.

================================= FAILURES ================================
-------------------------- test_order[registred] --------------------------

user = 'registred'

    def test_order(user):
>       assert 0
E       assert 0

t.py:11: AssertionError
------------------------- test_order[unregistred] -------------------------

user = 'unregistred'

    def test_order(user):
>       assert 0
E       assert 0

t.py:11: AssertionError
========================= 2 failed in 0.01 seconds =======================

Skvělé! Bude to fungovat i když bych chtěl testovat více produktů?

No jasně! Pak se test provede pro každou kombinaci. Samozřejmě lze parametrizovat i fixtury využité jinými fixtury. V našem případě například db_connection může vracet pokaždé jinou konexi – jednou na MySQL, podruhé třeba na PostgreSQL.

Dříve testovat všechny kombinace bylo peklo. S pytestem napíšete jeden test, jednu fixture a nastavíte parametry.

V některém testu bych chtěl mít ale nějaká specifická data. Třeba v jednom testu bych chtěl, aby se mi vytvořil produkt s určitou cenou, kdežto pro ostatní případy stačí cokoliv. V unittestu lze tohle snadno, ale tady se předává jen název fixtury.

I tohle lze zařídit a běžně využíváme. Dělá se to pomocí speciálního parametru request, už jsme ho viděli výše při předávání parametrů. Je to vlastně podobná technika.

@pytest.fixture
def user(request):
    return create_some_user_here(request.function.user_name)


def test(user):
    # ...
test.user_name = 'Some name'

V requestu naleznete také atribut module či cls. Ve skutečnosti je tam toho ještě víc, mrkněte do dokumentace.

Abychom si tohle usnadnili, udělali jsme si hezký dekorátor a pomocnou funkci na mergování těchto dat. Pak vytvoření záznamů do databáze máme jednou pro všechny testy, i když každý test potřebuje trochu jiná data.

def use_data(**data):
    def wrapper(func):
        for key, value in data.items():
            setattr(func, key, value)
        return func
    return wrapper

def get_data(request, attribute_name, default_data={}):
    data = default_data.copy()
    data.update(getattr(request.module, attribute_name, {}))
    data.update(getattr(request.cls, attribute_name, {}))
    data.update(getattr(request.function, attribute_name, {}))
    return data

Poté je použití velice jednoduché a můžete definovat na modulu i třídě najednou. Data se zmergují do sebe logicky, jak by se očekávalo (modul má nejnižší prioritu a funkce přebíjí vše).

@pytest.fixture
def user(request):
    data = get_data(request, 'user_data')
    return create_some_user_here(data)

user_data = {'username': 'test', 'name': 'name'}

class UserTest:
    user_data = {'email': 'test@example.com'}

    @use_data(user_data={'name': 'Some name'})
    def test(user):
	# user.username = 'test'
	# user.email = 'test@example.com'
	# user.name = 'Some name'

Naše pomocná metoda je trochu chytřejší a dokáže pracovat i se seznamem, takže snadno můžeme říct, že uživatelů chceme pro test víc. Ještě ale ladíme detaily. Jakmile vyladíme, velká šance, že na PyPI přibude balíček. Nebo pull request do samotného pytestu. :-)

EDIT: Balíček už existuje a je k nalezení pod názvem pytest-data.

Označování testů

Půjdeme dál. Je velice vhodné mít tzv. smoke testy. Pár testů, které musí vždy projít a je jistota, že základní věci budou určitě fungovat. S pytestem velice snadno.

@pytest.mark.smoketest
def test():
    pass

Hotovo. Nyní stačí spouštět buď všechny jako doposud, nebo s parametrem -m: py.test -m smoketest. Toto označení v pytestu není, takto jste si vlastně vytvořili vlastní označení testů a můžete si je označit jak chcete.

Co překlepy?

Aby nedocházelo k chybám, lze si marky zaregistrovat, případně nikdo nebrání si vytvořit mark a importovat si ho. Lze označit také pouze jeden parametr v parametrizovaném testu.

U nás jsme si to obohatili o vlastní command line option. Udělali jsme pro jistotu opak – místo označování důležitých testů označujeme nedůležité. Tak nezapomene na nějaký důležitý test. Díky tomuto spuštění testů je raz dva a integrační tool se pak postará o kompletní funkčnost. Mimochodem dříve jsme měli jednotkové a integrační testy oddělené – nyní je máme vedle sebe a integrační je označen jako slow.

slow = pytest.mark.slow

def pytest_addoption(parser):
    parser.addoption('--skip-slow-tests', action='store_true', help='skip slow tests')

def pytest_runtest_setup(item):
    if 'slow' in item.keywords and item.config.getoption('--skip-slow-tests'):
        pytest.skip('Skipping slow test (used param --skip-slow-tests)')

Jaké testy jsou ale pomalé? Easy peasy. Stačí přidat argument --duration. Pokud zavolám například py.test --duration 10, na konci testu pytest vyreportuje 10 nejpomalejších míst. Záměrně neříkám testů, ale míst, protože report je rozdělen na část setup, call a teardown. Což se hodí; například při testování se Seleniem je inicializace prohlížeče náročné a vidím aspoň tak, kolik reálně test dlouho trval. První test v řadě tak není hendikepován.

Zašli jsme ještě dál a začali jsme označovat integrační testy, které využívají cizí službu. Pracujeme na obchodní aplikaci, která komunikuje téměř se všemi službami Seznamu. To už je velká šance, že některé rozhraní v testu na chvíli spadne. Pokud taková situace nastane, popadá nám několik integračních testů, což akorát v reportu překáží.

Dnes si testy označíme a pokud potřebné rozhraní není dostupné, test se přeskočí. Stále vidíme, pokud se tak stalo, ale nyní se nemusíme prokousávat například sto spadlými testy. A stačí tak málo…

def pytest_runtest_setup(item):
    marker = item.get_market('uses')
    if marker:
        if not any(is_not_available(arg) for arg in marker.args):
            pytest.skip('Skipping test requiring {}'.format(marker.args))

@pytest.mark.uses('penezenka', 'sklik')
def test():
    # ...

Pluginy

Ještě nekončíme. Unittesty či nose mají argument fast fail indikující, aby běh testů spadl po první chybě. Je tu super věc na ladění. Pytest má ještě lepší: last fail.

Last fail je ve skutečnosti plugin. Instalace je však velice snadná a přidá do command line další argument --lf. A cože to dělá? Spustí jen ty testy, které selhaly v posledním běhu.

Modelová situace: spustíte všechny testy. Trvají 10 minut, spadne jich 20. Provedete opravu, ale nechcete čekat celých deset minut. Nevadí, přidáte argument --lf a spustí se pouze těch 20 rozbitých. Po dokončení většinu opravíte, ale ještě 6 rozbitých testů zbývá. Provedete opravu a zase spustíte s argumentem --lf. Spustí se pouze těch posledních šest. Jakmile se dostanete na nulu, spustíte opět všechny a ověříte, zda jste nerozbili něco dalšího. Mezitím ale můžete na kafe, protože velice pravděpodobně máte hotovo.

Šikovných pluginů je celá řada. Například si můžete udělat testy na PEP8. Existují pluginy pro Flask, Django, … Taky instafail na okamžitý report. Podívejte se do dokumentace a internetu.

Propojení s unittesty

Celé to je děsně super. Ale už mám v mém projektu unittesty…

Nevadí. Pouze zahoďte unittestový či nosový runner, nakonfigurujte si pytest dle potřeb a můžete začít psát testy v novém stylu. U nás máme nakombinované Seleniové testy a žádná velká dřina to nebyla. Pytest si s unittesty poradí, dokonce lze do unittestů dostat fixture. Takto jsem tam dostal Seleniový driver:

class BaseTestCase(unittest.TestCase):
    @pytest.fixutre(autouse=True)
    def _set_driver(self, driver):
        self.driver = driver

Parametr autouse u fixture je taky dobrá věc. Využili jsme například taky při vytváření virtuálního displeje pro Selenium. Není pak potřeba někde explicitně psát, aby se taková fixture všude použila.

OK. Ale pročti si dokumentaci. Tímhle to nekončí!

Pokročilé regulární výrazy

Říká se, že pokud má člověk problém a rozhodne se ho vyřešit pomocí regulárních výrazů, má najednou problémy dva. S tím nesouhlasím. Teda částečně – regulární výrazy jsou mocná zbraň. Musí se však umět použít…

Kdysi mi kolega ukazoval, jak si udělal funkci „oddělovač tísíců“. Popravdě se mi to ani nechtělo louskat a přišel jsem s regulárním výrazem:

>>> def groupjoin(s, sep, groupby):
...     return re.sub(r'(?<=\d)(?=(?:\d{%d})+$)' % groupby, sep, s)
... 
>>> groupjoin('123456789', ',', 2)
'1,23,45,67,89'
>>> groupjoin('123456789', ',', 3)
'123,456,789'

Mnohem lepší! Hned je to čitelnější. Bohužel ale ani tohle řešení moc lidí nedokáže přečíst. Jelikož je možné regulární výrazy využít téměř všude a s úspěchem je lze použít třeba i na hledání jehly v kupce sena (= slova v gigabajtových lozích), je škoda je neumět využít. S tím vám nyní trochu pomůžu. :-)

Základy

Předpokladem je, že už znáte základy. To jest tento seznam:

  • ^ – začátek řetězce,
  • $ – konec řetězce,
  • . – jakýkoliv znak,
  • ? – předchozí pravidlo se může a nemusí vyskytovat,
  • + – předchozí pravidlo se musí vyskytovat minimálně jednou,
  • * – předchozí pravidlo se může vyskytovat kolikrát chce,
  • {m} – předchozí pravidlo se musí vyskytovat m-krát,
  • {m,n} – předchozí pravidlo se musí vyskytovat minimálně m-krát a maximálně n-krát,
  • [abc] – jeden z vypsaných znaků (zde konkrétně a, b nebo c),
  • [^abc] – opak předchozího (zde konkrétně cokoliv, kromě a, b nebo c),
  • a|b – logické nebo (zde konkrétně a nebo b),

Pokud tato pravidla nemáte v krvi, raději si uložte tuto stránku do záložek a vraťte se sem později. Mohlo by se totiž stát, že vás dalšími pravidly od regulárních výrazů odradím. :-)

Tak pojďme na to!

Budeme pracovat s tímto řádkem z logu nginxu:

127.42.256.512 - - [16/Aug/2014:11:35:39 +0200] "GET / HTTP/1.1" 200 9730 "-" "Mozilla/5.0 (X11; CrOS x86_64 5978.26.0) AppleWebKit/537.36 (KHTML, lik
e Gecko) Chrome/37.0.2062.29 Safari/537.36"

Regulár Nenažranec

Z daného řádku chceme získat IP adresu. Začnete psát například takovýto regulární výraz: „.* “ (na konci je mezera), tedy vše do mezery. Vypadá to dobře, spustíte a máte výsledek… až po poslední mezeru. Jak je to možné? Protože regulární výrazy jsou by default nenažrané, greedy. To znamená, že se pokusí najít co nejdelší výsledek, který je možný.

Sice uvedený regulární výraz by šel vylepšit tak, aby greedy vlastnost nevadila, ale to teď ponechme stranou. Hladovost lze totiž změnit jednoduše otazníkem. Pokud za znaky plus, hvězdička či otazník přidáte otazník, rázem dostanete co nejkratší výsledek. „.*? “.

Cvičení: co se stane, pokud předchozí regulární výraz provedete bez mezery?

Odpověď: nevrátí nic. Protože minimum opakování pro hvězdičku je nulakrát. Mezera na konci nám zajišťovala, že se musí najít něco a jedna mezera. Pokud by mezera byla na začátku řetězce, výsledek by byl pouze ta mezera.

Hledej, ale nežer

Z předchozí sekce jsme získali IP adresu, super. Co ale ta mezera na konci? Regulární výrazy vrací vše, co matchnou a my máme mezeru v našem výrazu, tak ji dostaneme. Existuje konstrukce, díky kterým můžeme vyhledávat a nic nevracet. Ukážeme si to rovnou na ukázce: „.*?(?= )

Této konstrukci se říká lookahead (dopředné vyhledávání) a volně si ji můžete přeložit jako „matchni, pokud následuje mezera, ale nevracej mi to do odpovědi“. Samozřejmě existují další tři varianty:

  • (?!...) – negative lookahead (dopředné negativní vyhledávání)
  • (?<=...) – lookbehind (zpětné vyhledávání)
  • (?<!...) – negative lookbehind (zpětné negativní vyhledávání)

Abyste tomu lépe rozuměli, představte si kurzor, který se pohybuje textem. .* bude postupně zaznamenávat pro výsledek a posouvat se textem dál. Jakmile se narazí na lookahaed či lookbehind, kurzor zůstává na svém místě a jen se nahlíží, zda je dál, co má být. Tím, že se nahlíží a kurzor stojí na místě, pokud by následovalo další požírání, vzalo by se to, na co se nahlíželo. Například re.findall("a(?=bcd).", "abcd") vrátí ab.

Cvičení: najděte v našem řádku verzi Chrome prohlížeče, pokud je na architektuře x86_64.

Odpověď: to byla ode mne podpásovka. Není možné vyřešit pomocí lookbehind či lookahead. Lákalo by napsat něco takového: (?<=x86_64.*)Chrome/.*?(?= ), jenže lookbehind nesmí mít různou délku. Délka hledaného řetězce musí být jasná. Jinými slovy nelze v lookbehind využít +, * či {m,n}. Škoda.

Skupiny

Předchozí cvičení není neřešitelné. Lze vyřešit pomocí skupin. Než to složitě popisovat, rovnou ukázka:

x86_64.*(?P<chrome_version>Chrome/.*?)[ ]

Například v Pythonu lze pak použít takto:

>>> re.search(r'x86_64.*(?P<chrome_version>Chrome/.*?)[ ]', line).group('chrome_version')
'Chrome/37.0.2062.29'

Takových skupin si můžete udělat kolik potřebujete. Příklad pro parsování hodin z našeho logu: (?P<hour>\d{1,2}):(?P<minutes>\d{2}):(?P<seconds>\d{2}) (všimněte si, že jsem nespecifikoval nic dalšího; prostě hledám formát hodin v daném textu).

Skupiny si nemusíte pojmenovávat. Už jen použití závorek vytvoří novou skupinu a ve výsledku jsou pak nepojmenované skupiny v seznamu za sebou číslované od jedné (nula je vyhrazena pro celou odpověď). Je však lepší si skupiny pojmenovat, tím je čitelnější regulární výraz a hlavně pozdější využití dat.

Samozřejmostí je možnost využít skupiny (ať pojmenované či nikoliv) při nahrazování textu. Vhodné pro převedení ISO data na český atp.

Žádné skupiny

Upravme nyní náš výraz, aby nevyužíval pojmenované skupiny. Jen na chvíli pro demonstraci další důležité vlastnosti. A přidejme podmínku, že nás zajímá Chrome nebo Safari. Podíváme se, co jsme získali za skupiny:

>>> re.search(r'x86_64.*((Chrome|Safari)/.*?)[ ]', line).groups()
('Chrome/37.0.2062.29', 'Chrome')

Jak je vidět, opravdu jakékoliv použití závorek znamená automaticky další skupinu. Lze to nějak eliminovat, aby ve výsledku nebyl bordel? Jasně, že lze. Konstrukcí (?:...).

Áá, už dost…

Vrátíme se k lookahead a zkombinujeme to se skupinami. V regulárních výrazech lze vyhledávat podle předchozích skupin. Může se to hodit například pro vyhledávání v HTML. Naleznete otevírací tag a musíte najít stejný uzavírací tag. Konstrukce vypadá nějak takto: (?P=group).

Jednoduché. Použití na HTML nechám jako cvičení. My tu uděláme něco zajímavějšího – nalezneme v daném řádku logu všechna čísla, která se vyskytují více, než jednou. Půjdeme na to postupně:

  • Nejprve část hledající čísla: \d+
  • Číslo si zapamatujeme: (?P<number>...)
  • Dále to musíme obalit koncem čísla, abychom nevraceli jen část čísla: (?<!\d)...(?!\d)
  • A tuto novou skupinu využijeme ke konci k nalezení toho samého čísla: .*(?P=number)
  • Druhé číslo však musíme taky hledat celé, takže aplikujeme znovu třetí krok.
  • Výsledky se bohužel vracejí nepřekrývající se. To znamená, že momentálně by nám nula znemožnila nalézt číslo 537 a to zase 36. Protože zatím se nám kurzor hýbe i při hledaní druhého čísla. Napravíme dalším použitím lookahead, protože jich tu máme málo. :-)

Výsledek:

>>> re.findall(r'(?<!\d)(?P<number>\d+)(?!\d)(?=.*(?<!\d)(?P=number)(?!\d))', line)
['11', '1', '0', '0', '537', '36']

Dobré, což?

Může vzniknout otázka, proč se ohraničení čísla nedá také do skupiny, aby se to ve výrazu nemuselo opakovat. Jde o to, že chci vyhledat stejné číslo, ale co je kolem čísla může být pokaždé jiné. Popravdě lookahead a lookbehing fungují tak, že při znovupoužití skupiny matche i když se jedná o jiné ohraničení čísla, ale bohužel to vyhledá i části čísel. Což jsem úplně nepochopil a tak je lepší se tomu vyhnout. :-)

Mimochodem pokud bude potřeba konkrétně v Pythonu někdy hledat překrývající se výsledky, lze případně využít modul regex, který obsahuje parametr overlapped.

Optimalizace

Ještě než se pustíme do vysvětlení úvodního regulárního výrazu, dovolím si nakousnout něco o optimalizaci. Osobně neznám detailně všechny možné implementace regulárních výrazů a tak jsem nikdy neřešil víc, než základní optimalizaci. Zatím jsem si s tím však vystačil. Pokud vám stačit nebude, toto rozhodně není vše, jen špička ledovce.

Jako základ je používat nezachytávající se závorky. Tím ušetříte dost zbytečné režie. Tedy pokud je to možné, použijte (?:...) místo pouze závorek.

Další rada se týká greedy a non-greedy vyhledávání. Mějme na paměti, že u greedy verze se nejprve zkusí pohltit vše, co lze, a pokud nelze najít, ubere se znak a zkusí se pokračovat znovu. A pak znovu. A znovu… I když je výsledek stejný pro greedy i non-greedy verzi, neznamená to, že je to stejné výkonově. Mějme příklad, že chci z našeho řádku vše do prvních dvou pomlček. Jelikož dvě pomlčky jsou na začátku, rychleji se k výsledku dobere non-greedy verze:

>>> timeit.timeit("re.search(r'.* - -', s)", setup)
1.4329700469970703
>>> timeit.timeit("re.search(r'.*? - -', s)", setup)
1.0607848167419434

Pokud tedy víte, jak běžně vypadá text, ve kterém vyhledáváte, použijte tuto znalost při tvoření regulárního výrazu. Vyplatí se to.

Vysvětlení výrazu ze začátku

Nyní byste měli bez problému přečíst regulár ze začátku.

>>> def groupjoin(s, sep, groupby):
...     return re.sub(r'(?<=\d)(?=(?:\d{%d})+$)' % groupby, sep, s)

Část (?:\d{3})+ vyhledává skupiny čísel. Nezachytávání jsme použili čistě kvůli optimalizaci (s timeit 3.2 versus 3.6 sekundy). Tyto bloky hledá tak dlouho, dokud nenarazí na konec řetězce. Jelikož je to ale celé v lookahead, nikam se kurzor neposune, ani nic nepožere. Nalezne to pouze místo, kam přidat oddělovač. Funkce sub sice provádí náhradu, v tomto případě ale vlastně děláme vložení (nahrazujeme místo v řetězci). Lookbehind na začátku je proto, aby se nevložil oddělovač úplně na začátek, viz druhé ukázkové volání funkce.

Nic složitého, že? :-)

A pro vás, kteří jste stále greedy, mám doporučení v podobě knihy Mastering Regular Expressions.

Jasně, umím Git…

…tak to pěkně kecáš! ;-) Tedy pokud jsi nepoužil Git ve větším týmu s několika aktivními větvemi.

Nejednou jsem slyšel (nebo jen viděl v CV) a určitě ještě hodně krát uslyším, jak někdo „umí Git“. Po otázkách typu „U jakého projektu jste Git používal?“, „Kolik lidí v týmu bylo?“ či „Kolik jste měli přibližně aktivních větví?“ je hned jasno.

Ale nikomu se nedivím. Než jsme u nás v týmu přešli na Git, používal jsem ho jen na své malé projekty, kam jsem přispíval na 99 % pouze já do jediné větve – master. Přečetl jsem si knihu Pro Git, vyzkoušel si téměř vše a občas z toho i něco málo udělal. Samozřejmě jsem se v té době hodnotil, jako že Git umím více než dobře. Po přechodu v našem týmu jsem přesto brzy udělal průšvih a museli jsme repositář opravovat.

Dnes už tedy vím, jak jsem byl mimo. Nechci tím říkat, že byste měli jít a smazat si Git z CVčka (budu ale rád za poznámku, kde a jak jste ho používali!), spíš vám chci pomoct do začátku s užitečnými tipy. Z knih se dočtete většinou teorii a jak vše funguje od A do Z. Ale bude vám chybět co kdy jak proč použít, což chci napravit následujícím seznamem.


1. Konfigurace

Začneme zlehka. S konfigurací. Přizpůsobíme si prostředí, aby maximálně vyhovovalo.

Barvičky

Nejvíc základní (tedy po nastavení user.name a user.email) jsou barvy. Zobrazit si status nebo diff obarveně rozhodně zvýší efektivitu.

git config --global color.ui yes

Prompt

Je docela šikovné si nakonfigurovat konzolový prompt, abyste vždy měli na očích, ve které větvi jste, a případně také v jakém stavu. Tím se eliminují chyby, kdy zapomenete, ve které větvi se nacházíte a provedete něco… špatného. Jako třeba pushnout vývojovou větev do produkční, protože přeci naposled jsem byl v produkční, sakra!

Osobně používám následující prompt:

[michal@dev] 12:34:56 789 ~/workspace/emptygit
master$

Vždy tedy vidím, ve které větvi jsem, a pokud mám Git ve stavu „nothing to commit“, pak název větve není tučný. Kód, které je potřeba vložit do .bashrc, mám na GitHubu ve svém msh (my or Michael's shell).

Pokud chcete zajít ještě dál, doporučuji oh-my-git, který zobrazí všechno možné.

Pushování pouze aktuální větve

Příkaz git push automaticky pushuje všechny větve, které jsou svázané s nějakou vzdálenou větví. Pokud máte commity ve dvou větvích a chcete pushnout jen jednu z nich, musíte specifikovat kam pushovat. Pak se ale může stát, že budete ve větvi new-cool-feature a napíšete ze zvyku git push origin maser. A jéje.

Proto je dobré si nastavit, že git push bude pushovat jen aktuální větev, nikoliv všechny.

git config --global push.default current

Kdybyste náhodou potřebovali poté pushovat všechny větve, najděte si to v manuálu. Tohle si nemusíte pamatovat, protože to stejně nebudete nikdy potřebovat. :-)

Merge changelogů

Jestliže Gitujete aplikaci s debianím balením, setkáte se s merge hellem, který nikdo nerad řeší. Sice téměř vždy není co řešit, ale stejně to nikoho nebaví. Naštěstí existuje speciální mód mergování právě pro tyto situace: http://git.savannah.gnu.org/gitweb/?p=gnulib.git;a=blob;f=lib/git-merge-changelog.c

2. Základní operace

Log

Klasický git log odvede svou práci, ale každému prostě sedne něco jiného. Naštěstí lze log konfigurovat. Osobně jsem si oblíbil tento:

git log --pretty=format:"%Cgreen%h%Creset %ad %C(cyan)%an%Creset - %s%C(red)%d%Creset" --graph --date=short

Pokud potřebujete vidět log ze všech větví, pomůže parametr --all. Užitečné, pokud se potřebujete podívat na detail, jak se mergovalo a jaké jsou ve všech větvích aktuální stavy.

Aliasy

Jelikož si ale předchozí příkaz nelze zapamatovat, a i kdyby, nikoho nebude bavit neustále vypisovat, je možné vytvořit aliasy. Aliasy lze udělat na všechno možné, uvedu jen příklad pro předchozí příkazy.

git config --global alias.l 'log --pretty=format:"%Cgreen%h%Creset %ad %C(cyan)%an%Creset - %s%C(red)%d%Creset" --graph --date=short'
git config --global alias.ll 'log --pretty=format:"%Cgreen%h%Creset %ad %C(cyan)%an%Creset - %s%C(red)%d%Creset" --graph --date=short --all'

Prvním příkazem jsem si vytvořil alias git l, což mi provede zmíněný log. Druhý příkaz vytvoří alias git ll, což je ten samý log přes všechny větve.

Tig

I když je Git na příkazové řádce fajnový a lze si různě nakonfigurovat, stále nastane situace, kdy je potřeba něco grafického. Tig je právě jedním takovým programem. Pro konzoli. Používám velice často.

Grafické nástroje

Jako skutečným grafickým nástrojem budiž gitk či gitg, ale s nástrojem tig není potřeba konzoli opouštět.

Oprava commitu

Může se stát, že do commitu zapomenete zahrnout nějaké nové soubory. Nebo ještě najdete jednu chybku a nechcete to ukazovat v logu. Nebo zapomene do zprávy napsat číslo ticketu. Nebo jste jednoduše perfekcionalista a chcete si ve zprávě opravit hrubku. Tak přesně na to všechno tu je parametr --amend. Díky tomuto parametru lze opravit poslední commit.

git commit --amend

Představte si to jako kdybyste udělali další nový commit, ale sloučil se s tím předchozím. Tedy cokoliv přidáte do stage, se přidá k předchozímu commitu. Tedy pro změnu pouze commit message musíte mít stage čistý, samozřejmě.

Ale pozor! Tímto vlastně starý commit smažete a přestane existovat. Za žádnou cenu nesmíte takovou změnu provést, pokud jste již poslední commit pushnuli. Tím byste kolegům repositář rozbili!

Přidání jen části souboru

Často se mi stává, že při programování featury najdu ve stejném souboru chybu. Nenechám ji tam samozřejmě ležet a opravím ji. Ale nechci ji pak commitnout spolu s novou featurou. Tedy potřebuji commitnout jeden soubor na dvě části. Je možnost nejprve něco odmazat, commitnout, a pak to zase vrátit. Proč to však dělat krkolomně, když mohu využít parametr -p (patch):

git add -p

Tak dostanu na výběr, co chci do stage přidat.

Proč bych něco takového chtěl dělat? Většinou to asi tolik nevadí, ale pokud ta chyba není nějaký překlep nebo drobnost, lepší je to rozdělit. Pak při zpětném dohledávání je mnohem jasnější, co se dělalo a proč. Pokud už jste to nechtíc commitli nebo jste prostě lenoši, je dobré alespoň uvést do commit message, co vše v tom commitu je.

3. Pokročilejší operace

Cherry-pick

Když si vzpomenu, kdy jsem o cherry-picku slyšel poprvé, vybavím si strach. Bál jsem se toho, co mi to provede. Moc jsem tomu nevěřil. Přitom na tom nic není…

Situace: dělali jste featuru a narazili jste na chybu. Tu jste (díky předešlému tipu) commitli zvlášť, ale do vývojové větve. Objeví se u vás produktový manažer a oznámí vám, že to je kritická chyba a musí jít ven co nejrychleji. Nejlépe ihned.

Máte na výběr: buď se přepnete do správné větve a kód tam upravíte ručně a znovu commitnete. Nebo se jednoduše přepnete do správně větve, zavoláte cherry-pick, což znovu aplikuje commit v aktuální větvi a jste hotovi.

git cherry-pick #commithash

Mimochodem lze cherry-picknout také více commitů najednou. Tedy není problém ani pokud situace nebude o malé chybě, ale o celé vetší feature o několika commitech.

Zpětné větvení

Představte si situaci: začnete novou featuru. Uděláte commit. Dva. Tři. Zjistíte, že to bude celé komplikovanější a nechcete ostatním rozbít kód. Jenže už jste začali ve sdílené větvi…

Možná vás teď překvapím. Sice se ta větev jmenuje tak, jako na serveru, ale už jen tím, že jste si ji stáhli k sobě, jste v podstatě začali novou. Proto se říká, že je Git decentrializován. U vás to je jiná větev a vy jen posíláte commity z této větve do té vzdálené, odkud si to všichni synchronizují. Proto se může stát chyba, že pushnete commity z vývojové větve do provozní, jak jsem popisoval v sekci „Pushování pouze lokální větve“. To je hodně důležité si uvědomit. Abych to ještě trochu zkomplikoval: větev je pouze ukazatel na commit.

Když si to uvědomíte, zjistíte, že vlastně Git odvětvil automaticky a situaci o dva odstavce výše lze řešit velice jednoduše. Nejprve vytvořím novou větev s novými commity a poté vrátím ukazatel u mé lokální větve o X commitů zpět.

git branch newfeature
git reset --hard HEAD~x

Kde místo x dosaďte počet commitů.

Stash

Řekněme, že jste procesní šampion ve větvení, ale stále nastávají situace, kdy máte rozdělanou práci a potřebujete na chvíli odskočit udělat něco jiného. Commitovat však ještě nechcete a clonovat si nový Git repozitář vás už také nebaví. Řešení je jednoduché: stash.

stash rozdělané změny jednoduše uloží bokem a vyčistí pracovní kopii. Tím si můžete odskočit udělat fix v jiné větvi a pak se zase vrátit k rozdělané práci příkazem stash pop.

git stash
git stash pop

Dokonce, pokud se po návratu rozhodnete spíše pokračovat v nové větvi, není problém… Tohle teda patří mezi věci, které použijete asi jednou za Uherský rok. :-)

git stash branch newfeature

Edit: Pozor – existuje také git stash drop, což vaši práci zahodí. Doporučuji neplést. Případně lze používat git stash apply, jak doporučuje topa v diskuzi pod článkem.

Submoduly

Hodně lidí na ně nadává, ale to spíš proto, že jim nerozumí. Osobně se mi líbí. Pro BOObook.cz používám například Bootstrap, Font Awesome, Closure Library a další. Mohu buď přijít na jejich stránky a stáhnout si kód. Nebo si dát do mého repositáře odkaz na repositáře těchto komponent na konkrétní commit. Což je co submoduly dělají – drží cestu ke Gitu a hash commitu.

Jakou to má výhodu? Jedna, že mohu jednoduše upgradovat. Nemusím znovu stahovat kód a doufat, že jsem stáhl vše, co je potřeba. Dokonce mohu upgradovat podstatně detailněji, mohu upgradovat na konkrétní commit. Klidně i downgradovat, když na to přijde. Druhá, že mi nebobtná můj projekt. Spoustu takových komponent není zrovna malých. A třetí, pokud uvidím problém či změnu, mohu se podívat na blame a zjistit kdy a proč to v tom submodulu vzniklo.

Proto mám submoduly rád. Je ale potřeba s nimi umět pracovat.

Ten, kdo spravuje submodule, ho musí přidat: git submodule add. Poté ostatní, když si aktualizují, si musí ten modul zinicializovat (protože si stáhnou jen odkaz; teď je potřeba vycheckoutovat submoduly): git submodule init.

Ten, kdo spravuje submodule, občas musí upgradovat. Jednoduše vleze do adresáře submodulu a checkoutne se na commit, který chce. Poté se vrátí o úroveň výše do svého repositáře a tam commitne jako jakýkoliv jiný soubor. Ostatní po aktualizace uvidí, že tam je změna a musí si stáhnout změny (zase se jim stáhl jen update ukazatele), takže musí zadat: git submodule update.

A to je celý zázrak. Ten, kdo nespravuje submodule, si stačí pamatovat pouze init při prvním setkání submodulu (většinou při klonování, nové za běh projektu moc nevznikají) a update po aktualizaci ukazatele.

Pak je tedy ještě jeden příkaz: git submodule sync. To zase musí udělat každý, pokud se změnila adresa submodulu. Buď to a nebo znovu vyklonovat.

4. Týmový duch

Tagování

Ať už v týmu či v projektu o jednom vývojáři, doporučuji tagovat. Tak se dá případně kdykoliv dohledat, co přesně za kód v dané verzi byl. Je pravda, že jsem takhle dohledával pro konkrétní verzi asi jen dvakrát, ale stejně je hezké vidět releasy i v Gitu a ruce přidáním tagu neupadnou. Při vydání verze se stejně musí upravit setup.py, debianí changelog, či cokoliv jiného. Stačí tuto změnu otagovat taky verzí. V Gitu se lze pak na tag dostat jednoduše pomocí git checkout tagname.

Merge vs. rebase

Především lidi po přechodu z SVN mají tendenci pushovat po každém commitu. Za prvé – nedělejte to. Za druhé to má neblahé důsledky, protože jak jsme si řekli, jen vyclonování způsobí de facto odvětvení. Pokud potom chcete pusnout a někdo to stihl před vámi, musíte pullnout změny a pushnout znovu. To vytváří nepřehledné „kolejničky“. Co to je?

Většinou takové drobné commitování je u maintenance při opravě bugů a opravdu není nutné takové kolejničky vytvářet. Lepší je pro to použít rebase. Rebase znamená, že vaše commity odebere, fast-forwardne na poslední commit, který ve vzdálené větvi je, a poté vaše commity znovu aplikuje.

Tím se stanou dvě věci – nevytvoří se kolejničky a vytvoří se nové commity. S novým hashem. Tedy je to podobné, jako jsme probírali u ammend, – nesmí se rebasovat pushnuté commity!

Defaultně tedy git pull dělá merge. Jednoduše lze však říct, aby se raději zvolila metoda rebase:

git pull --rebase

Způsob větvení

Mít větev master jako stabilní větev nebo jako vývojovou? Mít pro každý aktivní release jednu aktivní větev? Dělat větev pro každou featuru? Dělat větev dokonce pro každý malý bug? Které všechny větve mít na vzdáleném serveru? Jak to mergovat mezi sebou?

To jsou jsou asi nejdůležitější otázky u začínajícího projektu. Nedokážu vám říct správnou odpověď. Všechny jsou správné. Záleží jen na vás, vašem kolektivu a jak to zapadá do vašeho způsobu releasování. To je krása Gitu – používáte ho tak, jak vám vyhovuje.



A už dost! S tímhle si už vystačíte.

P.S.: Možná se vám bude hodit Git Extras. Šikovná zjednodušovátka. Různé statistiky či mazání vzdálených větví apod. levou zadní.

JavaScriptové zákeřnosti

Určitě všichni znáte Watmana. Pokud si nepamatujete, určitě jste aspoň slyšeli o lightning talk Wat. A jestli ne, určitě se nejprve podívejte. Osobně mi to přijde zábavné, ale nikdy mě to nějak netrápilo (snad krom sčítání polí), protože kdo by v JavaScriptu chtěl sčítat pole s objektem nebo dva objekty? Nenapadá mě žádný rozumný příklad, kde toho využít.

Nedávno jsem na Twitteru narazil na podobnou srandičku:

Ano, může to vést k chybám, ale rozhodně to nemá nic společného s náhodou. Vysvětlení jak z videa, tak k tomuto, můžete najít na Stackoverflow tady. JavaScript se snaží nejprve přetypovat proměnné do podoby, kterou pravděpodobně potřebujeme. Vše je logicky odůvodnitelné.

Ono se dá takovéto chování pochopit, přihlédne-li se k tomu, pro co byl JavaScript stvořen – pro web. A na webu se komunikuje s uživatelem v řetězcích. Pokud vstupem je číslo, datum či cokoliv jiného, nikoliv text, musíme si to ohlídat sami. To samé se děje na backendu, kde musíme řešit stejnou věc – z requestu také dostaneme jen řetězce a kontrolu a převod do správných typů si musíme ošetřit. Teď už jen záleží, co je každému z nás bližší – zda je lepší nechat aplikaci spadnout či nějakými pravidly zkusit odhadnout, co dělat, a pokračovat.

Ať tak či onak, v obou případech si musíme nějak pomoct a nedoufat na automatiku daného jazyka. Osobně si myslím, že na klientské straně je toto chování asi i lepší. Při chybě je možnost, že aplikace poběží bez problémů dál (nebo taky může spadnou o kousek dál a pak je sranda najít, kde chyba vznikla).

Každopádně jsem ve stejný den narazil ještě na jeden tweet:

To mě docela překvapilo. Vysvětlení dává zase smysl. Tady ale už srandičky končí.

JavaScript se mi líbí, ale některé jeho nástrahy jsou zákeřné. Nejen ty výše zmíněné, další za zmínění stojí třeba porovnání polí  ([1,2,3] == [1,2,3] // false), či že null je object (typeof null // "object")… Z toho důvodu rád používám CoffeeScript s Closure Library. Některé zákeřnosti si tak odfiltruji, ale přitom se nijak nevzdávám žádných vlastností jazyka. A vám doporučuji udělat to samé. Ne nutně to samé, co používám já, ani co opěvuje někdo jiný. Sáhněte po něčem, co vám sedne. Možností je dost: CoffeeScript, TypeScript, Dart, Closure Library, … (pozor – jQuery zde není řešením) Důležité je něco si vybrat a používat.