SlideShare a Scribd company logo
1 of 38
Čisté testy, dobré testy
Petr Heinz
Čas na malou rozcvičku
Čas na malou rozcvičku
Kdo z vás píše automatické testy?
Čas na malou rozcvičku
Kdo z vás píše automatické testy?
Komu z vás někdy spadly, aniž byste věděli proč?
Čas na malou rozcvičku
Kdo z vás píše automatické testy?
Komu z vás někdy spadly, aniž byste věděli proč?
Kdo měl pocit, že mu testy hází klacky pod nohy?
Jak testujeme na ShopSys Frameworku
Unit testy - PHPUnit
Integrační / databázové testy
Crawler testy
Akceptační testy - Codeception, Selenium
Performance testy
automatické spouštění na CI serveru (Jenkins)
Co můžu očekávat od dobrého testu?
Testuje jednu funkčnost a spadne, přestane-li fungovat správně.
Je dostatečně robustní, aby nespadl při nesouvisejících úpravách.
I po dvou měsících vím, co, jak a proč testuje.
Když spadne, zjistím v čem je problém.
Je snadné jej spustit a proběhne rychle. Nespouštěný test je k ničemu.
Testuje důležitou funkčnost. Cílem není a priori 100% coverage.
Fáze testu
Arrange - nastavení počátečních podmínek
Act - provedení akce
Assert - ověření očekávaného výsledku
Jednotlivé fáze by měly být z kódu jasně patrné.
Nebojte se extrahovat kus kódu jen pro zvýšení čitelnosti.
Konečně zdrojáky!
Koukněme na akceptační test pro vyhledání
produktu dle katalogového čísla v administraci
class AdminProductSearchCest {
public function testSearchByCatnum(AcceptanceTester $me) {
$me->wantTo('search for product by catnum');
$me->amOnPage('/admin/');
$me->fillFieldByName('admin_login_form[username]', 'admin');
$me->fillFieldByName('admin_login_form[password]', 'admin123');
$me->clickByText('Přihlásit');
$me->amOnPage('/admin/product/list/');
$me->clickByText('Rozšířené hledání');
$me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum');
$me->fillFieldByCss('.js-search-rule-value input', '9176544MG');
$me->clickByText('Hledat');
$me->seeInCss('Aquila Pramenitá voda neperlivá', '.js-grid-column-name');
$foundProductCount = $me->countVisibleByCss('tbody .table-grid__row');
assertEquals(1, $foundProductCount);
}
}
Akceptační test filtrování - původní kód
class LoginPage extends AbstractPage {
const ADMIN_USERNAME = 'admin';
const ADMIN_PASSWORD = 'admin123';
/**
* @param string $username
* @param string $password
*/
public function login($username, $password) {
$this->tester->amOnPage('/admin/');
$this->tester->fillFieldByName('admin_login_form[username]', $username);
$this->tester->fillFieldByName('admin_login_form[password]', $password);
$this->tester->clickByText('Přihlásit');
}
}
Page object přihlášení
class AdminProductSearchCest {
public function testSearchByCatnum(AcceptanceTester $me, LoginPage $loginPage) {
$me->wantTo('search for product by catnum');
$loginPage->login(LoginPage::ADMIN_USERNAME, LoginPage::ADMIN_PASSWORD);
$me->amOnPage('/admin/product/list/');
$me->clickByText('Rozšířené hledání');
$me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum');
$me->fillFieldByCss('.js-search-rule-value input', '9176544MG');
$me->clickByText('Hledat');
$me->seeInCss('Aquila Pramenitá voda neperlivá', '.js-grid-column-name');
$foundProductCount = $me->countVisibleByCss('tbody .table-grid__row');
assertEquals(1, $foundProductCount);
}
}
Akceptační test filtrování - využití LoginPage
class LoginPage extends AbstractPage {
const ADMIN_USERNAME = 'admin';
const ADMIN_PASSWORD = 'admin123';
/**
* @param string $username
* @param string $password
*/
public function login($username, $password) {
$this->tester->amOnPage('/admin/');
$this->tester->fillFieldByName('admin_login_form[username]', $username);
$this->tester->fillFieldByName('admin_login_form[password]', $password);
$this->tester->clickByText('Přihlásit');
}
public function assertLoginFailed() {
$this->tester->see('Přihlášení se nepodařilo.');
$this->tester->seeCurrentPageEquals('/admin/');
}
}
Page object přihlášení - rozšíření o vlastní assert
class AdministratorLoginCest {
public function testSuccessfulLogin(AcceptanceTester $me, LoginPage $loginPage) {
$me->wantTo('login on admin with valid data');
$loginPage->login(LoginPage::ADMIN_USERNAME, LoginPage::ADMIN_PASSWORD);
$me->see('Nástěnka');
}
public function testLoginWithInvalidUsername(AcceptanceTester $me, LoginPage $loginPage)
{
$me->wantTo('login on admin with nonexistent username');
$loginPage->login('nonexistent username', LoginPage::ADMIN_PASSWORD);
$loginPage->assertLoginFailed();
}
public function testLoginWithInvalidPassword(AcceptanceTester $me, LoginPage $loginPage)
{
$me->wantTo('login on admin with invalid password');
$loginPage->login(LoginPage::ADMIN_USERNAME, 'invalid password');
$loginPage->assertLoginFailed();
}
}
Akceptační test přihlašování - znovuvyužití LoginPage
class AdminProductSearchCest {
public function testSearchByCatnum(AcceptanceTester $me, LoginPage $loginPage) {
$me->wantTo('search for product by catnum');
$loginPage->login(LoginPage::ADMIN_USERNAME, LoginPage::ADMIN_PASSWORD);
$me->amOnPage('/admin/product/list/');
$me->clickByText('Rozšířené hledání');
$me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum');
$me->fillFieldByCss('.js-search-rule-value input', '9176544MG');
$me->clickByText('Hledat');
$me->seeInCss('Aquila Pramenitá voda neperlivá', '.js-grid-column-name');
$foundProductCount = $me->countVisibleByCss('tbody .table-grid__row');
assertEquals(1, $foundProductCount);
}
}
Akceptační test filtrování - využití LoginPage
class ProductSearchPage extends AbstractPage {
const SEARCH_SUBJECT_CATNUM = 'productCatnum';
/**
* @param string $searchSubject
* @param string $value
*/
public function search($searchSubject, $value) {
$this->tester->amOnPage('/admin/product/list/');
$this->tester->clickByText('Rozšířené hledání');
$this->tester->selectOptionByCssAndValue('.js-search-rule-subject',
$searchSubject);
$this->tester->fillFieldByCss('.js-search-rule-value input', $value);
$this->tester->clickByText('Hledat');
}
public function assertFoundProductByName($productName) {
$this->tester->seeInCss($productName, '.js-grid-column-name');
}
public function assertFoundProductCount($productCount) {
$foundProductCount = $me->countVisibleByCss('tbody .table-grid__row');
assertEquals($productCount, $foundProductCount);
}
}
Page object filtrování
class AdminProductSearchCest {
public function testSearchByCatnum(
AcceptanceTester $me,
LoginPage $loginPage,
ProductSearchPage $productSearchPage
) {
$me->wantTo('search for product by catnum');
$loginPage->login(LoginPage::ADMIN_USERNAME, LoginPage::ADMIN_PASSWORD);
$productSearchPage->search(ProductSearchPage::SEARCH_SUBJECT_CATNUM, '9176544MG');
$productSearchPage->assertFoundProductByName('Aquila Pramenitá voda neperlivá');
$productSearchPage->assertFoundProductCount(1);
}
}
Akceptační test filtrování - využití ProductSearchPage
Pojmenování testovacích metod
Testovací metody se nemusí nutně jmenovat přesně dle testované metody.
Testovací metody je vhodné pojmenovat dle testovaného scénáře.
Měl by být jasný záměr testu a jeho očekávání.
Pokud je těžké pojmenovat testovací metodu, možná toho testuje příliš mnoho.
Nebojte se dlouhých názvů.
Zpátky do kódu!
Mrkněme na unit test výsledků metody
pro přidávání produktu do košíku
interface CartService {
// …
/**
* @param SS6ShopBundleModelCartCart $cart
* @param SS6ShopBundleModelProductProduct $product
* @param int $quantity
* @return SS6ShopBundleModelCartAddProductResult
* @throws SS6ShopBundleModelCartInvalidQuantityException
*/
public function addProductToCart(Cart $cart, Product $product, $quantity);
// …
}
Rozhraní testované třídy
interface AddProductResult {
/**
* @param SS6ShopBundleModelCartItemCartItem $cartItem
* @param bool $isNew
* @param int $addedQuantity
*/
public function __construct(CartItem $cartItem, $isNew, $addedQuantity);
/**
* @return SS6ShopBundleModelCartItemCartItem
*/
public function getCartItem();
/**
* @return bool
*/
public function getIsNew();
/**
* @return int
*/
public function getAddedQuantity();
}
Rozhraní návratové hodnoty testované metody
class CartServiceTest extends FunctionalTestCase {
// …
public function testAddProductToCartInvalidFloatQuantity() {
$cartService = $this->getCartService();
$product = $this->createProduct();
$cart = $this->createEmptyCart();
$addedQuantity = 1.1;
$this-
>setExpectedException('SS6ShopBundleModelCartInvalidQuantityException');
$cartService->addProductToCart($cart, $product, $addedQuantity);
}
// …
}
Unit test přidání do košíku - původní název metody
class CartServiceTest extends FunctionalTestCase {
// …
public function testCannotAddProductWithFloatQuantityToCart() {
$cartService = $this->getCartService();
$product = $this->createProduct();
$cart = $this->createEmptyCart();
$addedQuantity = 1.1;
$this-
>setExpectedException('SS6ShopBundleModelCartInvalidQuantityException');
$cartService->addProductToCart($cart, $product, $addedQuantity);
}
// …
}
Unit test přidání do košíku - nový název metody
class CartServiceTest extends FunctionalTestCase {
// …
public function testAddProductToCartInvalidZeroQuantity() {
$cartService = $this->getCartService();
$product = $this->createProduct();
$cart = $this->createEmptyCart();
$addedQuantity = 0;
$this-
>setExpectedException('SS6ShopBundleModelCartInvalidQuantityException');
$cartService->addProductToCart($cart, $product, $addedQuantity);
}
// …
}
Unit test přidání do košíku - původní název metody
class CartServiceTest extends FunctionalTestCase {
// …
public function testCannotAddProductWithZeroQuantityToCart() {
$cartService = $this->getCartService();
$product = $this->createProduct();
$cart = $this->createEmptyCart();
$addedQuantity = 0;
$this-
>setExpectedException('SS6ShopBundleModelCartInvalidQuantityException');
$cartService->addProductToCart($cart, $product, $addedQuantity);
}
// …
}
Unit test přidání do košíku - nový název metody
class CartServiceTest extends FunctionalTestCase {
// …
public function testAddProductToCartNewProduct() {
$cartService = $this->getCartService();
$product = $this->createProduct();
$cart = $this->createEmptyCart();
$addedQuantity = 2;
$result = $cartService->addProductToCart($cart, $product, $addedQuantity);
$this->assertTrue($result->getIsNew());
$this->assertSame($addedQuantity, $result->getAddedQuantity());
}
// …
}
Unit test přidání do košíku - původní název metody
class CartServiceTest extends FunctionalTestCase {
// …
public function
testAddProductToCartMarksNewlyAddedProductAsNewAndContainsAddedQuantity() {
$cartService = $this->getCartService();
$product = $this->createProduct();
$cart = $this->createEmptyCart();
$addedQuantity = 2;
$result = $cartService->addProductToCart($cart, $product, $addedQuantity);
$this->assertTrue($result->getIsNew());
$this->assertSame($addedQuantity, $result->getAddedQuantity());
}
// …
}
Unit test přidání do košíku - nový název metody?
class CartServiceTest extends FunctionalTestCase {
// …
public function testAddProductToCartMarksNewlyAddedProductAsNew() {
$cartService = $this->getCartService();
$product = $this->createProduct();
$cart = $this->createEmptyCart();
$addedQuantity = 2;
$result = $cartService->addProductToCart($cart, $product, $addedQuantity);
$this->assertTrue($result->getIsNew());
}
public function testAddProductResultContainsAddedProductQuantity() {
$cartService = $this->getCartService();
$product = $this->createProduct();
$cart = $this->createEmptyCart();
$addedQuantity = 2;
$result = $cartService->addProductToCart($cart, $product, $addedQuantity);
$this->assertSame($addedQuantity, $result->getAddedQuantity());
}
// …
} Unit test přidání do košíku - rozdělení metody
class CartServiceTest extends FunctionalTestCase {
// …
public function testAddProductToCartSameProduct() {
$cartService = $this->getCartService();
$product = $this->createProduct();
$cart = $this->createCartWithOneItem($product);
$addedQuantity = 2;
$result = $cartService->addProductToCart($cart, $product, $addedQuantity);
$this->assertFalse($result->getIsNew());
$this->assertSame($addedQuantity, $result->getAddedQuantity());
}
// …
}
Unit test přidání do košíku - původní název metody
class CartServiceTest extends FunctionalTestCase {
// …
public function testAddProductToCartMarksRepeatedlyAddedProductAsNotNew() {
$cartService = $this->getCartService();
$product = $this->createProduct();
$cart = $this->createCartWithOneItem($product);
$addedQuantity = 2;
$result = $cartService->addProductToCart($cart, $product, $addedQuantity);
$this->assertFalse($result->getIsNew());
}
public function testAddProductResultDoesNotContainPreviouslyAddedProductQuantity() {
$cartService = $this->getCartService();
$product = $this->createProduct();
$cart = $this->createCartWithOneItem($product);
$addedQuantity = 2;
$result = $cartService->addProductToCart($cart, $product, $addedQuantity);
$this->assertSame($addedQuantity, $result->getAddedQuantity());
}
// …
} Unit test přidání do košíku - rozdělení metody
Mockování
Mocky se hodí k simulaci příliš komplexních objektů.
Jejich chování můžeme dobře řídit přímo v kódu testů.
Je možné je použít i k ověřování správné komunikace mezi třídami.
Jejich tvorbu je vhodné extrahovat do privátní metody.
Vzhůru ke zdroji!
Podívejme se na ukázku mockování
v databázovém / integračním testu
interface TransferWebService {
// …
/**
* @param SS6ShopBundleModelTransferTransferRequest $request
* @return resource
*/
public function getResponseStream(TransferRequest $request);
// …
}
Rozhraní mockované třídy
class TransferProductTest extends DatabaseTestCase {
// …
/**
* @param string $fileName
* @return SS6ShopBundleComponentWebService|PHPUnit_Framework_MockObject_MockObject
*/
private function mockWebServiceReturningFileResource($fileName) {
$transferWebServiceMock = $this->getMockBuilder(WebService::class)
->disableOriginalConstructor()
->getMock();
$filePath = __DIR__ . '/Resources/' . $fileName;
$fileResource = fopen($filePath, 'r');
$transferWebServiceMock
->method('getResponseStream')
->willReturn($fileResource);
return $transferWebServiceMock;
}
// …
}
Tvorba mocku v privátní třídě
class TransferProductTest extends DatabaseTestCase {
// …
/**
* @param string $fileName
* @return SS6ShopBundleModelTransferTransferFacade
*/
private function createTransferFacadeMockingWebServiceWithFile($fileName) {
return new TransferFacade(
$this->getContainer()->get(TransferRepository::class),
$this->getWebServiceMockReturningFileResource($fileName),
$this->getContainer()->get(ByteFormatter::class),
$this->getContainer()->get(SqlLoggerFacade::class),
$this->getContainer()->get(RepeatedTransferFacade::class),
$this->getContainer()->get(TransferLoggerFactory::class),
$this->getContainer()->get(EntityManager::class),
$this->getContainer()->get(EntityManagerFacade::class)
);
}
// …
}
Vložení mocku do reálné testované třídy
class TransferProductTest extends DatabaseTestCase {
/**
* @var SS6ShopBundleModelTransferProductProductTransferProcessor
*/
private $productTransferProcessor;
/**
* @var SS6ShopBundleModelProductProductFacade
*/
private $productFacade;
// …
public function testCreateProductCreatesProduct() {
$transferFacade =
$this-
>createTransferFacadeMockingWebServiceWithFile(self::FILE_NAME);
$logger = $this->createLogger();
$transferFacade->process($this->productTransferProcessor, $logger);
$product = $this->productFacade-
>findOneByFloresId(self::PRODUCT_1_FLORES_ID);
$this->assertNotNull($product);
}
// …
}
Samotný integrační / databázový test
Pár rad závěrem
Testy nejsou od toho “aby byly”, jsou tu pro vás.
Začněte testováním nejdůležitějších scénářů.
Pomůžou udržovaná demonstrační data, které budete využívat i v testech.
Nebojte se vytvářet zvláštní třídy pouze pro účely testů.
Některé testy si zaslouží smazat.
Čistota kódu testů je stejně důležitá jako čistota kódu aplikace.
Díky za pozornost
Pusťme se do vašich dotazů!
petr.heinz@shopsys.com

