SlideShare a Scribd company logo
Clean tests - good tests
Petr Heinz
Time for an exercise
Time for an exercise
Who have ever written an automated test?
Time for an exercise
Who have ever written an automated test?
Who already dealt with tests failing for no apparent reason?
Time for an exercise
Who have ever written an automated test?
Who already dealt with tests failing for no apparent reason?
Who had ever feeling like the tests are just throwing
obstacles in your way?
How is testing done with ShopSys Framework
Unit tests - PHPUnit
Integration / database tests
Crawler tests
Acceptance tests - Codeception, Selenium
Performance tests
automated execution on CI server (Jenkins)
What can I expect from a good test?
It is testing one functionality and it fails when it doesn’t work properly.
It is robust enough not to fail when changing unrelated code.
Even after two months I know what, how and why it is testing.
When it fails, I know where the problem is.
It is easy to execute the test and it runs fast. Having unexecuted test is useless.
It is testing an important functionality. The aim isn’t 100% coverage.
Test phases
Arrange - initial requirements setting
Act - the execution of the test
Assert - expected result control
Each phase should be easily told apart in the code.
Don’t be afraid to extract bit of the code just to make it more readable.
Finally, source codes!
Let’s have a look at an acceptance test for searching
product in the administration using its catalogue
number
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('Advanced search');
$me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum');
$me->fillFieldByCss('.js-search-rule-value input', '9176544MG');
$me->clickByText('Search');
$me->seeInCss('Aquila Still Spring Water', '.js-grid-column-name');
$foundProductCount = $me->countVisibleByCss('tbody .table-grid__row');
assertEquals(1, $foundProductCount);
}
}
Acceptance test of filtering - the original code
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('Log in');
}
}
LoginPage object
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('Advanced search');
$me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum');
$me->fillFieldByCss('.js-search-rule-value input', '9176544MG');
$me->clickByText('Search');
$me->seeInCss('Aquila Still Spring Water', '.js-grid-column-name');
$foundProductCount = $me->countVisibleByCss('tbody .table-grid__row');
assertEquals(1, $foundProductCount);
}
}
Acceptance test of filtering - using the 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('Log in');
}
public function assertLoginFailed() {
$this->tester->see('Login failed.');
$this->tester->seeCurrentPageEquals('/admin/');
}
}
LoginPage object - assert extension
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('Dashboard');
}
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();
}
}
Acceptance test of filtering - reusing the 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('Advanced search');
$me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum');
$me->fillFieldByCss('.js-search-rule-value input', '9176544MG');
$me->clickByText('Search');
$me->seeInCss('Aquila Still Spring Water', '.js-grid-column-name');
$foundProductCount = $me->countVisibleByCss('tbody .table-grid__row');
assertEquals(1, $foundProductCount);
}
}
Acceptance test of filtering - using 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('Advanced search');
$this->tester->selectOptionByCssAndValue('.js-search-rule-subject',
$searchSubject);
$this->tester->fillFieldByCss('.js-search-rule-value input', $value);
$this->tester->clickByText('Search');
}
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);
}
}
ProductSearchPage object
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);
}
}
Acceptance test of filtering - using the ProductSearchPage
Naming of the testing methods
Testing methods don’t have to be named exactly after the tested method.
It is suitable to name the methods after the tested scenario.
The intention and the expectations of the test should be clear.
If it’s not easy to name the testing method it might be the case you are testing too
many things at once.
Don’t be afraid of long names.
Back to code!
Let’s have a look at a unit test of method for adding
product to the cart
interface CartService {
// …
/**
* @param SS6ShopBundleModelCartCart $cart
* @param SS6ShopBundleModelProductProduct $product
* @param int $quantity
* @return SS6ShopBundleModelCartAddProductResult
* @throws SS6ShopBundleModelCartInvalidQuantityException
*/
public function addProductToCart(Cart $cart, Product $product, $quantity);
// …
}
Test class interface
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();
}
Interface of the return value of the tested method
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);
}
// …
}
Adding to the cart unit test - original method name
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);
}
// …
}
Adding to the cart unit test - new method name
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);
}
// …
}
Adding to the cart unit test - original method name
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);
}
// …
}
Adding to the cart unit test - new method name
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());
}
// …
}
Adding to the cart unit test - original method name
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());
}
// …
}
Adding to the cart unit test - new method name?
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());
}
// …
} Adding to the cart unit test - separating the method
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());
}
// …
}
Adding to the cart unit test - original method name
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());
}
// …
} Adding to the cart unit test - separating the method
Mocking
Mocks are good when simulating too complex objects.
Its behavior can be controlled well directly in the test code.
It is possible to use it when verifying correct communication between classes.
It is good to extract its creation to a private method.
To the code!
Let’s have a look at a demonstration of a mocking in
database/integration test
interface WebService {
// …
/**
* @param SS6ShopBundleComponentWebServiceRequest $request
* @return resource
*/
public function getResponseStream(Request $request);
// …
}
Mocked class interface
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;
}
// …
}
Creating the mock in a private class
class TransferProductTest extends DatabaseTestCase {
// …
/**
* @param string $fileName
* @return SS6ShopBundleModelTransferTransferFacade
*/
private function createTransferFacadeMockingWebServiceWithFile($fileName) {
return new TransferFacade(
$this->getContainer()->get(TransferRepository::class),
$this->mockWebServicReturningFileResource($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)
);
}
// …
}
Injecting the mock into the real tested class
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);
}
// …
}
Intergraton/database test
Some advice in conclusion
Tests are not here in order “to exist”, they are here for you.
Start with testing the most important scenarios.
Well-kept demonstration data which you are going to use in the tests will help.
Don’t be afraid to create special classes only for the test.
Some tests are worth deleting.
Having clean code in tests is equally important as having it in application code.
Thank you for your attention
Let’s get down to your questions!
petr.heinz@shopsys.com

