Univerzální testovací nástroj Robot Framework a BDD testy

10. 12. 2019
Doba čtení: 23 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
Ve druhém článku o Robot Framework si ukážeme další možnosti, které lze využít při psaní testovacích scénářů. Zmíníme se mj. i o použití Robot Frameworku při tvorbě BDD testů, pro něž se běžně používá jazyk Gherkin.

Obsah

1. Univerzální testovací nástroj Robot Framework a BDD testy

2. Zápis testovacích scénářů

3. Atributy objektů použitých pro implementaci kroků testů

4. Platnost objektů vytvářených nástrojem Robot Framework

5. Uchování hodnoty akumulátoru mezi jednotlivými testy

6. Použití deklarace ROBOT_LIBRARY_SCOPE

7. Globální oblast platnosti objektu

8. Podpora operací typu setup a teardown

9. Explicitní zákaz operace typu setup nebo teardown

10. Testy řízené tabulkami

11. Celý testovací scénář založený na tabulce

12. Alternativní zápis testovacího scénáře

13. Jazyk Gherkin

14. Využití Robot Frameworku ve funkci nadmnožiny jazyka Gherkin

15. Praktický příklad: test třídy Accumulator6

16. Použití spojky and

17. BDD testy řízené tabulkou

18. Vylepšení předchozího příkladu

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Univerzální testovací nástroj Robot Framework a BDD testy

Na úvodní článek, v němž jsme se seznámili s některými základními koncepty, na nichž je postaven nástroj Robot Framework, dnes navážeme. Představíme si některé další možnosti, které je možné využít při psaní testovacích scénářů, zejména tvorbu testů založených na tabulkách se vstupními daty a očekávanými výsledky. Zmíníme se mj. i o použití Robot Frameworku při psaní BDD testů, pro něž se běžně používá spíše doménově specifický jazyk (DSL – Domain Specific Language) nazvaný Gherkin. Implementaci jednotlivých kroků testů postavíme opět na programovacím jazyku Python, protože se jedná o primární jazyk, pro nějž je Robot Framework určen (druhým podporovaným jazykem je Java, ovšem Python se v této oblasti s velkou pravděpodobností používá mnohem častěji).

Obrázek 1: Příklad vygenerované HTML stránky s výsledky testů vytvořených a spuštěných v Robot Frameworku.

2. Zápis testovacích scénářů

Připomeňme si ve stručnosti, jak vypadal poslední (plně funkční) test, s nímž jsme se seznámili v samotném závěru předchozího článku. Jednalo se o velmi jednoduchý test, který zjišťoval, zda se provádí korektně aritmetická (celočíselná) operace součtu v programovacím jazyku Python. Jedná se tedy o pouhý demonstrační příklad zvolený z toho důvodu, že je dostatečně jednoduchý a přehledný (v praxi by se spíše testovala minimálně celá třída). Jednotlivé kroky testů, z nichž se skládá testovací scénář, jsou zapsány s využitím takzvaných klíčových slov (keywords) a jejich parametrů, přičemž klíčová slova i jednotlivé parametry od sebe musí být odděleny minimálně dvěma mezerami nebo znakem Tab (protože samotné klíčové slovo je obecně tvořeno několika slovy oddělenými od sebe jedinou mezerou, takže je nutné rozlišit hranici slova od běžných mezer).

Samotný testovací scénář prezentovaný minule byl zapsán následujícím způsobem:

*** Settings ***
Library         Test16.py
 
*** Test Cases ***
Adder #1
    Add  1  2
    Result should be  3
 
Adder #2
    Add  0  0
    Result should be  0
 
Adder #3
    Add  1  -1
    Result should be  0

S výsledkem po spuštění celého scénáře příkazem robot:

$ robot test16.robot
 
==============================================================================
Test16
==============================================================================
Adder #1                                                              | PASS |
------------------------------------------------------------------------------
Adder #2                                                              | PASS |
------------------------------------------------------------------------------
Adder #3                                                              | PASS |
------------------------------------------------------------------------------
Test16                                                                | PASS |
3 critical tests, 3 passed, 0 failed
3 tests total, 3 passed, 0 failed
==============================================================================
Output:  /home/tester/src/Python/robot-framework-examples/article_01/output.xml
Log:     /home/tester/src/Python/robot-framework-examples/article_01/log.html
Report:  /home/tester/src/Python/robot-framework-examples/article_01/report.html

Alternativně je možné v případě potřeby použít odlišný způsob zápisu, v němž se explicitně oddělují jednotlivé sloupce tabulky pomocí znaku „|“. Právě na tomto zápisu je velmi dobře patrné oddělení klíčových slov od jejich parametrů i rozdělení jednotlivých parametrů do samostatných sloupců:

| *** Settings ***   |                  |   |
| Library            | Test16.py        |   |
|                    |                  |   |
| *** Test Cases *** |                  |   |
| Adder #1           |                  |   |
|                    | Add              | 1 | 2
|                    | Result should be | 3 |
|                    |                  |   |
| Adder #2           |                  |   |
|                    | Add              | 0 | 0
|                    | Result should be | 0 |
|                    |                  |   |
| Adder #3           |                  |   |
|                    | Add              | 1 | -1
|                    | Result should be | 0 |

Ve skutečnosti je však možné tabulku dále rozdělit na menší části oddělené jedním či větším množstvím prázdných řádků, což může zvýšit čitelnost:

| *** Settings ***   |                  |   |
| Library            | Test16.py        |   |
 
| *** Test Cases *** |                  |   |
| Adder #1           |                  |   |
|                    | Add              | 1 | 2
|                    | Result should be | 3 |
|                    | Add              | 2 | 3
|                    | Result should be | 5 |
|                    | Add              | 4 | 5
|                    | Result should be | 9 |
 
| Adder #2           |                  |   |
|                    | Add              | 0 | 0
|                    | Result should be | 0 |
 
| Adder #3           |                  |   |
|                    | Add              | 1 | -1
|                    | Result should be | 0 |

3. Atributy objektů použitých pro implementaci kroků testů

Jednotlivé kroky testů, které jsou v testovacím scénáři reprezentovány klíčovými slovy „Add“ a „Result should be“, byly implementovány ve formě běžných metod třídy Test16. Jméno této třídy přitom musí odpovídat jménu souboru, v němž je zdrojový kód třídy uložen (což do značné míry odpovídá konvencím programovacího jazyka Java v případě veřejných tříd). Pokud by tomu tak nebylo, je nutné změnit parametr Library takovým způsobem, aby obsahoval jak jméno souboru, tak i jméno třídy. Samotná implementace této třídy může vypadat následovně:

class Test16:
    def __init__(self):
        print("INIT")
 
    def add(self, x, y):
        self.result = int(x) + int(y)
 
    def result_should_be(self, expected):
        assert self.result == int(expected), "{} != {}".format(self.result, expected)
Poznámka: povšimněte si, že klíčová slova jsou realizována „běžnými“ metodami, tedy nikoli metodami statickými či třídními. Jejich prvním parametrem je tedy self, jenž je předáván automaticky (nepíše se do testovacího scénáře).

Tento test si připomínáme z jednoho důvodu – můžeme si na jeho příkladu vysvětlit, jakým způsobem se pracuje s atributy objektů použitých pro implementaci jednotlivých kroků testů. Náš test pracuje s jediným atributem nazvaným result, který reprezentuje stav objektu typu Test16. Tento atribut je nastavován v metodě add a tedy (z pohledu testera) v klíčovém slově „Add“; přistupujeme k němu v metodě result_should_be, tedy na straně testů v klíčovém slově „Result should be“. Bude nám tedy dostačovat, aby byl atribut platný mezi zavoláním těchto dvou metod v rámci jednoho test case.

