Upgrade to Pro — share decks privately, control downloads, hide ads and more …

(PHPers Wrocław #5) How to write valuable unit ...

(PHPers Wrocław #5) How to write valuable unit test?

Czy wiecie, jak zdefiniować czym są „dobre” testy jednostkowe? Główną obiektywną miarą ich jakości jest poziom pokrycia kodu testami. Tylko czy to wystarczy? Wprowadzając w RST zasadę 75% pokrycia kodu testami wiedzieliśmy, że samo kryterium liczbowe to za mało. Można przecież napisać testy dające 100% pokrycia, ale nie zawierające żadnej asercji. Nie chcąc bazować wyłącznie na poziomie pokrycia kodu, postanowiliśmy zebrać nasze doświadczenia w pisaniu testów jednostkowych i zorganizowaliśmy dla naszych zespołów deweloperskich warsztaty. Jeżeli chcesz się dowiedzieć: jakie nazywać testy i nadawać im czytelną strukturę, co to znaczy „testowalny kod”, jak się uchronić przed kruchością testów, czy też jaka jest różnica między pisaniem testów przed i po implementacji, to zapraszamy Cię na prezentację, podczas której podzielimy się zdobytą wiedzą i przykładami omawianymi na warsztatach.

Avatar for michalkopacz

michalkopacz

November 17, 2016
Tweet

Other Decks in Technology

Transcript

  1. 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()); }
  2. 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 ],
  3. 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()
  4. 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); }
  5. 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?
  6. BUILD REQUEST CONTENT SEND REQUEST HANDLE RESPONSE WRONG STATUS CODE

    INVALID RESPONSE BODY PROCESS VALID RESPONSE
  7. 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();
  8. 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; } ...
  9. 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); }
  10. 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
  11. 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?
  12. $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?
  13. 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?
  14. Are you tempted to: • Call a private method via

    reflection? • Mock a private method when testing a public method? Public Private SRP Public Public +
  15. $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
  16. 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