More Related Content

Viewers also liked

Эффективное рекламное сообщение для рынка недвижимости в 2016 году
Эффективное рекламное сообщение для рынка недвижимости в 2016 годуЭффективное рекламное сообщение для рынка недвижимости в 2016 году
Эффективное рекламное сообщение для рынка недвижимости в 2016 годуCoMagic
 
Docencia en forma de investigación
Docencia en forma de investigaciónDocencia en forma de investigación
Docencia en forma de investigaciónCe-tochtli
 
Las almendras naturales: mejor solución vienen cuerpo sano
Las almendras naturales: mejor solución vienen cuerpo sanoLas almendras naturales: mejor solución vienen cuerpo sano
Las almendras naturales: mejor solución vienen cuerpo sanoSaborati
 
Magazine Cover Feedback
Magazine Cover FeedbackMagazine Cover Feedback
Magazine Cover Feedbackelonawoodford
 
Rccn wiring duct vdrf
Rccn wiring duct vdrfRccn wiring duct vdrf
Rccn wiring duct vdrfRCCN RCCN
 
Necola Avery Director of Training Resume
Necola Avery Director of Training ResumeNecola Avery Director of Training Resume
Necola Avery Director of Training ResumeNecola27
 
Trabajo de recursos
Trabajo de recursosTrabajo de recursos
Trabajo de recursosSandrita Tlv
 

