View
67
Download
5
Category
Preview:
Citation preview
Č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
Recommended