More Related Content

PPTX
Testing the Untestable
Mark Baker
 
PDF
Developer Tests - Things to Know (Vilnius JUG)
vilniusjug
 
PPT
Automated javascript unit testing
ryan_chambers
 
PDF
Testing for Pragmatic People
davismr
 
ODP
Testing in Laravel
Ahmed Yahia
 
PDF
Easy tests with Selenide and Easyb
Iakiv Kramarenko
 
PDF
How do I write Testable Javascript?
Gavin Pickin
 
PDF
How do I write Testable Javascript
ColdFusionConference
 
Testing the Untestable
Mark Baker
 
Developer Tests - Things to Know (Vilnius JUG)
vilniusjug
 
Automated javascript unit testing
ryan_chambers
 
Testing for Pragmatic People
davismr
 
Testing in Laravel
Ahmed Yahia
 
Easy tests with Selenide and Easyb
Iakiv Kramarenko
 
How do I write Testable Javascript?
Gavin Pickin
 
How do I write Testable Javascript
ColdFusionConference
 

What's hot (20)

PDF
Write readable tests
Marian Wamsiedel
 
PPTX
Codeception
少東 張
 
KEY
Workshop quality assurance for php projects tek12
Michelangelo van Dam
 
PDF
Quality Assurance for PHP projects - ZendCon 2012
Michelangelo van Dam
 
PDF
Kiss PageObjects [01-2017]
Iakiv Kramarenko
 
PDF
QA Lab: тестирование ПО. Станислав Шмидт: "Self-testing REST APIs with API Fi...
GeeksLab Odessa
 
PDF
Workshop quality assurance for php projects - phpdublin
Michelangelo van Dam
 
PDF
Web ui tests examples with selenide, nselene, selene & capybara
Iakiv Kramarenko
 
PDF
Vejovis: Suggesting Fixes for JavaScript Faults
SALT Lab @ UBC
 
PDF
UA testing with Selenium and PHPUnit - PFCongres 2013
Michelangelo van Dam
 
PDF
Understanding JavaScript Testing
jeresig
 
PDF
Why Your Test Suite Sucks - PHPCon PL 2015
CiaranMcNulty
 
PDF
Workshop quality assurance for php projects - ZendCon 2013
Michelangelo van Dam
 
PDF
Unit testing PHP apps with PHPUnit
Michelangelo van Dam
 
PDF
QA Lab: тестирование ПО. Яков Крамаренко: "KISS Automation"
GeeksLab Odessa
 
PPT
TDD, BDD, RSpec
Nascenia IT
 
PDF
From Good to Great: Functional and Acceptance Testing in WordPress.
David Aguilera
 
PDF
Enrique Amodeo | Graphql + Microservices = Win! | Codemotion Madrid 2018
Codemotion
 
PDF
Codeception presentation
Andrei Burian
 
PPTX
Qunit Java script Un
akanksha arora
 
Write readable tests
Marian Wamsiedel
 
Codeception
少東 張
 
Workshop quality assurance for php projects tek12
Michelangelo van Dam
 
Quality Assurance for PHP projects - ZendCon 2012
Michelangelo van Dam
 
Kiss PageObjects [01-2017]
Iakiv Kramarenko
 
QA Lab: тестирование ПО. Станислав Шмидт: "Self-testing REST APIs with API Fi...
GeeksLab Odessa
 
