petr heinz - Čisté testy, dobré testy

38
Čisté testy, dobré testy Petr Heinz

Upload: anna-kovarova

Post on 22-Jan-2018

66 views

Category:

Technology


5 download

TRANSCRIPT

Page 1: Petr Heinz - Čisté testy, dobré testy

Čisté testy, dobré testyPetr Heinz

Page 2: Petr Heinz - Čisté testy, dobré testy

Čas na malou rozcvičku

Page 3: Petr Heinz - Čisté testy, dobré testy

Čas na malou rozcvičku

Kdo z vás píše automatické testy?

Page 4: Petr Heinz - Čisté testy, dobré 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č?

Page 5: Petr Heinz - Čisté testy, dobré 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č?

Kdo měl pocit, že mu testy hází klacky pod nohy?

Page 6: Petr Heinz - Čisté testy, dobré testy

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)

Page 7: Petr Heinz - Čisté testy, dobré testy

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.

Page 8: Petr Heinz - Čisté testy, dobré testy

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.

Page 9: Petr Heinz - Čisté testy, dobré testy

Konečně zdrojáky!

Koukněme na akceptační test pro vyhledání

produktu dle katalogového čísla v administraci

Page 10: Petr Heinz - Čisté testy, dobré testy

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

Page 11: Petr Heinz - Čisté testy, dobré testy

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í

Page 12: Petr Heinz - Čisté testy, dobré testy

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

Page 13: Petr Heinz - Čisté testy, dobré testy

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

Page 14: Petr Heinz - Čisté testy, dobré testy

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

Page 15: Petr Heinz - Čisté testy, dobré testy

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

Page 16: Petr Heinz - Čisté testy, dobré testy

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í

Page 17: Petr Heinz - Čisté testy, dobré testy

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

Page 18: Petr Heinz - Čisté testy, dobré testy

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ů.

Page 19: Petr Heinz - Čisté testy, dobré testy

Zpátky do kódu!

Mrkněme na unit test výsledků metody

pro přidávání produktu do košíku

Page 20: Petr Heinz - Čisté testy, dobré testy

interface CartService {

// …

/**

* @param \SS6\ShopBundle\Model\Cart\Cart $cart

* @param \SS6\ShopBundle\Model\Product\Product $product

* @param int $quantity

* @return \SS6\ShopBundle\Model\Cart\AddProductResult

* @throws \SS6\ShopBundle\Model\Cart\InvalidQuantityException

*/

public function addProductToCart(Cart $cart, Product $product, $quantity);

// …

}

Rozhraní testované třídy

Page 21: Petr Heinz - Čisté testy, dobré testy

interface AddProductResult {

/**

* @param \SS6\ShopBundle\Model\Cart\Item\CartItem $cartItem

* @param bool $isNew

* @param int $addedQuantity

*/

public function __construct(CartItem $cartItem, $isNew, $addedQuantity);

/**

* @return \SS6\ShopBundle\Model\Cart\Item\CartItem

*/

public function getCartItem();

/**

* @return bool

*/

public function getIsNew();

/**

* @return int

*/

public function getAddedQuantity();

}

Rozhraní návratové hodnoty testované metody

Page 22: Petr Heinz - Čisté testy, dobré testy

class CartServiceTest extends FunctionalTestCase {

// …

public function testAddProductToCartInvalidFloatQuantity() {

$cartService = $this->getCartService();

$product = $this->createProduct();

$cart = $this->createEmptyCart();

$addedQuantity = 1.1;

$this-

>setExpectedException('SS6\ShopBundle\Model\Cart\InvalidQuantityException');

$cartService->addProductToCart($cart, $product, $addedQuantity);

}

// …

}

Unit test přidání do košíku - původní název metody

Page 23: Petr Heinz - Čisté testy, dobré testy

class CartServiceTest extends FunctionalTestCase {

// …

public function testCannotAddProductWithFloatQuantityToCart() {

$cartService = $this->getCartService();

$product = $this->createProduct();

$cart = $this->createEmptyCart();

$addedQuantity = 1.1;

$this-

>setExpectedException('SS6\ShopBundle\Model\Cart\InvalidQuantityException');

$cartService->addProductToCart($cart, $product, $addedQuantity);

}

// …

}

Unit test přidání do košíku - nový název metody

Page 24: Petr Heinz - Čisté testy, dobré testy

class CartServiceTest extends FunctionalTestCase {

// …

public function testAddProductToCartInvalidZeroQuantity() {

$cartService = $this->getCartService();

$product = $this->createProduct();

$cart = $this->createEmptyCart();

$addedQuantity = 0;

$this-

>setExpectedException('SS6\ShopBundle\Model\Cart\InvalidQuantityException');

$cartService->addProductToCart($cart, $product, $addedQuantity);

}

// …

}

Unit test přidání do košíku - původní název metody

Page 25: Petr Heinz - Čisté testy, dobré testy

class CartServiceTest extends FunctionalTestCase {

// …

public function testCannotAddProductWithZeroQuantityToCart() {

$cartService = $this->getCartService();

$product = $this->createProduct();

$cart = $this->createEmptyCart();

$addedQuantity = 0;

$this-

>setExpectedException('SS6\ShopBundle\Model\Cart\InvalidQuantityException');

$cartService->addProductToCart($cart, $product, $addedQuantity);

}

// …

}

Unit test přidání do košíku - nový název metody

Page 26: Petr Heinz - Čisté testy, dobré testy

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

Page 27: Petr Heinz - Čisté testy, dobré testy

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?

Page 28: Petr Heinz - Čisté testy, dobré testy

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

Page 29: Petr Heinz - Čisté testy, dobré testy

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

Page 30: Petr Heinz - Čisté testy, dobré testy

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

Page 31: Petr Heinz - Čisté testy, dobré testy

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.

Page 32: Petr Heinz - Čisté testy, dobré testy

Vzhůru ke zdroji!

Podívejme se na ukázku mockování

v databázovém / integračním testu

Page 33: Petr Heinz - Čisté testy, dobré testy

interface TransferWebService {

// …

/**

* @param \SS6\ShopBundle\Model\Transfer\TransferRequest $request

* @return resource

*/

public function getResponseStream(TransferRequest $request);

// …

}

Rozhraní mockované třídy

Page 34: Petr Heinz - Čisté testy, dobré testy

class TransferProductTest extends DatabaseTestCase {

// …

/**

* @param string $fileName

* @return \SS6\ShopBundle\Component\WebService|\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ě

Page 35: Petr Heinz - Čisté testy, dobré testy

class TransferProductTest extends DatabaseTestCase {

// …

/**

* @param string $fileName

* @return \SS6\ShopBundle\Model\Transfer\TransferFacade

*/

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

Page 36: Petr Heinz - Čisté testy, dobré testy

class TransferProductTest extends DatabaseTestCase {

/**

* @var \SS6\ShopBundle\Model\Transfer\Product\ProductTransferProcessor

*/

private $productTransferProcessor;

/**

* @var \SS6\ShopBundle\Model\Product\ProductFacade

*/

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

Page 37: Petr Heinz - Čisté testy, dobré testy

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.

Page 38: Petr Heinz - Čisté testy, dobré testy

Díky za pozornostPusťme se do vašich dotazů!

[email protected]