Viewers also liked (12)

Эффективное рекламное сообщение для рынка недвижимости в 2016 году
Эффективное рекламное сообщение для рынка недвижимости в 2016 годуЭффективное рекламное сообщение для рынка недвижимости в 2016 году
Эффективное рекламное сообщение для рынка недвижимости в 2016 году
 
Docencia en forma de investigación
Docencia en forma de investigaciónDocencia en forma de investigación
Docencia en forma de investigación
 
Technocratz
TechnocratzTechnocratz
Technocratz
 
Las almendras naturales: mejor solución vienen cuerpo sano
Las almendras naturales: mejor solución vienen cuerpo sanoLas almendras naturales: mejor solución vienen cuerpo sano
Las almendras naturales: mejor solución vienen cuerpo sano
 
ESTUDIANTE
ESTUDIANTEESTUDIANTE
ESTUDIANTE
 
Esteban guevara
Esteban guevaraEsteban guevara
Esteban guevara
 
Magazine Cover Feedback
Magazine Cover FeedbackMagazine Cover Feedback
Magazine Cover Feedback
 
Rccn wiring duct vdrf
Rccn wiring duct vdrfRccn wiring duct vdrf
Rccn wiring duct vdrf
 
Movemens-2015-01
Movemens-2015-01Movemens-2015-01
Movemens-2015-01
 