Workshop quality assurance for php projects - phpdublin
Michelangelo van Dam
 
Web ui tests examples with selenide, nselene, selene & capybara
Iakiv Kramarenko
 
Vejovis: Suggesting Fixes for JavaScript Faults
SALT Lab @ UBC
 
UA testing with Selenium and PHPUnit - PFCongres 2013
Michelangelo van Dam
 
Understanding JavaScript Testing
jeresig
 
Why Your Test Suite Sucks - PHPCon PL 2015
CiaranMcNulty
 
Workshop quality assurance for php projects - ZendCon 2013
Michelangelo van Dam
 
Unit testing PHP apps with PHPUnit
Michelangelo van Dam
 
QA Lab: тестирование ПО. Яков Крамаренко: "KISS Automation"
GeeksLab Odessa
 
TDD, BDD, RSpec
Nascenia IT
 
From Good to Great: Functional and Acceptance Testing in WordPress.
David Aguilera
 
Enrique Amodeo | Graphql + Microservices = Win! | Codemotion Madrid 2018
Codemotion
 
Codeception presentation
Andrei Burian
 
Qunit Java script Un
akanksha arora
 
Ad

Similar to Clean tests good tests (20)

PDF
Php tests tips
Damian Sromek
 
KEY
Unit testing zend framework apps
Michelangelo van Dam
 
PPTX
Using of TDD practices for Magento
Ivan Chepurnyi
 
PPT
PHP Unit Testing
Tagged Social
 
PPTX
Testy integracyjne
Łukasz Zakrzewski
 
PPT
Test driven development_for_php
Lean Teams Consultancy
 
PDF
Unit testing after Zend Framework 1.8
Michelangelo van Dam
 
PPTX
Test in action week 4
Yi-Huan Chan
 
ODP
Bring the fun back to java
ciklum_ods
 
KEY
How To Test Everything
noelrap
 
PPTX
Testing ASP.NET - Progressive.NET
Ben Hall
 
PDF
Unit testing with zend framework tek11
Michelangelo van Dam
 
PDF
PHPUnit best practices presentation
Thanh Robi
 
PDF
Better Testing With PHP Unit
sitecrafting
 
KEY
Unit testing with zend framework PHPBenelux
Michelangelo van Dam
 
PPT
Automated Unit Testing
Mike Lively
 
PDF
Тестирование и Django
MoscowDjango
 
PDF
How to write clean tests
Danylenko Max
 
ODP
Getting to Grips with SilverStripe Testing
Mark Rickerby
 
PDF
EPHPC Webinar Slides: Unit Testing by Arthur Purnama
Enterprise PHP Center
 
Php tests tips
Damian Sromek
 
Unit testing zend framework apps
Michelangelo van Dam
 
Using of TDD practices for Magento
Ivan Chepurnyi
 
PHP Unit Testing
Tagged Social
 
Testy integracyjne
Łukasz Zakrzewski
 
Test driven development_for_php
Lean Teams Consultancy
 
Unit testing after Zend Framework 1.8
Michelangelo van Dam
 
Test in action week 4
Yi-Huan Chan
 
Bring the fun back to java
ciklum_ods
 
How To Test Everything
noelrap
 
Testing ASP.NET - Progressive.NET
Ben Hall
 
Unit testing with zend framework tek11
Michelangelo van Dam
 
PHPUnit best practices presentation
Thanh Robi
 
Better Testing With PHP Unit
sitecrafting
 
Unit testing with zend framework PHPBenelux
Michelangelo van Dam
 
Automated Unit Testing
Mike Lively
 
Тестирование и Django
MoscowDjango
 
How to write clean tests
Danylenko Max
 
Getting to Grips with SilverStripe Testing
Mark Rickerby
 
EPHPC Webinar Slides: Unit Testing by Arthur Purnama
Enterprise PHP Center
 
Ad

Recently uploaded (20)

PDF
AI Unleashed - Shaping the Future -Starting Today - AIOUG Yatra 2025 - For Co...
Sandesh Rao
 
PPTX
The Future of AI & Machine Learning.pptx
pritsen4700
 
PDF
Economic Impact of Data Centres to the Malaysian Economy
flintglobalapac
 
PDF
AI-Cloud-Business-Management-Platforms-The-Key-to-Efficiency-Growth.pdf
Artjoker Software Development Company
 
PDF
Structs to JSON: How Go Powers REST APIs
Emily Achieng
 
PDF
Make GenAI investments go further with the Dell AI Factory
Principled Technologies
 
PDF
The Future of Mobile Is Context-Aware—Are You Ready?
iProgrammer Solutions Private Limited
 
