Wrocław 2013 Wrocław University of Technology yesterday – today - tomorrow.
(PHPers Wrocław #5) How to write valuable unit test?
-
Upload
rst-software-masters -
Category
Technology
-
view
302 -
download
2
Transcript of (PHPers Wrocław #5) How to write valuable unit test?
How to write valuable unit
tests?
Łukasz Wróbel Michał Kopacz
Please save your questions until the end of presentation
75%CODE COVERAGE FOR UNIT TESTS
Tests without assertions
How we can measure quality of tests?
Workshops
Value = Benefits - Costs
05 06 07 08 0904
10
TEAM CODE
Readable tests
Data Provider /**
* @dataProvider dataCaseProvider
*/
public function testGetStatus(array $data, $expectedIsOk, $expectedIsWarning)
{
$statusProviderMock = $this->getMockBuilder(QueueStatusProvider::class)
->setConstructorArgs([$this->repositoryMock])
->setMethods(['getData'])
->getMock();
$statusProviderMock->expects($this->once())
->method('getData')
->willReturn($data);
$result = $statusProviderMock->getStatus();
$this->assertInstanceOf(StatusInterface::class, $result);
$this->assertSame($expectedIsOk, $result->isOk());
$this->assertSame($expectedIsWarning, $result->isWarning());
$this->assertSame($data, $result->getDetails());
}
Data Provider public function dataCaseProvider() { return [ [ [ QueueStatusProvider::SECTION_WARNING => [ QueueStatusProvider::STATUS_CATEGORY_DELAYED => [1], ], QueueStatusProvider::SECTION_FAILURE => [ QueueStatusProvider::STATUS_CATEGORY_DELAYED => [1, 2, 3], QueueStatusProvider::STATUS_CATEGORY_FAILED => [], ], ], false, //expectedIsOk false //expectedIsWarning ], [ [ QueueStatusProvider::SECTION_WARNING => [ QueueStatusProvider::STATUS_CATEGORY_DELAYED => [1], ], QueueStatusProvider::SECTION_FAILURE => [ QueueStatusProvider::STATUS_CATEGORY_DELAYED => [], QueueStatusProvider::STATUS_CATEGORY_FAILED => [], ], ], false, //expectedIsOk true //expectedIsWarning ],
Separate each case
/** * @test */public function returns_not_ok_status_when_any_delayed_or_failed_report_exists()
/** * @test */public function returns_not_ok_with_warning_status_when_only_delayed_reports_exist()
Name should include scenario under which it’s being tested.
returns_not_ok_status_when_any_delayed_or_failed_report_exists
Name should say something about the expected behaviour.
returns_not_ok_status_when_any_delayed_or_failed_report_exists
Expected behaviour
state-based testing interaction testing
Examples /** * @test */ public function talk_contain_message_after_it_has_been_added() { //Given $message = new Message("test message");
$talkId = 1;
$talkStorage = $this->getMock(TalkStorage::class); $talkManager = new TalkManager($talkStorage);
$talkStorage->method('getMessages') ->with($talkId)
->willReturn([$message]);
//When $talkManager->add($talkId, $message);
//Then $this->assertTrue($talkManager->hasMessage($talkId, $message)); }
/** * @test */ public function saved_message_in_storage_when_it_is_adding_to_talk() { //Given $message = new Message("test message");
$talkId = 1;
$talkStorage = $this->getMock(TalkStorage::class); $talkManager = new TalkManager($talkStorage);
//Expect $talkStorage->expects($this->once()) ->method('save') ->with($talkId, $message);
//When $talkManager->add($talkId, $message); }
Structure
Given
When
Then
Given
Expect
When
Stub vs Mock
$talkStorage->method('getMessages') ->with($talkId) ->willReturn([$message]);
$talkStorage->expects($this->once()) ->method('save') ->with($talkId, $message);
Stub Mock
given expect
Testable code
public function handle($id, $isForced = false)
{
$logger = $this->debugLogger;
$item = $this->debtReportTable->getById($id);
if (!$item) {
throw new DebtReportNotFoundException('Report does not exist: ' . $id);
}
$content = $item->getReportContent();
$this->updateContentWithEntity($content, $item)
->renameKeys($content)
->organizeCurrency($content)
->addFilesToReport($content, $isForced);
$this->apiClient->setRoute('api/rest/v1/debt');
$response = $this->apiClient->dispatch(Request::METHOD_POST, $content)->getResponse();
if (in_array($this->apiClient->getResponseStatusCode(), $this->AdaUnrecoverableFailureCodes)) {
$responseRawBody = $this->apiClient->getResponseRawBody();
if ($logger) {
$logger->critical(sprintf('Debt reporting failed at AdaSoftware request with status: [%s:
%s]',
$this->apiClient->getResponseStatusCode(),
$this->apiClient->getResponseStatus()));
$logger->debug(sprintf('ADA API internal server error [%s: %s]. Reason: %s',
$this->apiClient->getResponseStatusCode(),
$this->apiClient->getResponseStatus(), var_export($responseRawBody, true)));
}
$this->sendMailWithLog($item, $responseRawBody);
$item->setStatus(DebtReport::STATUS_FAILED);
$this->updateReportStats($item);
$this->debtReportTable->save($item);
throw new InternalSwApiException($responseRawBody);
} elseif (!$this->apiClient->hasValidData() || !isset($response['_embedded']['debts']) ||
!is_array($response['_embedded']['debts'])) {
$responseRawBody = $this->apiClient->getResponseRawBody();
if ($logger) {
$logger->critical('Debt reporting failed at AdaSoftware request.');
$logger->debug(sprintf('ADA API failure [%s: %s]. Response body: %s',
$this->apiClient->getResponseStatusCode(),
$this->apiClient->getResponseStatus(), var_export($responseRawBody, true)));
}
$this->sendMailWithLog($item, $responseRawBody);
$item->setStatus(DebtReport::STATUS_FAILED);
$this->debtReportTable->save($item);
} else {
if ($logger) {
$logger->debug(sprintf('ADA API success [%s: %s]. Response body: %s',
$this->apiClient->getResponseStatusCode(),
$this->apiClient->getResponseStatus(), var_export($this->apiClient->getResponseRawBody(),
true)));
}
foreach ($response['_embedded']['debts'] as $debt) {
$link = new DebtToDebtReport();
$link->setId($debt['id']);
$item->addDebtToDebtReport($link);
}
$item->setStatus(DebtReport::STATUS_COMPLETED);
$this->debtReportTable->save($item);
$this->updateReportStats($item);
return true;
}
return false;
}
Testable code?
BUILD REQUEST CONTENT
SEND REQUEST
HANDLE RESPONSE
WRONG STATUS CODE
INVALID RESPONSE BODY
PROCESS VALID RESPONSE
Let’s test request content building
$content = $item->getReportContent(); $this->updateContentWithEntity($content, $item) ->renameKeys($content) ->organizeCurrency($content) ->addFilesToReport($content, $isForced);
$this->apiClient->setRoute('api/rest/v1/debt'); $response = $this->apiClient->dispatch(Request::METHOD_POST, $content)->getResponse();
Verify request content in mock
$this->apiClientMock->expects($this->once()) ->method('dispatch') ->with(Request::METHOD_POST, $expectedContent);
But it’s not the end of preparing test ... $response = $this->apiClient->dispatch(Request::METHOD_POST, $content)->getResponse();
} else { if ($logger) { $logger->debug(sprintf('ADA API success [%s: %s]. Response body: %s', $this->apiClient->getResponseStatusCode(), $this->apiClient->getResponseStatus(), var_export($this->apiClient->getResponseRawBody(), true))); }
foreach ($response['_embedded']['debts'] as $debt) { $link = new DebtToDebtReport(); $link->setId($debt['id']); $item->addDebtToDebtReport($link); }
$item->setStatus(DebtReport::STATUS_COMPLETED); $this->debtReportTable->save($item); $this->updateReportStats($item);
return true; }
return false;}
...
Double other collaborators
protected function setUp()
{
$this->statsServiceMock = $this->getMockBuilder(\Inkasso\Statistics\DebtReportStatisticsService::class)
->disableOriginalConstructor()->getMock();
$this->apiClientMock = $this->getMockBuilder(\ApiClient\Client\AdaSoftware::class)
->disableOriginalConstructor()->getMock();
$this->debtReportTable = $this->getMockBuilder(\DebtReport\Entity\Table\DebtReportTable::class)
->disableOriginalConstructor()->getMock();
$this->storageAdapterMock = $this->getMockBuilder(\FileStorage\Storage\StorageAdapterInterface::class)
->disableOriginalConstructor()->getMock();
$this->consumer = new ReportHandler($this->statsServiceMock, $this->storageAdapterMock, $this->debtReportTable,
$this->apiClientMock);
}
Does our object fulfill Single Responsibility Principle?
Separate logic
● RequestBuilder● Strategies for handling response
Decouple logic from infrastructure
● DebtResponseToItemsMapper● Document Object Model (DOM)● Asynchrony (android.app.Service)
foreach ($response['_embedded']['debts'] as $debt) { $link = new DebtToDebtReport(); $link->setId($debt['id']); $item->addDebtToDebtReport($link);}
$item->setStatus(DebtReport::STATUS_COMPLETED);
$this->debtReportTable->save($item); infra
logic
Tests
public function test_fetchAll_emptyLanguage(){ $paramsMock = $this->getMock('Zend\Stdlib\Parameters'); $paramsMock->expects($this->at(0)) ->method('get') ->with($this->equalTo('labels')) ->will($this->returnValue('')); $paramsMock->expects($this->at(1)) ->method('get') ->with($this->equalTo('module')) ->will($this->returnValue('')); $paramsMock->expects($this->at(2)) ->method('get') ->with($this->equalTo('lang')) ->will($this->returnValue(''));}
Call order
Is the call order relevant?
How is it connected to the result?
Are empty strings important?
$mockObjects = [ '11' => Mockery::mock(ArticleObjectInterface::class) ->shouldReceive('getId') ->times(4) ->andReturn('11') ->getMock(), '22' => Mockery::mock(ArticleObjectInterface::class) ->shouldReceive('getId') ->times(5) ->andReturn('22') ->getMock(), '33' => Mockery::mock(ArticleObjectInterface::class) ->shouldReceive('getId') ->times(4) ->andReturn('33') ->getMock(),];
Call count
Are these performance tests?
Call count and parameters verification
Query Command
stub mock
low maintenance high value
Testing privacy public function testSetFilters()
{
$filters = ['foo' => 'bar'];
/** @var Client|\PHPUnit_Framework_MockObject_MockObject $clientMock */
$clientMock = $this->getMockBuilder(Client::class)
->disableOriginalConstructor()
->setMethods(['addParameter'])
->getMock();
$clientMock
->expects($this->once())
->method('addParameter')
->with(
$this->equalTo(HalStorageAdapter::FILTER_PARAM),
$this->equalTo(json_encode($filters))
);
$halStorageAdapter = new HalStorageAdapter('/foo/bar', $clientMock);
ReflectionHelper::executeMethod($halStorageAdapter, 'setFilters', [[]]);
ReflectionHelper::executeMethod($halStorageAdapter, 'setFilters', [$filters]);
}
Cannot we test this by calling public methods?
Are you tempted to:● Call a private method via reflection?● Mock a private method when testing a public method?
Public
Private
SRPPublic Public+
Unpredictability$timestamp = time();$this->assertEquals( date('c', $timestamp), Date::isoFormat(date(\DateTime::RFC1123, $timestamp)));
random()
What about Daylight Saving Time?
If I was about to give you…
Just one, the most precious piece of advice
Remember some of the previous examples?
Tests without assertions
$mockObjects = [ '11' => Mockery::mock(ArticleObjectInterface::class) ->shouldReceive('getId') ->times(4) ->andReturn('11') ->getMock(), '22' => Mockery::mock(ArticleObjectInterface::class) ->shouldReceive('getId') ->times(5) ->andReturn('22') ->getMock(), '33' => Mockery::mock(ArticleObjectInterface::class) ->shouldReceive('getId') ->times(4) ->andReturn('33') ->getMock(),];
Testing the implementation
public function handle($id, $isForced = false)
{
$logger = $this->debugLogger;
$item = $this->debtReportTable->getById($id);
if (!$item) {
throw new DebtReportNotFoundException('Report does not exist: ' . $id);
}
$content = $item->getReportContent();
$this->updateContentWithEntity($content, $item)
->renameKeys($content)
->organizeCurrency($content)
->addFilesToReport($content, $isForced);
$this->apiClient->setRoute('api/rest/v1/debt');
$response = $this->apiClient->dispatch(Request::METHOD_POST, $content)->getResponse();
if (in_array($this->apiClient->getResponseStatusCode(), $this->AdaUnrecoverableFailureCodes)) {
$responseRawBody = $this->apiClient->getResponseRawBody();
if ($logger) {
$logger->critical(sprintf('Debt reporting failed at AdaSoftware request with status: [%s:
%s]',
$this->apiClient->getResponseStatusCode(),
$this->apiClient->getResponseStatus()));
$logger->debug(sprintf('ADA API internal server error [%s: %s]. Reason: %s',
$this->apiClient->getResponseStatusCode(),
$this->apiClient->getResponseStatus(), var_export($responseRawBody, true)));
}
$this->sendMailWithLog($item, $responseRawBody);
$item->setStatus(DebtReport::STATUS_FAILED);
$this->updateReportStats($item);
$this->debtReportTable->save($item);
throw new InternalSwApiException($responseRawBody);
} elseif (!$this->apiClient->hasValidData() || !isset($response['_embedded']['debts']) ||
!is_array($response['_embedded']['debts'])) {
$responseRawBody = $this->apiClient->getResponseRawBody();
if ($logger) {
$logger->critical('Debt reporting failed at AdaSoftware request.');
$logger->debug(sprintf('ADA API failure [%s: %s]. Response body: %s',
$this->apiClient->getResponseStatusCode(),
$this->apiClient->getResponseStatus(), var_export($responseRawBody, true)));
}
$this->sendMailWithLog($item, $responseRawBody);
$item->setStatus(DebtReport::STATUS_FAILED);
$this->debtReportTable->save($item);
} else {
if ($logger) {
$logger->debug(sprintf('ADA API success [%s: %s]. Response body: %s',
$this->apiClient->getResponseStatusCode(),
$this->apiClient->getResponseStatus(), var_export($this->apiClient->getResponseRawBody(),
true)));
}
foreach ($response['_embedded']['debts'] as $debt) {
$link = new DebtToDebtReport();
$link->setId($debt['id']);
$item->addDebtToDebtReport($link);
}
$item->setStatus(DebtReport::STATUS_COMPLETED);
$this->debtReportTable->save($item);
$this->updateReportStats($item);
return true;
}
return false;
}
Big chunk of code
Would it all happen if…
Red
Green
Refactor*TDD
Invitation for workshops
3 XII 2016 (Saturday)
9:30-17:00RST (Racławicka 2)
http://rst.com.pl/unittestsworkshop/
Assignment● No time spent on the basics.● Equal level.● Engagement.
Questions?
Michał Kopacz Łukasz Wróbel
Thank you!