4. Platnost objektů vytvářených nástrojem Robot Framework

Ve výchozím nastavení jsou objekty (v předchozím příkladu instance třídy Test16) vytvořeny pro každý test case. Mezi jednotlivými test casy tedy není možné sdílet stav, pokud se samozřejmě neuchýlíme k použití globálních proměnných atd. Toto chování je většinou plně vyhovující a umožňuje nám psát například následující testovací scénáře, které počítají s tím, že výchozí hodnota akumulátoru bude na začátku každého test case vynulována:

| *** Settings ***   |                             |   |
| Library            | Accumulator1.py             |   |
|                    |                             |   |
| *** Test Cases *** |                             |   |
| Initial value      |                             |   |
|                    | Accumulator value should be | 0 |
|                    |                             |   |
| Adder #1           | Add value                   | 1 |
|                    | Accumulator value should be | 1 |
|                    | Add value                   | 2 |
|                    | Accumulator value should be | 3 |

Implementace třídy Accumulator využívá toho, že pro každý test case je vytvořen nový objekt a je tudíž možné hodnotu akumulátoru inicializovat přímo v konstruktoru. Deklarace obou metod realizujících klíčová slova je již triviální:

class Accumulator1:
    def __init__(self):
        print("INIT")
        self.result = 0
 
    def add_value(self, value):
        self.result += int(value)
 
    def accumulator_value_should_be(self, expected):
        assert self.result == int(expected), "{} != {}".format(self.result, expected)

Tento testovací scénář se dvěma testy je plně funkční, o čemž se můžeme snadno přesvědčit:

$ robot test01.robot
 
==============================================================================
Test01
==============================================================================
Initial value                                                         | PASS |
------------------------------------------------------------------------------
Adder #1                                                              | PASS |
------------------------------------------------------------------------------
Test01                                                                | PASS |
2 critical tests, 2 passed, 0 failed
2 tests total, 2 passed, 0 failed
==============================================================================
Output:  /home/tester/src/Python/robot-framework-examples/article_02/output.xml
Log:     /home/tester/src/Python/robot-framework-examples/article_02/log.html
Report:  /home/tester/src/Python/robot-framework-examples/article_02/report.html

5. Uchování hodnoty akumulátoru mezi jednotlivými testy

V některých případech – i když jich nebude v praxi velmi mnoho – by bylo vhodné, aby se hodnota akumulátoru zapamatovala a zůstala zachována i mezi jednotlivými testy. Umožnilo by nám to zapsat například následující testovací scénář, který k akumulátoru postupně přičítá další hodnoty a přitom sleduje mezivýsledky:

| *** Settings ***   |                             |   |
| Library            | Accumulator2.py             |   |
|                    |                             |   |
| *** Test Cases *** |                             |   |
| Initial value      |                             |   |
|                    | Accumulator value should be | 0 |
|                    |                             |   |
| Adder #1           | Add value                   | 1 |
|                    | Accumulator value should be | 1 |
|                    |                             |   |
| Adder #2           | Add value                   | 2 |
|                    | Accumulator value should be | 3 |

V případě, že nezměníme implementaci třídy Accumulator2, nebude takto zapsaný scénář proveden korektně:

class Accumulator2:
    def __init__(self):
        print("INIT")
        self.result = 0
 
    def add_value(self, value):
        self.result += int(value)
 
    def accumulator_value_should_be(self, expected):
        assert self.result == int(expected), "{} != {}".format(self.result, expected)

Samozřejmě si opět můžeme odzkoušet, jak se bude testovací scénář chovat po jeho spuštění v Robot Frameworku:

$ robot test02.robot
 
==============================================================================
Test02
==============================================================================
Initial value                                                         | PASS |
------------------------------------------------------------------------------
Adder #1                                                              | PASS |
------------------------------------------------------------------------------
Adder #2                                                              | FAIL |
2 != 3
------------------------------------------------------------------------------
Test02                                                                | FAIL |
3 critical tests, 2 passed, 1 failed
3 tests total, 2 passed, 1 failed
==============================================================================
Output:  /home/tester/src/Python/robot-framework-examples/article_02/output.xml
Log:     /home/tester/src/Python/robot-framework-examples/article_02/log.html
Report:  /home/tester/src/Python/robot-framework-examples/article_02/report.html
Poznámka: chybové hlášení znamená, že po přičtení dvojky k obsahu akumulátoru (ten je nulový) bude jeho novou hodnotou pochopitelně taktéž dvojka a nikoli hodnota 3.

Toto chování je očekávatelné, protože pro každý test case byla vytvořena nová instance třídy Accumulator2 a tím pádem došlo i k inicializaci výchozí hodnoty akumulátoru.

6. Použití deklarace ROBOT_LIBRARY_SCOPE

