php tests tips
DESCRIPTION
FewTRANSCRIPT
PHP Tests tips How not to shoot yourself in the foot?
Damian Sromekdamiansromek.pl
2012-06
Agenda
● What's about those tests?● What are the tests about?● What to do and not to do?
Test types
1. Acceptance (end-to-end)Test as if end user would use the whole system/feature.
2. IntegrationTest how different parts of system work together - eg. couple of classes or your class using some web service
3. UnitTest single unit (eg. class). Mock all dependencies, eg. web service, file system.
Tests will save you time & troubles
1. Prove you've done your work2. Help you check much faster if you're work is
done3. Protect you from breaking things - regresion4. Help you make better code design - easier to
maintain5. Let you apply changes with less worries -
refactoring will improve things without breaking anything
How to be happy about tests?
1. If you're new to testing try with writing tests after writing the code
2. Once you're confident about testing try to write tests before the code - TDD
3. Start with acceptance tests, than integration and unit tests when your functionality is implemented
4. Run tests often - after every change and before any commit
5. Use continuous integration server
You will regret you've got a lot of tests if you:
1. make them complex and hard to read2. duplicate the test code3. do not use descriptive fail messages4. do not run them often - eg. you don't have
continuous integration server 5. do not make them run fast
Behavioral tests
Draft - Behat Feature: Log in and use the application In order to use the application As a user I need to log in using my smart card
@javascriptScenario: Log in with inactive smart card Given I am inactive smart card user Given I am on "http://localhost/somethign.cool.php" When I log in Then I should see log in error message
What's good test like?
1. Finds as much bugs as possible2. Tells you what the software can do and how
to use it3. Is easy to maintain4. Independent from other tests and repeatable
How to start testing?
1. Write acceptance test for functionality you're implementing - it should cover main story flow
2. Make the test show meaningful fail message3. Implement functionality so the test is passing4. Add tests covering more story flows5. Improve functionality code so all tests are
passing again6. Refactor (also tests)7. Enjoy!
How to start even better?
1. "Wait" for a bug2. Write a test confirming/reproducing that bug3. Fix the code using the test you've got4. Enjoy!
TDD flow
Repeat this process1. Write test2. Make test failure message descriptive and
easy to understand3. Make the test pass - implement feature
you've write the test for4. Refactor
Do not - test something irrelevant<?php // SHOULD TEST PLUGIN BUT IS TESTING DATABASE MODEL AND NOTHING MORE class Plugin_Smooth_Streaming_BoundariesTest extends TestCase_Plugin{ ... public function testLowerBoundary() { $settings = Xstream_Loader::getModelHelper("SettingHelper"); $lowerLimit = $settings->setSetting('plugin_boundaries', 'lower_limit',1.0); $this->assertEquals(1.0, $settings->getSetting('plugin_boundaries', 'lower_limit')->value); } ... }
Do not - test too much at once<?php// TRY NOT TO MAKE MORE THAN 2-3 ASSERTIONS IN ONE TESTclass ResponseSetTest extends PHPUnit_Framework_TestCase
{
...
public function testAddItems()
{
$responseSet = new Xs_Monitor_Response_Set('test');
$this->assertEquals(Xs_Monitor_Interface::STATE_WARNING, $responseSet->getStateAsInteger());
$responseSet->addResponse(new Xs_Monitor_Response('service 1', Xs_Monitor_Interface::STATE_OK, 'detail of service 1'));
$this->assertEquals(Xs_Monitor_Interface::STATE_OK, $responseSet->getStateAsInteger());
$this->assertNotEquals('detail of service 1', $responseSet->getDetails());
$responseSet->addResponse(new Xs_Monitor_Response('service 2', 3, 'detail of service 2'));
$this->assertEquals(3, $responseSet->getStateAsInteger());
$this->assertEquals('UNKNOWN', $responseSet->getState());
$this->assertEquals('detail of service 2', $responseSet->getDetails());
$responseSet
->addResponse(new Xs_Monitor_Response('service 3', Xs_Monitor_Interface::STATE_WARNING, 'detail of service 3'));
$this->assertEquals(Xs_Monitor_Interface::STATE_WARNING, $responseSet->getStateAsInteger());
$this->assertEquals('detail of service 3', $responseSet->getDetails());
$responseSet
->addResponse(new Xs_Monitor_Response('service 4', Xs_Monitor_Interface::STATE_CRITICAL, 'detail of service 4'));
$this->assertEquals(Xs_Monitor_Interface::STATE_CRITICAL, $responseSet->getStateAsInteger());
$this->assertEquals('detail of service 4', $responseSet->getDetails());
$responseSet
->addResponse(new Xs_Monitor_Response('service 5', Xs_Monitor_Interface::STATE_CRITICAL, 'detail of service 5'));
$this->assertEquals(Xs_Monitor_Interface::STATE_CRITICAL, $responseSet->getStateAsInteger());
$this->assertEquals('detail of service 4', $responseSet->getDetails());
$responseSet
->addResponse(new Xs_Monitor_Response('service 6', Xs_Monitor_Interface::STATE_WARNING, 'detail of service 6'));
$this->assertEquals(Xs_Monitor_Interface::STATE_CRITICAL, $responseSet->getStateAsInteger());
$this->assertEquals('detail of service 4', $responseSet->getDetails());
return $responseSet;
}
Do - use meaningful test name
Test name should be descriptive even if it has very long name.Try to avoid test names like "testAdd" etc.Better would be something like "testAddWillPutNewItemIntoContainer" or "testAddWillThrowExceptionIfContainerIsFull" /** * @test */
public function add() { ...
Do - use data providers<?php
namespace Xs\Test\Unit;
class StringTest extends TestCase
{
/** * @param string $heystack * @param string $needle * @param boolean $expected * * @dataProvider endsWithDataProvider */
public function testEndsWith($heystack, $needle, $expected)
{
$this->assertEquals($expected, \Xs\String::endsWith($heystack, $needle));
}
public function endsWithDataProvider()
{ return array( array('asdf', 'f', true), array('asdf', 'df', true), array('asdf', 'asdf', true), array('asdf', 'd', false), array('asdf', 'xf', false), array('asdf', 'asd', false), ); }
}
Do - test one unit in unit test
Unit test should NOT:1. Test more than one unit/class2. Touch database, filesystem,
API/WebService etc. - you should mock all "externals"
3. Execute very fast - matter of 10th of second
Do - make tests easy to read and understandclass Acceptance_Product_ProductListTest extends Test_TestCase{ public function testReturnsErrorAboutNoProductsUserCanBuyWhenHeHasNoProductsRelatedToMedia() { $this->user() ->logIn() ->withoutProducts(array( $this->svodProductId, $this->premiumSvodProductId )) ->withoutOrderedMedia($this->mediaId); $this->shop()->respondsJsonAboutNoProductsAvailable($this->mediaId);
}
Do - make tests easy to read and understand (2)
Draftclass SortingTest extends \Xs\CanalDigital\Test\Stb\Adb\TestCase
{
/** * @test */
public function mediaListWithoutFixedSortingCanBeSortedByPressingRedButton()
{
$this->user()
->onPage($this->stbApp()->page('sort_fixed'));
$initialAppState = $this->stbApp()->getState('maincovers');
$this->user()->pressedButton(Key::BUTTON_RED); $appStateAfterPressingRedButton = $this->stbApp()->getState('maincovers'); // TODO: better way of comparing items sorting $this->assertNotEquals( $initialAppState, $appStateAfterPressingRedButton, 'Pressing red button should change page sorting' );
}
Summary
1. Use TDD2. Unit tests should test what you've written /
proper unit and nothing more3. Do not test too much in one test4. Make your tests easy to maintain - proper
naming etc.5. Try to use data provider to test a lot of
different cases without code duplication
Thank you
@see Test Driven Development@see PHPUnit@see Behavioral Driven Development@see Behat @see Growing Object-Oriented Software, Guided by Tests@see www.jenkins-ci.org