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

(Boiling Frogs 2017) How to increase the value of unit tests?

(Boiling Frogs 2017) How to increase the value of unit tests?

Quality Sheriffs

February 26, 2017
Tweet

Other Decks in Technology

Transcript

  1. What do we verify? /** * @dataProvider dataCaseProvider */ public

    function testGetStatus(array $data, $expectedIsOk, $expectedIsWarning) { … $result = $statusProvider->getStatus(); $this->assertSame($expectedIsOk, $result->isOk()); $this->assertSame($expectedIsWarning, $result->isWarning()); $this->assertSame($data, $result->getDetails()); }
  2. Take a look at data provider public function dataCaseProvider() {

    return [ [ [ ... ], //data false, //expectedIsOk false //expectedIsWarning ], [ [ ... ], //data false, //expectedIsOk true //expectedIsWarning ], Not ok status when any delayed or failed report exists. Not ok with warning status when only delayed reports exist.
  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. State-based testing public function talk_contains_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->addMessage($talkId, $message); //Then $this->assertTrue($talkManager->hasMessage($talkId, $message)); }
  5. Interaction testing public function saves_message_to_storage_when_it_is_being_added_to_a_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); }
  6. 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?
  7. BUILD REQUEST CONTENT SEND REQUEST HANDLE RESPONSE WRONG STATUS CODE

    INVALID RESPONSE BODY PROCESS VALID RESPONSE
  8. BUILD REQUEST CONTENT SEND REQUEST HANDLE RESPONSE WRONG STATUS CODE

    INVALID RESPONSE BODY PROCESS VALID RESPONSE
  9. Let’s test request content building $content = $item->getReportContent(); $this->updateContentWithEntity($content, $item)

    ->renameKeys($content) ->organizeCurrency($content) ->addFilesToReport($content, $isForced);
  10. But it’s not the end of preparing test $response =

    $this->apiClient->dispatch(Request::METHOD_POST, $content)->getResponse(); ... } else { 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; } Prepare response body Mock interaction with debtReportTable
  11. Separate logic BUILD REQUEST CONTENT SEND REQUEST HANDLE RESPONSE WRONG

    STATUS CODE INVALID RESPONSE BODY PROCESS VALID RESPONSE RequestBuilder RequestBuild er RequestBuild er WrongStatus CodeHandler
  12. Decouple logic from infrastructure 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); DebtResponseToItemMapper
  13. 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?
  14. $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?
  15. Testing privacy public function testSetFilters() { $filters = ['foo' =>

    'bar']; /** @var Client|\PHPUnit_Framework_MockObject_MockObject $clientMock */ $clientMock = $this->getMockBuilder(Client::class) ->disableOriginalConstructor() ->setMethods(['addParameter']) ->getMock(); $halStorageAdapter = new HalStorageAdapter('/foo/bar', $clientMock); ReflectionHelper::executeMethod($halStorageAdapter, 'setFilters', [[]]); ReflectionHelper::executeMethod($halStorageAdapter, 'setFilters', [$filters]); } Cannot we test this by calling public methods?
  16. Are you tempted to: • Call a private method via

    reflection? • Mock a private method when testing a public method? Public Private SRP Public Public +
  17. $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
  18. 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
  19. Currency converter • USD, EUR, PLN. • Rates provided by

    external service. • Rounding up and down.
  20. Currency converter • USD, EUR, PLN. • Rates provided by

    external service. • Rounding up and down.
  21. Step 1: expected behaviour public function returns_equal_money_if_converting_money_with_the_same_currency() { $converter =

    new MoneyConverter(); $convertedMoney = $converter->convert( new Money(10.50, new Currency('PLN')), new Currency('PLN'), MoneyConverter::ROUND_UP ); $this->assertEquals(new Money(10.50, new Currency('PLN')), $convertedMoney); } Currency Money Converter
  22. Step 1: building blocks class MoneyConverter { const ROUND_UP =

    'ROUND_UP'; public function __construct() { } public function convert(Money $fromMoney, Currency $toCurrency, $roundingMode = self::ROUND_UP) { return $fromMoney; } } Currency Money Converter
  23. Currency converter • USD, EUR, PLN. • Rates provided by

    external service. • Rounding up and down.
  24. Step 2: USD → PLN public function returns_converted_money_if_converting_with_diferent_currencies() { $exchange

    = $this->getMockForAbstractClass(ExchangeServiceInterface::class); $exchange->method('getRatio')->willReturn(4.0); $converter = new MoneyConverter($exchange); $convertedMoney = $converter->convert( new Money(Decimal::fromFloat(10.10, 2), new Currency('USD')), new Currency('PLN'), MoneyConverter::ROUND_UP ); $this->assertEquals( new Money(Decimal::fromFloat(40.40, 2), new Currency('PLN')), $convertedMoney ); }
  25. Step 2: USD → PLN class MoneyConverter { const ROUND_UP

    = 'ROUND_UP'; protected $exchangeService; public function __construct(ExchangeServiceInterface $exchangeService) { $this->exchangeService = $exchangeService; } public function convert(Money $fromMoney, Currency $toCurrency, $roundingMode = self::ROUND_UP) { $ratio = $this->exchangeService->getRatio($fromMoney->getCurrency(), $toCurrency); return new Money( $fromMoney->getAmount()->mul(Decimal::fromFloat($ratio)), $toCurrency ); } }
  26. • Do you want testing workshops to take place in

    your company? • We would work on your code, not on our examples. • Focus on what matters to you. • Long-term support available. Personalized workshops fb.me/QualitySheriffs