PDF
CIFDAQ's Market Wrap : Bears Back in Control?
CIFDAQ
 
PDF
Software Development Methodologies in 2025
KodekX
 
PDF
Doc9.....................................
SofiaCollazos
 
PDF
Get More from Fiori Automation - What’s New, What Works, and What’s Next.pdf
Precisely
 
PDF
How Open Source Changed My Career by abdelrahman ismail
a0m0rajab1
 
PDF
Research-Fundamentals-and-Topic-Development.pdf
ayesha butalia
 
PDF
Accelerating Oracle Database 23ai Troubleshooting with Oracle AHF Fleet Insig...
Sandesh Rao
 
PDF
How ETL Control Logic Keeps Your Pipelines Safe and Reliable.pdf
Stryv Solutions Pvt. Ltd.
 
PDF
MASTERDECK GRAPHSUMMIT SYDNEY (Public).pdf
Neo4j
 
PDF
Google I/O Extended 2025 Baku - all ppts
HusseinMalikMammadli
 
PDF
GDG Cloud Munich - Intro - Luiz Carneiro - #BuildWithAI - July - Abdel.pdf
Luiz Carneiro
 
PPTX
New ThousandEyes Product Innovations: Cisco Live June 2025
ThousandEyes
 
PDF
NewMind AI Weekly Chronicles - July'25 - Week IV
NewMind AI
 
AI Unleashed - Shaping the Future -Starting Today - AIOUG Yatra 2025 - For Co...
Sandesh Rao
 
The Future of AI & Machine Learning.pptx
pritsen4700
 
Economic Impact of Data Centres to the Malaysian Economy
flintglobalapac
 
AI-Cloud-Business-Management-Platforms-The-Key-to-Efficiency-Growth.pdf
Artjoker Software Development Company
 
Structs to JSON: How Go Powers REST APIs
Emily Achieng
 
Make GenAI investments go further with the Dell AI Factory
Principled Technologies
 
The Future of Mobile Is Context-Aware—Are You Ready?
iProgrammer Solutions Private Limited
 
CIFDAQ's Market Wrap : Bears Back in Control?
CIFDAQ
 
Software Development Methodologies in 2025
KodekX
 
Doc9.....................................
SofiaCollazos
 
Get More from Fiori Automation - What’s New, What Works, and What’s Next.pdf
Precisely
 
How Open Source Changed My Career by abdelrahman ismail
a0m0rajab1
 
Research-Fundamentals-and-Topic-Development.pdf
ayesha butalia
 
Accelerating Oracle Database 23ai Troubleshooting with Oracle AHF Fleet Insig...
Sandesh Rao
 
How ETL Control Logic Keeps Your Pipelines Safe and Reliable.pdf
Stryv Solutions Pvt. Ltd.
 
MASTERDECK GRAPHSUMMIT SYDNEY (Public).pdf
Neo4j
 
Google I/O Extended 2025 Baku - all ppts
HusseinMalikMammadli
 
GDG Cloud Munich - Intro - Luiz Carneiro - #BuildWithAI - July - Abdel.pdf
Luiz Carneiro
 
New ThousandEyes Product Innovations: Cisco Live June 2025
ThousandEyes
 
NewMind AI Weekly Chronicles - July'25 - Week IV
NewMind AI
 

