petr heinz - Čisté testy, dobré testy

Post on 22-Jan-2018

66 Views

Category:

Technology

5 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Čisté testy, dobré testyPetr 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 \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

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

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

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

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

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

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 \SS6\ShopBundle\Model\Transfer\TransferRequest $request

* @return resource

*/

public function getResponseStream(TransferRequest $request);

// …

}

Rozhraní mockované třídy

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ě

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

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

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 pozornostPusťme se do vašich dotazů!

petr.heinz@shopsys.com

top related