ݺߣ

ݺߣShare a Scribd company logo
Čisté testy, dobré testy
Petr Heinz
Čas na malou dzč
Čas na malou dzč
Kdo z vás píše automatické testy?
Čas na malou dzč
Kdo z vás píše automatické testy?
Komu z vás někdy spadly, aniž byste věděli proč?
Čas na malou dzč
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

Petr Heinz - Čisté testy, dobré testy

  • 1. Čisté testy, dobré testy Petr Heinz
  • 3. Čas na malou dzč Kdo z vás píše automatické testy?
  • 4. Čas na malou dzč 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 dzč 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

  • #2: Představení sebe jako