My resume
My resumeMy resume
My resume
 
Necola Avery Director of Training Resume
Necola Avery Director of Training ResumeNecola Avery Director of Training Resume
Necola Avery Director of Training Resume
 
Trabajo de recursos
Trabajo de recursosTrabajo de recursos
Trabajo de recursos
 

Similar to Petr Heinz - Čisté testy, dobré testy

Vizuální regresní testy
Vizuální regresní testyVizuální regresní testy
Vizuální regresní testyMartin Krištof
 
Open Monday: Jak se připravit na zkoušku Google Analytics IQ
Open Monday: Jak se připravit na zkoušku Google Analytics IQOpen Monday: Jak se připravit na zkoušku Google Analytics IQ
Open Monday: Jak se připravit na zkoušku Google Analytics IQH1.cz
 
Woocommerce úpravy funkčnosti a ovlivňování dat
Woocommerce   úpravy funkčnosti a ovlivňování datWoocommerce   úpravy funkčnosti a ovlivňování dat
Woocommerce úpravy funkčnosti a ovlivňování datVladislav Musílek
 
Google Tag Manager pro vývojáře
Google Tag Manager pro vývojářeGoogle Tag Manager pro vývojáře
Google Tag Manager pro vývojářeMichal Blažek
 
Rozšiřujeme jQuery aneb proč si nenapsat plugin?
Rozšiřujeme jQuery aneb proč si nenapsat plugin?Rozšiřujeme jQuery aneb proč si nenapsat plugin?
Rozšiřujeme jQuery aneb proč si nenapsat plugin?Bohdan Ganický
 
JavaScript v GTM - Measure Camp Brno 2017
JavaScript v GTM - Measure Camp Brno 2017JavaScript v GTM - Measure Camp Brno 2017
JavaScript v GTM - Measure Camp Brno 2017Michal Blažek
 
Miroslav Bajtoš - Nativní async/await v Node.js - už tam jsme?
Miroslav Bajtoš - Nativní async/await v Node.js - už tam jsme?Miroslav Bajtoš - Nativní async/await v Node.js - už tam jsme?
Miroslav Bajtoš - Nativní async/await v Node.js - už tam jsme?Develcz
 
MoroSystems na ostravském CZJUGu o Apache Wicket
MoroSystems na ostravském CZJUGu o Apache WicketMoroSystems na ostravském CZJUGu o Apache Wicket
MoroSystems na ostravském CZJUGu o Apache WicketTomáš Páral
 