Existuje však možnost, jak zajistit, aby se hodnota akumulátoru zachovala i mezi jednotlivými test casy. Můžeme totiž určit, že nějaký zvolený objekt (v našem případě instance třídy Accumulator3 bude zkonstruován jen jedenkrát a bude platný pro celý testovací scénář. Stačí k tomu maličkost – deklarovat třídní atribut nazvaný ROBOT_LIBRARY_SCOPE a nastavit ho na řetězec „TEST SUITE“. Povšimněte si, že není nutné provádět žádné importy dalších knihoven, což by bylo zapotřebí například v případě, že by Robot Framework používal anotace atd.:

class Accumulator3:
 
    ROBOT_LIBRARY_SCOPE = 'TEST SUITE'
 
    def __init__(self):
        print("INIT")
        self.result = 0
 
    def add_value(self, value):
        self.result += int(value)
 
    def accumulator_value_should_be(self, expected):
        assert self.result == int(expected), "{} != {}".format(self.result, expected)

Samotný zápis testovacího scénáře se prakticky nezmění, pochopitelně až na nutnost specifikace jiné knihovny s implementací klíčových slov:

| *** Settings ***   |                             |   |
| Library            | Accumulator3.py             |   |
|                    |                             |   |
| *** Test Cases *** |                             |   |
| Initial value      |                             |   |
|                    | Accumulator value should be | 0 |
|                    |                             |   |
| Adder #1           | Add value                   | 1 |
|                    | Accumulator value should be | 1 |
|                    |                             |   |
| Adder #2           | Add value                   | 2 |
|                    | Accumulator value should be | 3 |

Nyní ovšem celý testovací scénář proběhne v pořádku, což si ověříme na dalším výpisu:

$ robot test03.robot
 
==============================================================================
Test03
==============================================================================
Initial value                                                         | PASS |
------------------------------------------------------------------------------
Adder #1                                                              | PASS |
------------------------------------------------------------------------------
Adder #2                                                              | PASS |
------------------------------------------------------------------------------
Test03                                                                | PASS |
3 critical tests, 3 passed, 0 failed
3 tests total, 3 passed, 0 failed
==============================================================================
Output:  /home/tester/src/Python/robot-framework-examples/article_02/output.xml
Log:     /home/tester/src/Python/robot-framework-examples/article_02/log.html
Report:  /home/tester/src/Python/robot-framework-examples/article_02/report.html

7. Globální oblast platnosti objektu

Existuje ještě jedna možnost specifikace platnosti objektu (tedy instance nějaké třídy vytvořené samotným Robot Frameworkem). Můžeme totiž nastavit, že objekt bude globálně platný a tedy bude existovat napříč všemi testovacími scénáři, které budou spuštěny. Postačuje definovat třídní atribut ROBOT_LIBRARY_SCOPE a nastavit ho na řetězec „GLOBAL“ tak, jak je to ukázáno v dnešním čtvrtém demonstračním příkladu:

class Accumulator4:
 
    ROBOT_LIBRARY_SCOPE = 'GLOBAL'
 
    def __init__(self):
        print("INIT")
        self.result = 0
 
    def add_value(self, value):
        self.result += int(value)
 
    def accumulator_value_should_be(self, expected):
        assert self.result == int(expected), "{} != {}".format(self.result, expected)

Chování si můžeme ověřit na tomto testovacím scénáři:

| *** Settings ***   |                             |   |
| Library            | Accumulator4.py             |   |
|                    |                             |   |
| *** Test Cases *** |                             |   |
| Initial value      |                             |   |
|                    | Accumulator value should be | 0 |
|                    |                             |   |
| Adder #1           | Add value                   | 1 |
|                    | Accumulator value should be | 1 |
|                    |                             |   |
| Adder #2           | Add value                   | 2 |
|                    | Accumulator value should be | 3 |

S očekávaným výsledkem:

$ robot test04.robot
 
==============================================================================
Test04
==============================================================================
Initial value                                                         | PASS |
------------------------------------------------------------------------------
Adder #1                                                              | PASS |
------------------------------------------------------------------------------
Adder #2                                                              | PASS |
------------------------------------------------------------------------------
Test04                                                                | PASS |
3 critical tests, 3 passed, 0 failed
3 tests total, 3 passed, 0 failed
==============================================================================
Output:  /home/tester/src/Python/robot-framework-examples/article_02/output.xml
Log:     /home/tester/src/Python/robot-framework-examples/article_02/log.html
Report:  /home/tester/src/Python/robot-framework-examples/article_02/report.html

8. Podpora operací typu setup a teardown

V prakticky všech knihovnách a frameworcích určených pro tvorbu a spouštění různých typů testů se setkáme s možností specifikace operací, které se mají provést před každým testem, popř. naopak po doběhnutí testu. Většinou jsou tyto operace definovány ve funkcích nebo metodách nazvaných setup a teardown. V Robot Frameworku tuto možnost máme taky, přičemž se operace typu setup a teardown definují v sekci Settings, tedy na začátku testovacího scénáře. Tyto operace lze implementovat libovolným slovem (keyword), přičemž tato slova mohou mít i parametry, pokud je to vyžadováno.

V dalším demonstračním příkladu pro operaci setup použijeme klíčové slovo Setup method s parametrem 0 a pro operaci teardown použijeme klíčové slovo Teardown method, tentokrát bez parametrů:

| *** Settings ***   |                             |   |
| Library            | Accumulator5.py             |   |
| Test setup         | Setup method                | 0 |
| Test teardown      | Teardown method             |   |
|                    |                             |   |
| *** Test Cases *** |                             |   |
| Initial value      |                             |   |
|                    | Accumulator value should be | 0 |
|                    |                             |   |
| Adder #1           | Add value                   | 1 |
|                    | Accumulator value should be | 1 |
|                    |                             |   |
| Adder #2           | Add value                   | 2 |
|                    | Accumulator value should be | 3 |

Implementace obou výše zmíněných klíčových slov se ve skutečnosti žádným způsobem neliší od implementací slov jiných – bude se jednat o běžné metody třídy Accumulator5:

class Accumulator5:
 
    ROBOT_LIBRARY_SCOPE = 'GLOBAL'
 
    def __init__(self):
        print("INIT")
        self.result = None
 
    def setup_method(self, value):
        self.result = int(value)
 
    def teardown_method(self):
        pass
 
    def add_value(self, value):
        self.result += int(value)
 
    def accumulator_value_should_be(self, expected):
        assert self.result == int(expected), "{} != {}".format(self.result, expected)

Na začátku každého testu (přesněji test casu) se inicializuje atribut result na nulovou hodnotu, takže je zřejmé, že tentokrát budou později spuštěné testy havarovat, a to i přesto, že je hodnota tohoto atributu zachována i mezi jednotlivými testy (ve skutečnosti je sice hodnota zachována, ale na začátku testu je atribut explicitně vynulován):

$ robot test05.robot
 
==============================================================================
Test05
==============================================================================
Initial value                                                         | PASS |
------------------------------------------------------------------------------
Adder #1                                                              | PASS |
------------------------------------------------------------------------------
Adder #2                                                              | FAIL |
2 != 3
------------------------------------------------------------------------------
Test05                                                                | FAIL |
3 critical tests, 2 passed, 1 failed
3 tests total, 2 passed, 1 failed
==============================================================================
Output:  /home/tester/src/Python/robot-framework-examples/article_02/output.xml
Log:     /home/tester/src/Python/robot-framework-examples/article_02/log.html
Report:  /home/tester/src/Python/robot-framework-examples/article_02/report.html

9. Explicitní zákaz operace typu setup nebo teardown

V některých situacích budeme potřebovat, aby se operace typu setup nebo teardown vůbec neprovedly, nebo aby se sice provedly, ale s jinými parametry. I tohoto chování je možné v Robot Frameworku dosáhnout, a to tímto zápisem:

| Adder #2           | [Setup]                     | NONE |
|                    | Add value                   | 2    |
|                    | Accumulator value should be | 3    |

Výše uvedený zvýrazněný řádek zakazoval pro jeden konkrétní test (Adder #2) provedení operace typu setup. Tímto způsobem můžeme mnohdy i dosti zásadním způsobem ovlivnit spouštění jednotlivých testů, popř. modifikovat jejich kontext (tedy prostředí, v jakém se testy spouští):

| *** Settings ***   |                             |      |
| Library            | Accumulator6.py             |      |
| Test setup         | Setup method                | 0    |
| Test teardown      | Teardown method             |      |
|                    |                             |      |
| *** Test Cases *** |                             |      |
| Initial value      |                             |      |
|                    | Accumulator value should be | 0    |
|                    |                             |      |
| Adder #1           | Add value                   | 1    |
|                    | Accumulator value should be | 1    |
|                    |                             |      |
| Adder #2           | [Setup]                     | NONE |
|                    | Add value                   | 2    |
|                    | Accumulator value should be | 3    |
|                    |                             |      |
| Adder #3           |                             |      |
|                    | Add value                   | 2    |
|                    | Accumulator value should be | 5    |

Implementace knihovny Accumulator6 může zůstat stejná, jako tomu bylo v knihovně Accumulator5, tedy:

class Accumulator6:
 
    ROBOT_LIBRARY_SCOPE = 'GLOBAL'
 
    def __init__(self):
        print("INIT")
        self.result = None
 
    def setup_method(self, value):
        self.result = int(value)
 
    def teardown_method(self):
        pass
 
    def add_value(self, value):
        self.result += int(value)
 
    def accumulator_value_should_be(self, expected):
        assert self.result == int(expected), "{} != {}".format(self.result, expected)

Nyní by první tři testy měly proběhnout korektně; zhavarovat by měl až test čtvrtý:

$ robot test06.robot
 
==============================================================================
Test06
==============================================================================
Initial value                                                         | PASS |
------------------------------------------------------------------------------
Adder #1                                                              | PASS |
------------------------------------------------------------------------------
Adder #2                                                              | PASS |
------------------------------------------------------------------------------
Adder #3                                                              | FAIL |
2 != 5
------------------------------------------------------------------------------
Test06                                                                | FAIL |
4 critical tests, 3 passed, 1 failed
4 tests total, 3 passed, 1 failed
==============================================================================
Output:  /home/tester/src/Python/robot-framework-examples/article_02/output.xml
Log:     /home/tester/src/Python/robot-framework-examples/article_02/log.html
Report:  /home/tester/src/Python/robot-framework-examples/article_02/report.html

10. Testy řízené tabulkami

Jedna z předností nástroje Robot Framework spočívá v tom, že umožňuje tvořit testy řízené tabulkami. Jedná se o možnost zapsat nějaké vstupní hodnoty (parametry) a očekávané výsledky pro tyto vstupní hodnoty formou nějaké tabulky. V našem konkrétním případě, pokud budeme chtít testovat implementaci třídy s akumulátorem, je možné použít například tento zápis:

| *** Test Cases *** | Value    | Expected |
| Test1              | 0        | 0        |
|                    | 1        | 1        |
|                    | 10       | 11       |
|                    | -10      | 1        |
|                    | 1        | 2        |
|                    | 1        | 3        |

Ve sloupci Value je uložena hodnota přičítaná do akumulátoru, ve sloupci Expected pak očekávaná (výsledná) hodnota uložená v akumulátoru.

To ovšem pochopitelně není vše, protože je nutné Robot Frameworku oznámit, jakým způsobem se mají hodnoty zapsané do tabulky namapovat na konkrétní klíčová slova neboli kroky testu. To můžeme provést v sekci Keywords (i když existují i další možnosti), a to následujícím způsobem, který zaručí, že se každý řádek předchozí tabulky s testem přepíše na dvojici klíčových slov Add value a Accumulator value should be:

| *** Keywords ***   |                             |             |             |
| Accumulate         |                             |             |             |
|                    | [Arguments]                 | ${value}    | ${expected} |
|                    | Add value                   | ${value}    |             |
|                    | Accumulator value should be | ${expected} |             |
Poznámka: zápisem ${value} a ${expected} se specifikují proměnné spravované přímo Robot Frameworkem.

11. Celý testovací scénář založený na tabulce

Ukažme si nyní, jak by mohl vypadat test třídy Accumulator (přesněji řečeno její šesté iterace Accumulator6) přepsaný do formátu, v němž se bude využívat tabulka se vstupními parametry a očekávanými výsledky testů. Tabulka bude mít dva sloupce nazvané Value a Expected, přičemž každý řádek této tabulky bude expandován do dvou kroků testu způsobem, který jsme si popsali v předchozí kapitole:

| *** Settings ***   |                             |             |             |
| Library            | Accumulator6.py             |             |             |
| Test template      | Accumulate                  |             |             |
| Test setup         | Setup method                | 0           |             |
| Test teardown      | Teardown method             |             |             |
|                    |                             |             |             |
| *** Test Cases *** | Value                       | Expected    |             |
| Test1              | 0                           | 0           |             |
|                    | 1                           | 1           |             |
|                    | 10                          | 11          |             |
|                    | -10                         | 1           |             |
|                    | 1                           | 2           |             |
|                    | 1                           | 3           |             |
|                    |                             |             |             |
| *** Keywords ***   |                             |             |             |
| Accumulate         |                             |             |             |
|                    | [Arguments]                 | ${value}    | ${expected} |
|                    | Add value                   | ${value}    |             |
|                    | Accumulator value should be | ${expected} |             |

Po spuštění tohoto testu by měly všechny kroky testovacího scénáře proběhnout korektně (bez pádu):

$ robot test07.robot
 
==============================================================================
Test07
==============================================================================
Test1                                                                 | PASS |
------------------------------------------------------------------------------
Test07                                                                | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed
==============================================================================
Output:  /home/tester/src/Python/robot-framework-examples/article_02/output.xml
Log:     /home/tester/src/Python/robot-framework-examples/article_02/log.html
Report:  /home/tester/src/Python/robot-framework-examples/article_02/report.html
Poznámka: opět platí, že testovací scénář lze rozdělit na několik tabulek a použít odlišný, i když sémanticky ekvivalentní zápis:
| *** Settings ***   |                   |    |
| Library            | Accumulator6.py   |    |
| Test template      | Accumulate        |    |
| Test setup         | Setup method      | 0  |
| Test teardown      | Teardown method   |    |
 
| *** Test Cases *** | Value             | Expected    |
| Test1              | 0                 | 0           |
|                    | 1                 | 1           |
|                    | 10                | 11          |
|                    | -10               | 1           |
|                    | 1                 | 2           |
|                    | 1                 | 3           |
 
| *** Keywords ***   |                             |             |             |
| Accumulate         |                             |             |             |
|                    | [Arguments]                 | ${value}    | ${expected} |
|                    | Add value                   | ${value}    |             |
|                    | Accumulator value should be | ${expected} |             |

12. Alternativní zápis testovacího scénáře

Předchozí testovací scénář můžeme zapsat i nepatrně odlišným způsobem. Nemusí se totiž jednat o tabulku se sloupci oddělenými znakem „|“, ale o použití mezer (či sady mezer) pro oddělení jednotlivých sloupců dvou tabulek (Test Cases a Keywords):

*** Settings ***
Library             Accumulator6.py
Test template       Accumulate
Test setup          Setup method  0
Test teardown       Teardown method
 
*** Test Cases ***  Value      Expected
Test1               0          0
                    1          1
                    10         11
                    -10        1
                    1          2
                    1          3
 
*** Keywords ***
Accumulate
                    [Arguments]                  ${value}     ${expected}
                    Add value                    ${value}
                    Accumulator value should be  ${expected}

I přes odlišný způsob zápisu testovacího scénáře bude výsledek totožný s předchozím testem:

==============================================================================
Test08
==============================================================================
Test1                                                                 | PASS |
------------------------------------------------------------------------------
Test08                                                                | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed
==============================================================================
Output:  /home/tester/src/Python/robot-framework-examples/article_02/output.xml
Log:     /home/tester/src/Python/robot-framework-examples/article_02/log.html
Report:  /home/tester/src/Python/robot-framework-examples/article_02/report.html

13. Jazyk Gkerkin

Dalším tématem, kterým se dnes budeme zabývat, je tvorba BDD (behavior) testů, neboli testů zjišťujících, zda se testovaný systém chová podle popsaných předpokladů. Tyto testy lze vytvářet různým způsobem, ovšem v této oblasti se velmi často setkáme s použitím doménově specifického jazyka nazvaného Gherkin. Tento jazyk odstiňuje autora testů od vlastní implementace systému i od programovacího jazyka (či jazyků), v nichž je systém vytvořen. Ostatně v Gherkinu lze popsat očekávané chování prakticky jakéhokoli systému, který dokonce nemusí mít nic společného s IT. Testovací scénář vytvořený v Gherkinu může vypadat následovně:

Obrázek 2: Ukázka scénářů napsaných v jazyce Gherkin.

Na předchozím screenshotu jsou zvýrazněna klíčová slova uvozující jednotlivé kroky testu. Ostatní slova a číslice ve větách jsou buď pevně daná (svázaná s konkrétním krokem), nebo se jedná o proměnné. Ve scénáři je i tabulka, jejíž obsah se řádek po řádku postupně stává obsahem jednotlivých kroků testu (obsahem tabulky se nahrazují slova umístěná do ostrých závorek).

Poznámka: jazyk Gherkin existuje v různých jazykových mutacích, my se však budeme držet jeho originální anglické varianty. Ostatně mnoho knihoven, které Gherkin podporují, pracuje pouze s anglickou variantou.

Jednotlivé kroky testu napsané v jazyce Gherkin je samozřejmě nutné nějakým způsobem implementovat. Způsob, jakým lze implementaci zařídit v nástroji Robot Framework, bude popsán v navazujících kapitolách.

S jazykem Gherkin a se způsobem jeho použití jsme se již na stránkách Rootu několikrát setkali, protože jsme si ukázali implementaci Gherkinu jak pro programovací jazyk Clojure, tak i pro Python a dokonce i pro jazyk Go. Podrobnější informace o těchto implementacích naleznete v následujících článcích:

  1. Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure
    https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure/
  2. Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure (2)
    https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure-2/
  3. Behavior-driven development v Pythonu s využitím knihovny Behave
    https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave/
  4. Behavior-driven development v Pythonu s využitím knihovny Behave (druhá část)
    https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-druha-cast/
  5. Behavior-driven development v Pythonu s využitím knihovny Behave (závěrečná část)
    https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-zaverecna-cast/
  6. Tvorba BDD testů s využitím jazyka Go a nástroje godog
    https://www.root.cz/clanky/tvorba-bdd-testu-s-vyuzitim-jazyka-go-a-nastroje-godog/

14. Využití Robot Frameworku ve funkci nadmnožiny jazyka Gherkin

Testovací scénář pro třídu Accumulator6 lze přepsat do podoby BDD testů, a to jazykem, který připomíná Gherkin. Ústřední část bude vypadat následovně:

*** Test Cases ***
Accumulate
    Given accumulator has been zeroed
    When I add "1" to accumulator
    Then the accumulated value should be "1"
    When I add "1" to accumulator
    Then the accumulated value should be "2"
    When I add "-10" to accumulator
    Then the accumulated value should be "-8"

Ovšem podobně jako u předchozích testů řízených tabulkami je i zde nutné specifikovat klíčová slova za „Given“, „When“ a „Then“, a to konkrétně v sekci Keywords:

*** Keywords ***
Accumulator has been zeroed
    log  accumulator init
 
I add "${value}" to accumulator
    Add value  ${value}
 
Then the accumulated value should be "${expected}"
    Accumulator value should be  ${expected}

Tímto způsobem vytvořenou kostru testů již můžeme použít v reálném testovacím scénáři.

Poznámka: povšimněte si, že v implementaci klíčových slov se nepoužívají první slova z BDD testů, tedy „Given“, „When“ a „Then“.

15. Praktický příklad: test třídy Accumulator6

Předchozí testy třídy s implementací jednoduchého akumulátoru je možné přepsat do formy BDD připomínající (ve vlastní části s popisem testů) jazyk Gherkin. Výsledek, a to plně funkční, může vypadat například takto:

*** Settings ***
Library             Accumulator6.py
Test setup          Setup method  0
Test teardown       Teardown method
 
*** Test Cases ***
Accumulate
    Given accumulator has been zeroed
    When I add "1" to accumulator
    Then the accumulated value should be "1"
    When I add "1" to accumulator
    Then the accumulated value should be "2"
    When I add "-10" to accumulator
    Then the accumulated value should be "-8"
 
*** Keywords ***
Accumulator has been zeroed
    log  accumulator init
 
I add "${value}" to accumulator
    Add value  ${value}
 
Then the accumulated value should be "${expected}"
    Accumulator value should be  ${expected}

Tento testovací scénář je plně funkční:

==============================================================================
Test09
==============================================================================
Accumulate                                                            | PASS |
------------------------------------------------------------------------------
Test09                                                                | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed
==============================================================================
Output:  /home/tester/src/Python/robot-framework-examples/article_02/output.xml
Log:     /home/tester/src/Python/robot-framework-examples/article_02/log.html
Report:  /home/tester/src/Python/robot-framework-examples/article_02/report.html

Obrázek 3: Výsledky běhu BDD testů.

Obrázek 4: Ve výpisu jednotlivých kroků vidíme, jak se věty z BDD testů namapovaly na konkrétní klíčová slova.

16. Použití spojky and

V BDD testech je dobrým zvykem nahrazovat sekvenci vět začínajících stejným slovem „When“ či „Then“:

When I add "1" to accumulator
When I add "1" to accumulator
When I add "-10" to accumulator
Then the accumulated value should be "-8"

spojkou And, která je čitelnější:

When I add "1" to accumulator
 And I add "1" to accumulator
 And I add "-10" to accumulator
Then the accumulated value should be "-8"

I tato vlastnost je v Robot Frameworku podporována, takže můžeme psát:

*** Settings ***
Library             Accumulator6.py
Test setup          Setup method  0
Test teardown       Teardown method
 
*** Test Cases ***
Accumulate
    Given accumulator has been zeroed
    When I add "1" to accumulator
     And I add "1" to accumulator
     And I add "-10" to accumulator
    Then the accumulated value should be "-8"
 
*** Keywords ***
Accumulator has been zeroed
    log  accumulator init
 
I add "${value}" to accumulator
    Add value  ${value}
 
Then the accumulated value should be "${expected}"
    Accumulator value should be  ${expected}

Výsledek takto upraveného testovacího scénáře:

==============================================================================
Test10
==============================================================================
Accumulate                                                            | PASS |
------------------------------------------------------------------------------
Test10                                                                | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed
==============================================================================
Output:  /home/tester/src/Python/robot-framework-examples/article_02/output.xml
Log:     /home/tester/src/Python/robot-framework-examples/article_02/log.html
Report:  /home/tester/src/Python/robot-framework-examples/article_02/report.html

17. BDD testy řízené tabulkou

I BDD testy je možné řídit tabulkami. Zde se ovšem setkáme s určitým zkomplikováním testů, protože je nutné použít dvojího mapování klíčových slov. Nejdříve se z tabulky vytvoří jednotlivé kroky testu, které jsou ovšem popsány v jazyku Gherkin. A následně se věty z Gherkinu mapují na nová uživatelsky definovaná klíčová slova. Výsledek může vypadat následovně:

zabbix_tip

*** Settings ***
Library             Accumulator6.py
Test setup          Setup method  0
Test teardown       Teardown method
Test template       Accumulator operation
 
*** Test Cases ***     Value  Expected
Accumulator operation  1      1
Accumulator operation  2      2
 
*** Keywords ***
Accumulator operation
                    [Arguments]  ${value}  ${expected}
                    Given accumulator has been zeroed
                    When I add ${value} to accumulator
                    Then the accumulated value should be ${expected}
 
Accumulator has been zeroed
    log  accumulator init
 
I add ${value} to accumulator
    Add value  ${value}
 
Then the accumulated value should be ${expected}
    Accumulator value should be  ${expected}

18. Vylepšení předchozího příkladu

Předchozí příklad lze vylepšit a učinit více čitelným. Nejdříve zapíšeme vlastní testovací scénář, tj. BDD část s tabulkou, a posléze tu méně důležitou část, tedy mapování vět v BDD/Gherkinu na konkrétní klíčová slova:

*** Settings ***
Library             Accumulator6.py
Test setup          Setup method  0
Test teardown       Teardown method
Test template       Accumulator operation
 
*** Keywords ***
Accumulator operation
                    [Arguments]  ${value}  ${expected}
                    Given accumulator has been zeroed
                    When I add ${value} to accumulator
                    Then the accumulated value Should Be ${expected}
 
*** Test Cases ***     Value  Expected
Accumulator operation  1      1
Accumulator operation  10     10
 
 
 
*** Keywords ***
Accumulator has been zeroed
    log  accumulator init
 
I add ${value} to accumulator
    Add value  ${value}
 
Then the accumulated value should be ${expected}
    Accumulator value should be  ${expected}
Poznámka: na posledních dvou příkladech je patrná velká flexibilita Robot Frameworku.

19. Repositář s demonstračními příklady

Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/robot-framework-examples (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:

# Příklad Stručný popis Cesta
1 test01.robot první test akumulátoru implementovaného ve třídě Accumulator1 https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test01.robot
2 Accumulator1.py první implementace akumulátoru https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/Accumulator1.py
3 test02.robot několik na sobě nezávislých testů https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test02.robot
4 Accumulator2.py druhá implementace akumulátoru https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/Accumulator2.py
5 test03.robot několik navzájem závislých testů https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test03.robot
6 Accumulator3.py změna rozsahu platnosti objektu s akumulátorem https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/Accumulator3.py
7 test04.robot několik navzájem závislých testů https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test04.robot
8 Accumulator4.py změna rozsahu platnosti objektu s akumulátorem https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/Accumulator4.py
9 test05.robot použití setup a teardown https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test05.robot
10 Accumulator5.py implementace metod setup a teardown https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/Accumulator5.py
11 test06.robot zákaz setup ve zvoleném testu https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test06.robot
12 Accumulator6.py implementace metod setup a teardown https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/Accumulator6.py
13 test07.robot testy řízené tabulkami https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test07.robot
14 test08.robot testy řízené tabulkami https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test08.robot
15 test09.robot jednoduchý BDD test https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test09.robot
16 test10.robot jednoduchý BDD test se spojkou „and“ https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test10.robot
17 test11.robot BDD testy řízené tabulkami https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test11.robot
18 test12.robot vylepšení předchozího příkladu https://github.com/tisnik/robot-framework-examples/blob/master/arti­cle02/test12.robot

20. Odkazy na Internetu

  1. Stránka projektu Robot Framework
    https://robotframework.org/
  2. GitHub repositář Robot Frameworku
    https://github.com/robotfra­mework/robotframework
  3. Robot Framework (Wikipedia)
    https://en.wikipedia.org/wi­ki/Robot_Framework
  4. Tutoriál Robot Frameworku
    http://www.robotframeworktu­torial.com/
  5. Robot Framework Documentation
    https://robotframework.or­g/robotframework/
  6. Robot Framework Introduction
    https://blog.testproject.i­o/2016/11/22/robot-framework-introduction/
  7. robotframework 3.1.2 na PyPi
    https://pypi.org/project/ro­botframework/
  8. Robot Framework demo (GitHub)
    https://github.com/robotfra­mework/RobotDemo
  9. Robot Framework web testing demo using SeleniumLibrary
    https://github.com/robotfra­mework/WebDemo
  10. Robot Framework for Mobile Test Automation Demo
    https://www.youtube.com/wat­ch?v=06LsU08slP8
  11. Gherkin
    https://cucumber.io/docs/gherkin/
  12. Selenium
    https://selenium.dev/
  13. SeleniumLibrary
    https://robotframework.org/
  14. The Practical Test Pyramid
    https://martinfowler.com/ar­ticles/practical-test-pyramid.html
  15. Acceptance Tests and the Testing Pyramid
    http://www.blog.acceptance­testdrivendevelopment.com/ac­ceptance-tests-and-the-testing-pyramid/
  16. Programovací jazyk Clojure – testování s využitím knihovny Expectations
    https://www.root.cz/clanky/pro­gramovaci-jazyk-clojure-testovani-s-vyuzitim-knihovny-expectations/
  17. Programovací jazyk Clojure – některé užitečné triky použitelné (nejenom) v testech
    https://www.root.cz/clanky/pro­gramovaci-jazyk-clojure-nektere-uzitecne-triky-pouzitelne-nejenom-v-testech/
  18. Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure
    https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure/
  19. Tab-separated values
    https://en.wikipedia.org/wiki/Tab-separated_values
  20. A quick guide about Python implementations
    https://blog.rmotr.com/a-quick-guide-about-python-implementations-aa224109f321
  21. Python 2.7 will retire in…
    https://pythonclock.org/

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.

'; document.getElementById('preroll-iframe').onload = function () { setupIframe(); } prerollContainer = document.getElementsByClassName('preroll-container-iframe')[0]; } function setupIframe() { prerollDocument = document.getElementById('preroll-iframe').contentWindow.document; let el = prerollDocument.createElement('style'); prerollDocument.head.appendChild(el); el.innerText = "#adContainer>div:nth-of-type(1),#adContainer>div:nth-of-type(1) > iframe { width: 99% !important;height: 99% !important;max-width: 100%;}#videoContent,body{ width:100vw;height:100vh}body{ font-family:'Helvetica Neue',Arial,sans-serif}#videoContent{ overflow:hidden;background:#000}#adMuteBtn{ width:35px;height:35px;border:0;background:0 0;display:none;position:absolute;fill:rgba(230,230,230,1);bottom:20px;right:25px}"; videoContent = prerollDocument.getElementById('contentElement'); videoContent.style.display = 'none'; videoContent.volume = 1; videoContent.muted = false; const playPromise = videoContent.play(); if (playPromise !== undefined) { playPromise.then(function () { console.log('PREROLL sound allowed'); // setUpIMA(true); videoContent.volume = 1; videoContent.muted = false; setUpIMA(); }).catch(function () { console.log('PREROLL sound forbidden'); videoContent.volume = 0; videoContent.muted = true; setUpIMA(); }); } } function setupDimensions() { prerollWidth = Math.min(iinfoPrerollPosition.offsetWidth, 480); prerollHeight = Math.min(iinfoPrerollPosition.offsetHeight, 320); } function setUpIMA() { google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true); google.ima.settings.setLocale('cs'); google.ima.settings.setNumRedirects(10); // Create the ad display container. createAdDisplayContainer(); // Create ads loader. adsLoader = new google.ima.AdsLoader(adDisplayContainer); // Listen and respond to ads loaded and error events. adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, onAdsManagerLoaded, false); adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, onAdError, false); // An event listener to tell the SDK that our content video // is completed so the SDK can play any post-roll ads. const contentEndedListener = function () { adsLoader.contentComplete(); }; videoContent.onended = contentEndedListener; // Request video ads. const adsRequest = new google.ima.AdsRequest(); adsRequest.adTagUrl = iinfoVastUrls[iinfoVastUrlIndex]; console.log('Preroll advert: ' + iinfoVastUrls[iinfoVastUrlIndex]); videoContent.muted = false; videoContent.volume = 1; // Specify the linear and nonlinear slot sizes. This helps the SDK to // select the correct creative if multiple are returned. // adsRequest.linearAdSlotWidth = prerollWidth; // adsRequest.linearAdSlotHeight = prerollHeight; adsRequest.nonLinearAdSlotWidth = 0; adsRequest.nonLinearAdSlotHeight = 0; adsLoader.requestAds(adsRequest); } function createAdDisplayContainer() { // We assume the adContainer is the DOM id of the element that will house // the ads. prerollDocument.getElementById('videoContent').style.display = 'none'; adDisplayContainer = new google.ima.AdDisplayContainer( prerollDocument.getElementById('adContainer'), videoContent); } function unmutePrerollAdvert() { adVolume = !adVolume; if (adVolume) { adsManager.setVolume(0.3); prerollDocument.getElementById('adMuteBtn').innerHTML = ''; } else { adsManager.setVolume(0); prerollDocument.getElementById('adMuteBtn').innerHTML = ''; } } function onAdsManagerLoaded(adsManagerLoadedEvent) { // Get the ads manager. const adsRenderingSettings = new google.ima.AdsRenderingSettings(); adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true; adsRenderingSettings.loadVideoTimeout = 12000; // videoContent should be set to the content video element. adsManager = adsManagerLoadedEvent.getAdsManager(videoContent, adsRenderingSettings); // Add listeners to the required events. adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, onAdError); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, onContentPauseRequested); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, onContentResumeRequested); adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, onAdEvent); // Listen to any additional events, if necessary. adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, onAdEvent); playAds(); } function playAds() { // Initialize the container. Must be done through a user action on mobile // devices. videoContent.load(); adDisplayContainer.initialize(); // setupDimensions(); try { // Initialize the ads manager. Ad rules playlist will start at this time. adsManager.init(1920, 1080, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will // start at this time; the call will be ignored for ad rules. adsManager.start(); // window.addEventListener('resize', function (event) { // if (adsManager) { // setupDimensions(); // adsManager.resize(prerollWidth, prerollHeight, google.ima.ViewMode.NORMAL); // } // }); } catch (adError) { // An error may be thrown if there was a problem with the VAST response. // videoContent.play(); } } function onAdEvent(adEvent) { const ad = adEvent.getAd(); console.log('Preroll event: ' + adEvent.type); switch (adEvent.type) { case google.ima.AdEvent.Type.LOADED: if (!ad.isLinear()) { videoContent.play(); } prerollDocument.getElementById('adContainer').style.width = '100%'; prerollDocument.getElementById('adContainer').style.maxWidth = '640px'; prerollDocument.getElementById('adContainer').style.height = '360px'; break; case google.ima.AdEvent.Type.STARTED: window.addEventListener('scroll', onActiveView); if (ad.isLinear()) { intervalTimer = setInterval( function () { // Example: const remainingTime = adsManager.getRemainingTime(); // adsManager.pause(); }, 300); // every 300ms } prerollDocument.getElementById('adMuteBtn').style.display = 'block'; break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: if (ad.isLinear()) { clearInterval(intervalTimer); } if (prerollLastError === 303) { playYtVideo(); } break; case google.ima.AdEvent.Type.COMPLETE: if (ad.isLinear()) { clearInterval(intervalTimer); } playYtVideo(); break; } } function onAdError(adErrorEvent) { console.log(adErrorEvent.getError()); prerollLastError = adErrorEvent.getError().getErrorCode(); if (!loadNext()) { playYtVideo(); } } function loadNext() { iinfoVastUrlIndex++; if (iinfoVastUrlIndex < iinfoVastUrls.length) { iinfoPrerollPosition.remove(); playPrerollAd(); } else { return false; } adVolume = 1; return true; } function onContentPauseRequested() { videoContent.pause(); } function onContentResumeRequested() { videoContent.play(); } function onActiveView() { if (prerollContainer) { const containerOffset = prerollContainer.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight/1 && containerOffset.bottom > 0.0) { if (prerollPaused) { adsManager.resume(); prerollPaused = false; } return true; } else { if (!prerollPaused) { adsManager.pause(); prerollPaused = true; } } } return false; } function playYtVideo() { iinfoPrerollPosition.remove(); youtubeIframe.style.display = 'block'; youtubeIframe.src += '&autoplay=1&mute=1'; } }
'; document.getElementById('outstream-iframe').onload = function () { setupIframe(); } replayScreen = document.getElementById('iinfoOutstreamReplay'); iinfoOutstreamPosition = document.getElementById('iinfoOutstreamPosition'); outstreamContainer = document.getElementsByClassName('outstream-container')[0]; setupReplayScreen(); } function setupIframe() { outstreamDocument = document.getElementById('outstream-iframe').contentWindow.document; let el = outstreamDocument.createElement('style'); outstreamDocument.head.appendChild(el); el.innerText = "#adContainer>div:nth-of-type(1),#adContainer>div:nth-of-type(1) > iframe { width: 99% !important;height: 99% !important;max-width: 100%;}#videoContent,body{ width:100vw;height:100vh}body{ font-family:'Helvetica Neue',Arial,sans-serif}#videoContent{ overflow:hidden;background:#000}#adMuteBtn{ width:35px;height:35px;border:0;background:0 0;display:none;position:absolute;fill:rgba(230,230,230,1);bottom:-5px;right:25px}"; videoContent = outstreamDocument.getElementById('contentElement'); videoContent.style.display = 'none'; videoContent.volume = 1; videoContent.muted = false; if ( location.href.indexOf('rejstriky.finance.cz') !== -1 || location.href.indexOf('finance-rejstrik') !== -1 || location.href.indexOf('firmy.euro.cz') !== -1 || location.href.indexOf('euro-rejstrik') !== -1 || location.href.indexOf('/rejstrik/') !== -1 || location.href.indexOf('/rejstrik-firem/') !== -1) { outstreamDirectPlayed = true; soundAllowed = true; iinfoVastUrlIndex = 0; } if (!outstreamDirectPlayed) { console.log('OUTSTREAM direct'); setUpIMA(true); } else { if (soundAllowed) { const playPromise = videoContent.play(); if (playPromise !== undefined) { playPromise.then(function () { console.log('OUTSTREAM sound allowed'); setUpIMA(false); }).catch(function () { console.log('OUTSTREAM sound forbidden'); renderBanner(); }); } } else { renderBanner(); } } } function getWrapper() { let articleWrapper = document.querySelector('.rs-outstream-placeholder'); // Outstream Placeholder from RedSys manipulation if (articleWrapper && articleWrapper.style.display !== 'block') { articleWrapper.innerHTML = ""; articleWrapper.style.display = 'block'; } // Don't render OutStream on homepages if (articleWrapper === null) { if (document.querySelector('body.p-index')) { return null; } } if (articleWrapper === null) { articleWrapper = document.getElementById('iinfo-outstream'); } if (articleWrapper === null) { articleWrapper = document.querySelector('.layout-main__content .detail__article p:nth-of-type(6)'); } if (articleWrapper === null) { // Euro, Autobible, Zdravi articleWrapper = document.querySelector('.o-article .o-article__text p:nth-of-type(6)'); } if (articleWrapper === null) { articleWrapper = document.getElementById('sidebar'); } if (!articleWrapper) { console.error("Outstream wrapper of article was not found."); } return articleWrapper; } function setupDimensions() { outstreamWidth = Math.min(iinfoOutstreamPosition.offsetWidth, 480); outstreamHeight = Math.min(iinfoOutstreamPosition.offsetHeight, 320); } /** * Sets up IMA ad display container, ads loader, and makes an ad request. */ function setUpIMA(direct) { google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true); google.ima.settings.setLocale('cs'); google.ima.settings.setNumRedirects(10); // Create the ad display container. createAdDisplayContainer(); // Create ads loader. adsLoader = new google.ima.AdsLoader(adDisplayContainer); // Listen and respond to ads loaded and error events. adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, onAdsManagerLoaded, false); adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, onAdError, false); // An event listener to tell the SDK that our content video // is completed so the SDK can play any post-roll ads. const contentEndedListener = function () { adsLoader.contentComplete(); }; videoContent.onended = contentEndedListener; // Request video ads. const adsRequest = new google.ima.AdsRequest(); if (direct) { adsRequest.adTagUrl = directVast; console.log('Outstream DIRECT CAMPAING advert: ' + directVast); videoContent.muted = true; videoContent.volume = 0; outstreamDirectPlayed = true; } else { adsRequest.adTagUrl = iinfoVastUrls[iinfoVastUrlIndex]; console.log('Outstream advert: ' + iinfoVastUrls[iinfoVastUrlIndex]); videoContent.muted = false; videoContent.volume = 1; } // Specify the linear and nonlinear slot sizes. This helps the SDK to // select the correct creative if multiple are returned. // adsRequest.linearAdSlotWidth = outstreamWidth; // adsRequest.linearAdSlotHeight = outstreamHeight; adsRequest.nonLinearAdSlotWidth = 0; adsRequest.nonLinearAdSlotHeight = 0; adsLoader.requestAds(adsRequest); } function setupReplayScreen() { replayScreen.addEventListener('click', function () { iinfoOutstreamPosition.remove(); iinfoVastUrlIndex = 0; outstreamInit(); }); } /** * Sets the 'adContainer' div as the IMA ad display container. */ function createAdDisplayContainer() { // We assume the adContainer is the DOM id of the element that will house // the ads. outstreamDocument.getElementById('videoContent').style.display = 'none'; adDisplayContainer = new google.ima.AdDisplayContainer( outstreamDocument.getElementById('adContainer'), videoContent); } function unmuteAdvert() { adVolume = !adVolume; if (adVolume) { adsManager.setVolume(0.3); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } else { adsManager.setVolume(0); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } } /** * Loads the video content and initializes IMA ad playback. */ function playAds() { // Initialize the container. Must be done through a user action on mobile // devices. videoContent.load(); adDisplayContainer.initialize(); // setupDimensions(); try { // Initialize the ads manager. Ad rules playlist will start at this time. adsManager.init(1920, 1080, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will // start at this time; the call will be ignored for ad rules. adsManager.start(); // window.addEventListener('resize', function (event) { // if (adsManager) { // setupDimensions(); // adsManager.resize(outstreamWidth, outstreamHeight, google.ima.ViewMode.NORMAL); // } // }); } catch (adError) { // An error may be thrown if there was a problem with the VAST response. // videoContent.play(); } } /** * Handles the ad manager loading and sets ad event listeners. * @param { !google.ima.AdsManagerLoadedEvent } adsManagerLoadedEvent */ function onAdsManagerLoaded(adsManagerLoadedEvent) { // Get the ads manager. const adsRenderingSettings = new google.ima.AdsRenderingSettings(); adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true; adsRenderingSettings.loadVideoTimeout = 12000; // videoContent should be set to the content video element. adsManager = adsManagerLoadedEvent.getAdsManager(videoContent, adsRenderingSettings); // Add listeners to the required events. adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, onAdError); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, onContentPauseRequested); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, onContentResumeRequested); adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, onAdEvent); // Listen to any additional events, if necessary. adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, onAdEvent); playAds(); } /** * Handles actions taken in response to ad events. * @param { !google.ima.AdEvent } adEvent */ function onAdEvent(adEvent) { // Retrieve the ad from the event. Some events (for example, // ALL_ADS_COMPLETED) don't have ad object associated. const ad = adEvent.getAd(); console.log('Outstream event: ' + adEvent.type); switch (adEvent.type) { case google.ima.AdEvent.Type.LOADED: // This is the first event sent for an ad - it is possible to // determine whether the ad is a video ad or an overlay. if (!ad.isLinear()) { // Position AdDisplayContainer correctly for overlay. // Use ad.width and ad.height. videoContent.play(); } outstreamDocument.getElementById('adContainer').style.width = '100%'; outstreamDocument.getElementById('adContainer').style.maxWidth = '640px'; outstreamDocument.getElementById('adContainer').style.height = '360px'; break; case google.ima.AdEvent.Type.STARTED: window.addEventListener('scroll', onActiveView); // This event indicates the ad has started - the video player // can adjust the UI, for example display a pause button and // remaining time. if (ad.isLinear()) { // For a linear ad, a timer can be started to poll for // the remaining time. intervalTimer = setInterval( function () { // Example: const remainingTime = adsManager.getRemainingTime(); // adsManager.pause(); }, 300); // every 300ms } outstreamDocument.getElementById('adMuteBtn').style.display = 'block'; break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: if (ad.isLinear()) { clearInterval(intervalTimer); } if (outstreamLastError === 303) { if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } } break; case google.ima.AdEvent.Type.COMPLETE: // This event indicates the ad has finished - the video player // can perform appropriate UI actions, such as removing the timer for // remaining time detection. if (ad.isLinear()) { clearInterval(intervalTimer); } if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } break; } } /** * Handles ad errors. * @param { !google.ima.AdErrorEvent } adErrorEvent */ function onAdError(adErrorEvent) { // Handle the error logging. console.log(adErrorEvent.getError()); outstreamLastError = adErrorEvent.getError().getErrorCode(); if (!loadNext()) { renderBanner(); } } function renderBanner() { if (isBanner) { console.log('Outstream: Render Banner'); iinfoOutstreamPosition.innerHTML = ""; iinfoOutstreamPosition.style.height = "330px"; iinfoOutstreamPosition.appendChild(bannerDiv); } else { console.log('Outstream: Banner is not set'); } } function loadNext() { iinfoVastUrlIndex++; if (iinfoVastUrlIndex < iinfoVastUrls.length) { iinfoOutstreamPosition.remove(); outstreamInit(); } else { return false; } adVolume = 1; return true; } /** * Pauses video content and sets up ad UI. */ function onContentPauseRequested() { videoContent.pause(); // This function is where you should setup UI for showing ads (for example, // display ad timer countdown, disable seeking and more.) // setupUIForAds(); } /** * Resumes video content and removes ad UI. */ function onContentResumeRequested() { videoContent.play(); // This function is where you should ensure that your UI is ready // to play content. It is the responsibility of the Publisher to // implement this function when necessary. // setupUIForContent(); } function onActiveView() { if (outstreamContainer) { const containerOffset = outstreamContainer.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight/1 && containerOffset.bottom > 0.0) { if (outstreamPaused) { adsManager.resume(); outstreamPaused = false; } return true; } else { if (!outstreamPaused) { adsManager.pause(); outstreamPaused = true; } } } return false; } let outstreamInitInterval; if (typeof cpexPackage !== "undefined") { outstreamInitInterval = setInterval(tryToInitializeOutstream, 100); } else { const wrapper = getWrapper(); if (wrapper) { let outstreamInitialized = false; window.addEventListener('scroll', () => { if (!outstreamInitialized) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { outstreamInit(); outstreamInitialized = true; } } }); } } function tryToInitializeOutstream() { const wrapper = getWrapper(); if (wrapper) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { if (cpexPackage.adserver.displayed) { clearInterval(outstreamInitInterval); outstreamInit(); } } } else { clearInterval(outstreamInitInterval); } } }
OSZAR »