Clean tests good tests

  • 1. Clean tests - good tests Petr Heinz
  • 2. Time for an exercise
  • 3. Time for an exercise Who have ever written an automated test?
  • 4. Time for an exercise Who have ever written an automated test? Who already dealt with tests failing for no apparent reason?
  • 5. Time for an exercise Who have ever written an automated test? Who already dealt with tests failing for no apparent reason? Who had ever feeling like the tests are just throwing obstacles in your way?
  • 6. How is testing done with ShopSys Framework Unit tests - PHPUnit Integration / database tests Crawler tests Acceptance tests - Codeception, Selenium Performance tests automated execution on CI server (Jenkins)
  • 7. What can I expect from a good test? It is testing one functionality and it fails when it doesn’t work properly. It is robust enough not to fail when changing unrelated code. Even after two months I know what, how and why it is testing. When it fails, I know where the problem is. It is easy to execute the test and it runs fast. Having unexecuted test is useless. It is testing an important functionality. The aim isn’t 100% coverage.
  • 8. Test phases Arrange - initial requirements setting Act - the execution of the test Assert - expected result control Each phase should be easily told apart in the code. Don’t be afraid to extract bit of the code just to make it more readable.
  • 9. Finally, source codes! Let’s have a look at an acceptance test for searching product in the administration using its catalogue number
  • 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('Advanced search'); $me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum'); $me->fillFieldByCss('.js-search-rule-value input', '9176544MG'); $me->clickByText('Search'); $me->seeInCss('Aquila Still Spring Water', '.js-grid-column-name'); $foundProductCount = $me->countVisibleByCss('tbody .table-grid__row'); assertEquals(1, $foundProductCount); } } Acceptance test of filtering - the original code
  • 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('Log in'); } } LoginPage object
  • 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('Advanced search'); $me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum'); $me->fillFieldByCss('.js-search-rule-value input', '9176544MG'); $me->clickByText('Search'); $me->seeInCss('Aquila Still Spring Water', '.js-grid-column-name'); $foundProductCount = $me->countVisibleByCss('tbody .table-grid__row'); assertEquals(1, $foundProductCount); } } Acceptance test of filtering - using the 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('Log in'); } public function assertLoginFailed() { $this->tester->see('Login failed.'); $this->tester->seeCurrentPageEquals('/admin/'); } } LoginPage object - assert extension
  • 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('Dashboard'); } 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(); } } Acceptance test of filtering - reusing the 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('Advanced search'); $me->selectOptionByCssAndValue('.js-search-rule-subject', 'productCatnum'); $me->fillFieldByCss('.js-search-rule-value input', '9176544MG'); $me->clickByText('Search'); $me->seeInCss('Aquila Still Spring Water', '.js-grid-column-name'); $foundProductCount = $me->countVisibleByCss('tbody .table-grid__row'); assertEquals(1, $foundProductCount); } } Acceptance test of filtering - using 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('Advanced search'); $this->tester->selectOptionByCssAndValue('.js-search-rule-subject', $searchSubject); $this->tester->fillFieldByCss('.js-search-rule-value input', $value); $this->tester->clickByText('Search'); } 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); } } ProductSearchPage object
  • 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); } } Acceptance test of filtering - using the ProductSearchPage
  • 18. Naming of the testing methods Testing methods don’t have to be named exactly after the tested method. It is suitable to name the methods after the tested scenario. The intention and the expectations of the test should be clear. If it’s not easy to name the testing method it might be the case you are testing too many things at once. Don’t be afraid of long names.
  • 19. Back to code! Let’s have a look at a unit test of method for adding product to the cart
  • 20. interface CartService { // … /** * @param SS6ShopBundleModelCartCart $cart * @param SS6ShopBundleModelProductProduct $product * @param int $quantity * @return SS6ShopBundleModelCartAddProductResult * @throws SS6ShopBundleModelCartInvalidQuantityException */ public function addProductToCart(Cart $cart, Product $product, $quantity); // … } Test class interface
  • 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(); } Interface of the return value of the tested method
  • 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); } // … } Adding to the cart unit test - original method name
  • 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); } // … } Adding to the cart unit test - new method name
  • 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); } // … } Adding to the cart unit test - original method name
  • 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); } // … } Adding to the cart unit test - new method name
  • 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()); } // … } Adding to the cart unit test - original method name
  • 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()); } // … } Adding to the cart unit test - new method name?
  • 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()); } // … } Adding to the cart unit test - separating the method
  • 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()); } // … } Adding to the cart unit test - original method name
  • 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()); } // … } Adding to the cart unit test - separating the method
  • 31. Mocking Mocks are good when simulating too complex objects. Its behavior can be controlled well directly in the test code. It is possible to use it when verifying correct communication between classes. It is good to extract its creation to a private method.
  • 32. To the code! Let’s have a look at a demonstration of a mocking in database/integration test
  • 33. interface WebService { // … /** * @param SS6ShopBundleComponentWebServiceRequest $request * @return resource */ public function getResponseStream(Request $request); // … } Mocked class interface
  • 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; } // … } Creating the mock in a private class
  • 35. class TransferProductTest extends DatabaseTestCase { // … /** * @param string $fileName * @return SS6ShopBundleModelTransferTransferFacade */ private function createTransferFacadeMockingWebServiceWithFile($fileName) { return new TransferFacade( $this->getContainer()->get(TransferRepository::class), $this->mockWebServicReturningFileResource($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) ); } // … } Injecting the mock into the real tested class
  • 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); } // … } Intergraton/database test
  • 37. Some advice in conclusion Tests are not here in order “to exist”, they are here for you. Start with testing the most important scenarios. Well-kept demonstration data which you are going to use in the tests will help. Don’t be afraid to create special classes only for the test. Some tests are worth deleting. Having clean code in tests is equally important as having it in application code.
  • 38. Thank you for your attention Let’s get down to your questions! [email protected]