INPTP Rekapitulace
INPTP Rekapitulace INPTP Rekapitulace
INPTP Rekapitulace Jan Hřídel
 
Nette framework - How to compile an extensible di container
Nette framework - How to compile an extensible di containerNette framework - How to compile an extensible di container
Nette framework - How to compile an extensible di containerFilip Procházka
 
Aplikační nastavení v .NET
Aplikační nastavení v .NETAplikační nastavení v .NET
Aplikační nastavení v .NETJan Hřídel
 
Trendy a nové možnosti test automation
Trendy a nové možnosti test automationTrendy a nové možnosti test automation
Trendy a nové možnosti test automationOndřej Machulda
 
Dependency injection v Nette 2.1 prakticky
Dependency injection v Nette 2.1 praktickyDependency injection v Nette 2.1 prakticky
Dependency injection v Nette 2.1 praktickyFilip Procházka
 
Technologie užívané při vývoji velkých e-shopů
Technologie užívané při vývoji velkých e-shopůTechnologie užívané při vývoji velkých e-shopů
Technologie užívané při vývoji velkých e-shopůPeckaDesign.cz
 
Lex Vjatkin + Ondřej procházka: Jak to děláme ve Wikidi
Lex Vjatkin + Ondřej procházka: Jak to děláme ve WikidiLex Vjatkin + Ondřej procházka: Jak to děláme ve Wikidi
Lex Vjatkin + Ondřej procházka: Jak to děláme ve WikidiDevelcz
 

Similar to Petr Heinz - Čisté testy, dobré testy (20)

Vizuální regresní testy
Vizuální regresní testyVizuální regresní testy
Vizuální regresní testy
 
Doctrine ORM & model
Doctrine ORM & modelDoctrine ORM & model
Doctrine ORM & model
 
Open Monday: Jak se připravit na zkoušku Google Analytics IQ
Open Monday: Jak se připravit na zkoušku Google Analytics IQOpen Monday: Jak se připravit na zkoušku Google Analytics IQ
Open Monday: Jak se připravit na zkoušku Google Analytics IQ
 
Drupal Front-end
Drupal Front-endDrupal Front-end
Drupal Front-end
 
Woocommerce úpravy funkčnosti a ovlivňování dat
Woocommerce   úpravy funkčnosti a ovlivňování datWoocommerce   úpravy funkčnosti a ovlivňování dat
Woocommerce úpravy funkčnosti a ovlivňování dat
 
Django
DjangoDjango
Django
 
Google Tag Manager pro vývojáře
Google Tag Manager pro vývojářeGoogle Tag Manager pro vývojáře
Google Tag Manager pro vývojáře
 
Rozšiřujeme jQuery aneb proč si nenapsat plugin?
Rozšiřujeme jQuery aneb proč si nenapsat plugin?Rozšiřujeme jQuery aneb proč si nenapsat plugin?
Rozšiřujeme jQuery aneb proč si nenapsat plugin?
 
JavaScript v GTM - Measure Camp Brno 2017
JavaScript v GTM - Measure Camp Brno 2017JavaScript v GTM - Measure Camp Brno 2017
JavaScript v GTM - Measure Camp Brno 2017
 
Miroslav Bajtoš - Nativní async/await v Node.js - už tam jsme?
Miroslav Bajtoš - Nativní async/await v Node.js - už tam jsme?Miroslav Bajtoš - Nativní async/await v Node.js - už tam jsme?
Miroslav Bajtoš - Nativní async/await v Node.js - už tam jsme?
 
Canoo Show Sk
Canoo Show SkCanoo Show Sk
Canoo Show Sk
 
MoroSystems na ostravském CZJUGu o Apache Wicket
MoroSystems na ostravském CZJUGu o Apache WicketMoroSystems na ostravském CZJUGu o Apache Wicket
MoroSystems na ostravském CZJUGu o Apache Wicket
 
INPTP Rekapitulace
INPTP Rekapitulace INPTP Rekapitulace
INPTP Rekapitulace
 
Clean code
Clean codeClean code
Clean code
 
Nette framework - How to compile an extensible di container
Nette framework - How to compile an extensible di containerNette framework - How to compile an extensible di container
Nette framework - How to compile an extensible di container
 
Aplikační nastavení v .NET
Aplikační nastavení v .NETAplikační nastavení v .NET
Aplikační nastavení v .NET
 
Trendy a nové možnosti test automation
Trendy a nové možnosti test automationTrendy a nové možnosti test automation
Trendy a nové možnosti test automation
 
Dependency injection v Nette 2.1 prakticky
Dependency injection v Nette 2.1 praktickyDependency injection v Nette 2.1 prakticky
Dependency injection v Nette 2.1 prakticky
 
Technologie užívané při vývoji velkých e-shopů
Technologie užívané při vývoji velkých e-shopůTechnologie užívané při vývoji velkých e-shopů
Technologie užívané při vývoji velkých e-shopů
 
Lex Vjatkin + Ondřej procházka: Jak to děláme ve Wikidi
Lex Vjatkin + Ondřej procházka: Jak to děláme ve WikidiLex Vjatkin + Ondřej procházka: Jak to děláme ve Wikidi
Lex Vjatkin + Ondřej procházka: Jak to děláme ve Wikidi
 

