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
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
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
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!
petr.heinz@shopsys.com