Petr Heinz - Čisté testy, dobré testy

  • 1. Čisté testy, dobré testy Petr Heinz
  • 2. Čas na malou rozcvičku
  • 3. Čas na malou rozcvičku Kdo z vás píše automatické testy?
  • 4. Čas na malou rozcvičku Kdo z vás píše automatické testy? Komu z vás někdy spadly, aniž byste věděli proč?
  • 5. Čas na malou rozcvičku Kdo z vás píše automatické testy? Komu z vás někdy spadly, aniž byste věděli proč? Kdo měl pocit, že mu testy hází klacky pod nohy?
  • 6. Jak testujeme na ShopSys Frameworku Unit testy - PHPUnit Integrační / databázové testy Crawler testy Akceptační testy - Codeception, Selenium Performance testy automatické spouštění na CI serveru (Jenkins)
  • 7. Co můžu očekávat od dobrého testu? Testuje jednu funkčnost a spadne, přestane-li fungovat správně. Je dostatečně robustní, aby nespadl při nesouvisejících úpravách. I po dvou měsících vím, co, jak a proč testuje. Když spadne, zjistím v čem je problém. Je snadné jej spustit a proběhne rychle. Nespouštěný test je k ničemu. Testuje důležitou funkčnost. Cílem není a priori 100% coverage.
  • 8. Fáze testu Arrange - nastavení počátečních podmínek Act - provedení akce Assert - ověření očekávaného výsledku Jednotlivé fáze by měly být z kódu jasně patrné. Nebojte se extrahovat kus kódu jen pro zvýšení čitelnosti.
  • 9. Konečně zdrojáky! Koukněme na akceptační test pro vyhledání produktu dle katalogového čísla v administraci
  • 10. class AdminProductSearchCest { public function testSearchByCatnum(AcceptanceTester $me) { $me->wantTo('search for product by catnum'); $me->amOnPage('/admin/'); $me->fillFieldByName('admin_login_form[username]', 'admin'); $me->fillFieldByName('admin_login_form[password]', 'admin123'); $me->clickByText('Přihlásit'); $me->amOnPage('/admin/product/list/'); $me->clickByText('Rozšířené hledání'); $me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum'); $me->fillFieldByCss('.js-search-rule-value input', '9176544MG'); $me->clickByText('Hledat'); $me->seeInCss('Aquila Pramenitá voda neperlivá', '.js-grid-column-name'); $foundProductCount = $me->countVisibleByCss('tbody .table-grid__row'); assertEquals(1, $foundProductCount); } } Akceptační test filtrování - původní kód
  • 11. class LoginPage extends AbstractPage { const ADMIN_USERNAME = 'admin'; const ADMIN_PASSWORD = 'admin123'; /** * @param string $username * @param string $password */ public function login($username, $password) { $this->tester->amOnPage('/admin/'); $this->tester->fillFieldByName('admin_login_form[username]', $username); $this->tester->fillFieldByName('admin_login_form[password]', $password); $this->tester->clickByText('Přihlásit'); } } Page object přihlášení
  • 12. class AdminProductSearchCest { public function testSearchByCatnum(AcceptanceTester $me, LoginPage $loginPage) { $me->wantTo('search for product by catnum'); $loginPage->login(LoginPage::ADMIN_USERNAME, LoginPage::ADMIN_PASSWORD); $me->amOnPage('/admin/product/list/'); $me->clickByText('Rozšířené hledání'); $me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum'); $me->fillFieldByCss('.js-search-rule-value input', '9176544MG'); $me->clickByText('Hledat'); $me->seeInCss('Aquila Pramenitá voda neperlivá', '.js-grid-column-name'); $foundProductCount = $me->countVisibleByCss('tbody .table-grid__row'); assertEquals(1, $foundProductCount); } } Akceptační test filtrování - využití LoginPage
  • 13. class LoginPage extends AbstractPage { const ADMIN_USERNAME = 'admin'; const ADMIN_PASSWORD = 'admin123'; /** * @param string $username * @param string $password */ public function login($username, $password) { $this->tester->amOnPage('/admin/'); $this->tester->fillFieldByName('admin_login_form[username]', $username); $this->tester->fillFieldByName('admin_login_form[password]', $password); $this->tester->clickByText('Přihlásit'); } public function assertLoginFailed() { $this->tester->see('Přihlášení se nepodařilo.'); $this->tester->seeCurrentPageEquals('/admin/'); } } Page object přihlášení - rozšíření o vlastní assert
  • 14. class AdministratorLoginCest { public function testSuccessfulLogin(AcceptanceTester $me, LoginPage $loginPage) { $me->wantTo('login on admin with valid data'); $loginPage->login(LoginPage::ADMIN_USERNAME, LoginPage::ADMIN_PASSWORD); $me->see('Nástěnka'); } public function testLoginWithInvalidUsername(AcceptanceTester $me, LoginPage $loginPage) { $me->wantTo('login on admin with nonexistent username'); $loginPage->login('nonexistent username', LoginPage::ADMIN_PASSWORD); $loginPage->assertLoginFailed(); } public function testLoginWithInvalidPassword(AcceptanceTester $me, LoginPage $loginPage) { $me->wantTo('login on admin with invalid password'); $loginPage->login(LoginPage::ADMIN_USERNAME, 'invalid password'); $loginPage->assertLoginFailed(); } } Akceptační test přihlašování - znovuvyužití LoginPage
  • 15. class AdminProductSearchCest { public function testSearchByCatnum(AcceptanceTester $me, LoginPage $loginPage) { $me->wantTo('search for product by catnum'); $loginPage->login(LoginPage::ADMIN_USERNAME, LoginPage::ADMIN_PASSWORD); $me->amOnPage('/admin/product/list/'); $me->clickByText('Rozšířené hledání'); $me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum'); $me->fillFieldByCss('.js-search-rule-value input', '9176544MG'); $me->clickByText('Hledat'); $me->seeInCss('Aquila Pramenitá voda neperlivá', '.js-grid-column-name'); $foundProductCount = $me->countVisibleByCss('tbody .table-grid__row'); assertEquals(1, $foundProductCount); } } Akceptační test filtrování - využití LoginPage
  • 16. class ProductSearchPage extends AbstractPage { const SEARCH_SUBJECT_CATNUM = 'productCatnum'; /** * @param string $searchSubject * @param string $value */ public function search($searchSubject, $value) { $this->tester->amOnPage('/admin/product/list/'); $this->tester->clickByText('Rozšířené hledání'); $this->tester->selectOptionByCssAndValue('.js-search-rule-subject', $searchSubject); $this->tester->fillFieldByCss('.js-search-rule-value input', $value); $this->tester->clickByText('Hledat'); } public function assertFoundProductByName($productName) { $this->tester->seeInCss($productName, '.js-grid-column-name'); } public function assertFoundProductCount($productCount) { $foundProductCount = $me->countVisibleByCss('tbody .table-grid__row'); assertEquals($productCount, $foundProductCount); } } Page object filtrování
  • 17. class AdminProductSearchCest { public function testSearchByCatnum( AcceptanceTester $me, LoginPage $loginPage, ProductSearchPage $productSearchPage ) { $me->wantTo('search for product by catnum'); $loginPage->login(LoginPage::ADMIN_USERNAME, LoginPage::ADMIN_PASSWORD); $productSearchPage->search(ProductSearchPage::SEARCH_SUBJECT_CATNUM, '9176544MG'); $productSearchPage->assertFoundProductByName('Aquila Pramenitá voda neperlivá'); $productSearchPage->assertFoundProductCount(1); } } Akceptační test filtrování - využití ProductSearchPage
  • 18. Pojmenování testovacích metod Testovací metody se nemusí nutně jmenovat přesně dle testované metody. Testovací metody je vhodné pojmenovat dle testovaného scénáře. Měl by být jasný záměr testu a jeho očekávání. Pokud je těžké pojmenovat testovací metodu, možná toho testuje příliš mnoho. Nebojte se dlouhých názvů.
  • 19. Zpátky do kódu! Mrkněme na unit test výsledků metody pro přidávání produktu do košíku
  • 20. interface CartService { // … /** * @param SS6ShopBundleModelCartCart $cart * @param SS6ShopBundleModelProductProduct $product * @param int $quantity * @return SS6ShopBundleModelCartAddProductResult * @throws SS6ShopBundleModelCartInvalidQuantityException */ public function addProductToCart(Cart $cart, Product $product, $quantity); // … } Rozhraní testované třídy
  • 21. interface AddProductResult { /** * @param SS6ShopBundleModelCartItemCartItem $cartItem * @param bool $isNew * @param int $addedQuantity */ public function __construct(CartItem $cartItem, $isNew, $addedQuantity); /** * @return SS6ShopBundleModelCartItemCartItem */ public function getCartItem(); /** * @return bool */ public function getIsNew(); /** * @return int */ public function getAddedQuantity(); } Rozhraní návratové hodnoty testované metody
  • 22. class CartServiceTest extends FunctionalTestCase { // … public function testAddProductToCartInvalidFloatQuantity() { $cartService = $this->getCartService(); $product = $this->createProduct(); $cart = $this->createEmptyCart(); $addedQuantity = 1.1; $this- >setExpectedException('SS6ShopBundleModelCartInvalidQuantityException'); $cartService->addProductToCart($cart, $product, $addedQuantity); } // … } Unit test přidání do košíku - původní název metody
  • 23. class CartServiceTest extends FunctionalTestCase { // … public function testCannotAddProductWithFloatQuantityToCart() { $cartService = $this->getCartService(); $product = $this->createProduct(); $cart = $this->createEmptyCart(); $addedQuantity = 1.1; $this- >setExpectedException('SS6ShopBundleModelCartInvalidQuantityException'); $cartService->addProductToCart($cart, $product, $addedQuantity); } // … } Unit test přidání do košíku - nový název metody
  • 24. class CartServiceTest extends FunctionalTestCase { // … public function testAddProductToCartInvalidZeroQuantity() { $cartService = $this->getCartService(); $product = $this->createProduct(); $cart = $this->createEmptyCart(); $addedQuantity = 0; $this- >setExpectedException('SS6ShopBundleModelCartInvalidQuantityException'); $cartService->addProductToCart($cart, $product, $addedQuantity); } // … } Unit test přidání do košíku - původní název metody
  • 25. class CartServiceTest extends FunctionalTestCase { // … public function testCannotAddProductWithZeroQuantityToCart() { $cartService = $this->getCartService(); $product = $this->createProduct(); $cart = $this->createEmptyCart(); $addedQuantity = 0; $this- >setExpectedException('SS6ShopBundleModelCartInvalidQuantityException'); $cartService->addProductToCart($cart, $product, $addedQuantity); } // … } Unit test přidání do košíku - nový název metody
  • 26. class CartServiceTest extends FunctionalTestCase { // … public function testAddProductToCartNewProduct() { $cartService = $this->getCartService(); $product = $this->createProduct(); $cart = $this->createEmptyCart(); $addedQuantity = 2; $result = $cartService->addProductToCart($cart, $product, $addedQuantity); $this->assertTrue($result->getIsNew()); $this->assertSame($addedQuantity, $result->getAddedQuantity()); } // … } Unit test přidání do košíku - původní název metody
  • 27. class CartServiceTest extends FunctionalTestCase { // … public function testAddProductToCartMarksNewlyAddedProductAsNewAndContainsAddedQuantity() { $cartService = $this->getCartService(); $product = $this->createProduct(); $cart = $this->createEmptyCart(); $addedQuantity = 2; $result = $cartService->addProductToCart($cart, $product, $addedQuantity); $this->assertTrue($result->getIsNew()); $this->assertSame($addedQuantity, $result->getAddedQuantity()); } // … } Unit test přidání do košíku - nový název metody?
  • 28. class CartServiceTest extends FunctionalTestCase { // … public function testAddProductToCartMarksNewlyAddedProductAsNew() { $cartService = $this->getCartService(); $product = $this->createProduct(); $cart = $this->createEmptyCart(); $addedQuantity = 2; $result = $cartService->addProductToCart($cart, $product, $addedQuantity); $this->assertTrue($result->getIsNew()); } public function testAddProductResultContainsAddedProductQuantity() { $cartService = $this->getCartService(); $product = $this->createProduct(); $cart = $this->createEmptyCart(); $addedQuantity = 2; $result = $cartService->addProductToCart($cart, $product, $addedQuantity); $this->assertSame($addedQuantity, $result->getAddedQuantity()); } // … } Unit test přidání do košíku - rozdělení metody
  • 29. class CartServiceTest extends FunctionalTestCase { // … public function testAddProductToCartSameProduct() { $cartService = $this->getCartService(); $product = $this->createProduct(); $cart = $this->createCartWithOneItem($product); $addedQuantity = 2; $result = $cartService->addProductToCart($cart, $product, $addedQuantity); $this->assertFalse($result->getIsNew()); $this->assertSame($addedQuantity, $result->getAddedQuantity()); } // … } Unit test přidání do košíku - původní název metody
  • 30. class CartServiceTest extends FunctionalTestCase { // … public function testAddProductToCartMarksRepeatedlyAddedProductAsNotNew() { $cartService = $this->getCartService(); $product = $this->createProduct(); $cart = $this->createCartWithOneItem($product); $addedQuantity = 2; $result = $cartService->addProductToCart($cart, $product, $addedQuantity); $this->assertFalse($result->getIsNew()); } public function testAddProductResultDoesNotContainPreviouslyAddedProductQuantity() { $cartService = $this->getCartService(); $product = $this->createProduct(); $cart = $this->createCartWithOneItem($product); $addedQuantity = 2; $result = $cartService->addProductToCart($cart, $product, $addedQuantity); $this->assertSame($addedQuantity, $result->getAddedQuantity()); } // … } Unit test přidání do košíku - rozdělení metody
  • 31. Mockování Mocky se hodí k simulaci příliš komplexních objektů. Jejich chování můžeme dobře řídit přímo v kódu testů. Je možné je použít i k ověřování správné komunikace mezi třídami. Jejich tvorbu je vhodné extrahovat do privátní metody.
  • 32. Vzhůru ke zdroji! Podívejme se na ukázku mockování v databázovém / integračním testu
  • 33. interface TransferWebService { // … /** * @param SS6ShopBundleModelTransferTransferRequest $request * @return resource */ public function getResponseStream(TransferRequest $request); // … } Rozhraní mockované třídy
  • 34. class TransferProductTest extends DatabaseTestCase { // … /** * @param string $fileName * @return SS6ShopBundleComponentWebService|PHPUnit_Framework_MockObject_MockObject */ private function mockWebServiceReturningFileResource($fileName) { $transferWebServiceMock = $this->getMockBuilder(WebService::class) ->disableOriginalConstructor() ->getMock(); $filePath = __DIR__ . '/Resources/' . $fileName; $fileResource = fopen($filePath, 'r'); $transferWebServiceMock ->method('getResponseStream') ->willReturn($fileResource); return $transferWebServiceMock; } // … } Tvorba mocku v privátní třídě
  • 35. class TransferProductTest extends DatabaseTestCase { // … /** * @param string $fileName * @return SS6ShopBundleModelTransferTransferFacade */ private function createTransferFacadeMockingWebServiceWithFile($fileName) { return new TransferFacade( $this->getContainer()->get(TransferRepository::class), $this->getWebServiceMockReturningFileResource($fileName), $this->getContainer()->get(ByteFormatter::class), $this->getContainer()->get(SqlLoggerFacade::class), $this->getContainer()->get(RepeatedTransferFacade::class), $this->getContainer()->get(TransferLoggerFactory::class), $this->getContainer()->get(EntityManager::class), $this->getContainer()->get(EntityManagerFacade::class) ); } // … } Vložení mocku do reálné testované třídy
  • 36. class TransferProductTest extends DatabaseTestCase { /** * @var SS6ShopBundleModelTransferProductProductTransferProcessor */ private $productTransferProcessor; /** * @var SS6ShopBundleModelProductProductFacade */ private $productFacade; // … public function testCreateProductCreatesProduct() { $transferFacade = $this- >createTransferFacadeMockingWebServiceWithFile(self::FILE_NAME); $logger = $this->createLogger(); $transferFacade->process($this->productTransferProcessor, $logger); $product = $this->productFacade- >findOneByFloresId(self::PRODUCT_1_FLORES_ID); $this->assertNotNull($product); } // … } Samotný integrační / databázový test
  • 37. Pár rad závěrem Testy nejsou od toho “aby byly”, jsou tu pro vás. Začněte testováním nejdůležitějších scénářů. Pomůžou udržovaná demonstrační data, které budete využívat i v testech. Nebojte se vytvářet zvláštní třídy pouze pro účely testů. Některé testy si zaslouží smazat. Čistota kódu testů je stejně důležitá jako čistota kódu aplikace.
  • 38. Díky za pozornost Pusťme se do vašich dotazů! petr.heinz@shopsys.com

Editor's Notes

  1. Představení sebe jako