Back-End

6 jul, 2015

Como o PHP 7 pode te ajudar a escrever testes melhores

Publicidade

Com a introdução do strict type hinting, o PHP 7 vai criar testes mais robustos que nos ajudarão a desenvolver um código de melhor qualidade.

Leia este artigo para aprender como você pode escrever testes mais robustos em prática com a ajuda de strict type hinting.

Retorne declarações de tipo scalar type hints

Na sequência de um artigo sobre este assunto, agora podemos ver como essa melhoria vai nos ajudar a escrever um código melhor, mais compreensível e mais fácil de criar testes mais robustos.

Com o PHP 7, ao ler a declaração de uma função, o desenvolvedor sabe exatamente o que a função espera. Mas, sem essa melhoria, só poderíamos descobrir os parâmetros através do PHPdoc, ou talvez pelo nome da função ou seus parâmetros, ou seja, uma função chamada sum vai esperar dois parâmetros numéricos, e uma função convertToHtml retornará uma string, um parâmetro chamado $content ou $text provavelmente detém uma string, enquanto um parâmetro $int possui um inteiro.

Mas o que acontece se o programa falhar e enviar uma string quando a função esperar um número inteiro? Agora, com essa melhoria, já não temos que adivinhar.

Vantagens de type hinting em declarações de função

Vamos ver alguns códigos de amostra e como podemos usar essa nova melhoria para nosso benefício:

<?php
declare(strict_types=1);
class Math
{
  public static function factorial(int $number): int{
    if ($number < 0){
      throw new Exception( “factorial expects a positive number” );
    }
    if (1 >= $number){
      return 1;
    }
  
      return self::factorial( $number - 1) * $number;
    }
}

A primeira coisa que nós ganhamos com declarações de tipo é que não precisamos verificar se os parâmetros são não-numéricos, porque isso não é permitido. Se você passar um tipo incorreto, o PHP lança uma exceção de tipo quando o código é executado.

Outra vantagem é que não precisamos mais ter PHPDoc. Pessoalmente, eu prefiro ter essa informação extra, mas alguns colegas, com certeza, serão gratos por não terem que escrever esse tipo de documentação extra.

Além disso: com a declaração strict_types = 1, Math :: factorial (“1”); lança uma exceção, mas funciona bem no modo fraco, como esperado.

Quando você escreve os testes, já não tem a necessidade de escrever o código para verificar os tipos manualmente. Em outras palavras: se a função não aceita uma string, não temos de verificar esse caso.

Talvez a explicação acima ficará mais clara com um exemplo:

No modo estrito:

<?php
declare(strict_types=1);

require_once __DIR__."/../Math.php";

class MathTest extends PHPUnit_Framework_TestCase
{
  public function testFactorial()
  {
    $this->assertEquals(5 * 4 * 3 * 2, Math::factorial(5) );
  
    $this->assertEquals(
     Math::factorial(8) * 9,
     Math::factorial(9) );
  }
}

php-1

No modo padrão:

<?php
declare(strict_types=0);

require_once __DIR__."/../Math.php";

class MathTest extends PHPUnit_Framework_TestCase
{
  public function testFactorial()
  {
    $this->assertEquals( 5 * 4 * 3 * 2, Math::factorial(5));

    $this->assertEquals( Math::factorial(8) * 9, Math::factorial(9) );
  }

  public function testFactorialWeak()
  {
    $this->assertEquals( 4 * 3 * 2, Math::factorial("4") );
  }
}

php-2

Exemplos aplicados a testes

<?php
declare(strict_types=1);  // local file calls are strict-type checked

class HtmlHelper
{
  protected static function arrayToList(array $items, bool $ordered): string
  {
    $result = $ordered ? '<ol>' : '<ul>';
    foreach( $items as $item ) {
      $result .= sprintf( '<li>%s</li>', $item );
    }
    $result .= $ordered ? '</ol>' : '</ul>';

    return $result;
  }

  public static function arrayToUnsortedList( array $items ): string
  {
    return self::arrayToList($items, false);
  }

  public static function arrayToOrderedList( array $items ): string 
  {
    return self::arrayToList($items, true);
  }
}

A ideia, com esse código simples, é ter o tipo de retorno de strings e chamadas locais dentro do mesmo arquivo PHP. Como expliquei no meu artigo anterior, a declaração de strict_types = 0 | 1 deve estar no início do arquivo. Ela afeta apenas as chamadas invocadas no arquivo onde essa declaração está.

<?php
declare(strict_types=1);

require_once __DIR__."/../HtmlHelper.php";

class MathTest extends PHPUnit_Framework_TestCase
{
  static $data;
  static $itemsNumber;

  public static  function setUpBeforeClass()
  {
    self::$data = array('first element', 'second element', 'third element');
    self::$itemsNumber = count(self::$data);
  }

  public function testArrayToUnsortedList()
  {
    $result = HtmlHelper::arrayToUnsortedList( self::$data );
  
    $this->assertEquals( self::$itemsNumber, substr_count( $result, '<li>' ));
    $this->assertEquals( self::$itemsNumber, substr_count($result, '</li>' ));
    $this->assertEquals( 1, substr_count($result, '<ul>') );
    $this->assertEquals( 1, substr_count($result, '</ul>') );
  }

  public function testArrayToOrderedList()
  {
    $result = HtmlHelper::arrayToOrderedList( self::$data );
  
    $this->assertEquals( self::$itemsNumber, substr_count($result, '<li>') );
    $this->assertEquals( self::$itemsNumber, substr_count($result, '</li>') );
    $this->assertEquals( 1, substr_count($result, '<ol>') );
    $this->assertEquals( 1, substr_count($result, '</ol>') );
  }
}

Como os helpers HTML sempre retornam uma string, não precisamos verificar nesse caso e podemos limitar o nosso teste para a funcionalidade principal.

php-3

Para dar um exemplo mais concreto, eu recuperei uma classe a partir de um repositório meu no GitHub e tentei convertê-lo para usar digitação estrita para demonstrar seus benefícios.

A pequena classe calcula a distância entre dois pontos da terra.

Minha classe Point antes de converter para strict typing:

<?php

namespace JLaso\Gps;

/**
 * Class Point
 * @package JLaso\Gps
 * @author Joseluis Laso <jlaso@joseluislaso.es>
 */

class Point
{
 /** @var float */
 protected $longitude;
 /** @var float */
 protected $latitude;

 /**
  * @param float $latitude
  * @param float $longitude
  */
 function __construct($latitude, $longitude)
 {
  $this->latitude  = $latitude;
  $this->longitude = $longitude;
 }

 /**
  * @param float $latitude
  */
 public function setLatitude($latitude)
 {
  $this->latitude = $latitude;
 }

 /**
  * @return float
  */
 public function getLatitude()
 {
  return $this->latitude;
 }

 /**
  * @param float $longitude
  */
 public function setLongitude($longitude)
 {
  $this->longitude = $longitude;
 }

 /**
  * @return float
  */
 public function getLongitude()
 {
  return $this->longitude;
 }

 /**
  * @param Point $point
  * @return float
  */
 public function distanceTo(Point $point)
 {
  return Tools::distance($this->getLatitude(), $this->getLongitude(), $point->getLatitude(), $point->getLongitude());
 }
}

Aplicando strict typing:

<?php
declare(strict_types=1);

namespace JLaso\Gps;

/**
 * Class Point
 * @package JLaso\Gps
 * @author Joseluis Laso <jlaso@joseluislaso.es>
 */

class Point
{
 /** @var float */
 protected $longitude;
 /** @var float */
 protected $latitude;

 function __construct(float $latitude, float $longitude)
 {
  $this->latitude  = $latitude;
  $this->longitude = $longitude;
 }

 public function setLatitude(float $latitude)
 {
  $this->latitude = $latitude;
 }

 public function getLatitude(): float
 {
  return $this->latitude;
 }

 public function setLongitude(float $longitude)
 {
  $this->longitude = $longitude;
 }

 public function getLongitude(): float
 {
  return $this->longitude;
 }

 public function distanceTo(Point $point): float
 {
  return Tools::distance($this->getLatitude(), $this->getLongitude(), $point->getLatitude(), $point->getLongitude());
 }
}

Eu criei uma nova tag (1.1) para ilustrar as melhorias.

Para concentrar a nossa atenção no assunto principal, aqui estão as diferenças na classe Point:

php-4

Isso foi fácil. Acabei de remover os comentários PHPDoc e adicionei type hints para parâmetros e valores de retorno.

A mesma coisa aconteceu para as classes principais chamadas Tools.

O código original:

<?php

namespace JLaso\Gps;

/**
 * Class Tools
 * @package JLaso\Gps
 * @author Joseluis Laso <jlaso@joseluislaso.es>
 */

class Tools
{

 /**
  * @param $latitude1
  * @param $longitude1
  * @param $latitude2
  * @param $longitude2
  * @return float distance between coordinates in kilometers
  * @throws \Exception
  */
 public static function distance($latitude1, $longitude1, $latitude2, $longitude2)
 {
  if(!is_numeric($latitude1) || !is_numeric($longitude1) || !is_numeric($latitude2) || !is_numeric($longitude2)){
     throw new \Exception( "distance can not be calculated with non numerical values!" );
  }
  // normalize values
  $latitude1  = floatval($latitude1);
  $longitude1 = floatval($longitude1);
  $latitude2  = floatval($latitude2);
  $longitude2 = floatval($longitude2);

  $dLatitude  = ($latitude2 - $latitude1) / 2;
  $dLongitude = ($longitude2 - $longitude1) / 2;
  $tmp        = sin(deg2rad($dLatitude)) * sin(deg2rad($dLatitude)) +
                 cos(deg2rad($latitude1)) * cos(deg2rad($latitude2)) * sin(deg2rad($dLongitude)) * sin(deg2rad($dLongitude));
  $aux        = asin(min(1, sqrt($tmp)));

  return round(12745.9728 * $aux, 4);
 }

 /**
  * @param $value
  * @return float
  */
 public static function toMiles($value)
 {
  return 0.621 * $value;
 }
}

O código convertido:

<?php
declare(strict_types=1);

namespace JLaso\Gps;

/**
 * Class Tools
 * @package JLaso\Gps
 * @author Joseluis Laso <jlaso@joseluislaso.es>
 */

class Tools
{
 public static function distance(float $latitude1, float $longitude1, float $latitude2, float $longitude2): float
 {
  $dLatitude  = ($latitude2 - $latitude1) / 2;
  $dLongitude = ($longitude2 - $longitude1) / 2;
  $tmp        = sin(deg2rad($dLatitude)) * sin(deg2rad($dLatitude)) +
               cos(deg2rad($latitude1)) * cos(deg2rad($latitude2)) * sin(deg2rad($dLongitude)) * sin(deg2rad($dLongitude));
  $aux        = asin(min(1, sqrt($tmp)));

  return round(12745.9728 * $aux, 4);
 }

 public static function toMiles(float $value): float
 {
  return 0.621 * $value;
 }
}

E as diferenças:

php-5

Neste último caso, as alterações são mais importantes, pois não precisamos mais converter os parâmetros para float.

E, finalmente, para os testes PHPUnit, só adicionamos o declare (strict_types = 1); no início de cada teste. Verificar o caso especial de recepção diferente do flutuador não é mais necessário.

A suíte de testes original:

<?php

use JLaso\Gps\Tools;


class ConversionTest extends PHPUnit_Framework_TestCase
{
 function testKilometersMilesConversion()
 {
  $this->assertEquals(0.621, Tools::toMiles(1));
 }
 /**
  * source: www.distace.to/Origin/Destination
  *
  * Paris/Madrid  1.052,69 km   #1 Paris (48.856667,2.350987)  #2 Madrid (40.416691,-3.700345)
  */
 function testParisMadridDistance()
 {
  $distance = Tools::distance(48.856667, 2.350987, 40.416691, -3.700345);
  // Let assume the result it's okay if the error of calculated distance is less than 1/1000  (1km)
  $this->assertLessThan(1.052, abs(1052.69 - $distance));

  // now use the indirect method to calculate distance in the same conditions
  $madrid = new Point( 40.416691, -3.700345 );
  $paris = new Point( 48.856667, 2.350987 );

  $this->assertLessThan(1.052, abs(1052.69 - $madrid->distanceTo($paris) ));
 }

 /**
  * @expectedException \Exception
  */
 function testException()
 {
  $distance = Tools::distance('a', 'b', 'c', 'd');
 }
}

O convertido:

<?php
declare(strict_types=1);

use JLaso\Gps\Tools;


class ConversionTest extends PHPUnit_Framework_TestCase
{
 function testKilometersMilesConversion()
 {
  $this->assertEquals(0.621, Tools::toMiles(1));
 }
 /**
  * source: www.distace.to/Origin/Destination
  *
  * Paris/Madrid  1.052,69 km   #1 Paris (48.856667,2.350987)  #2 Madrid (40.416691,-3.700345)
  */
 function testParisMadridDistance()
 {
  $distance = Tools::distance(48.856667, 2.350987, 40.416691, -3.700345);
  // Let assume the result it's okay if the error of calculated distance is less than 1/1000  (1km)
  $this->assertLessThan(1.052, abs(1052.69 - $distance));

  // now use the indirect method to calculate distance in the same conditions
  $madrid = new Point( 40.416691, -3.700345 );
  $paris = new Point( 48.856667, 2.350987 );

  $this->assertLessThan(1.052, abs(1052.69 - $madrid->distanceTo($paris)));
 }

}

Conclusão

Strict type hinting é, definitivamente, um grande avanço para o PHP que nos permitirá escrever um código mais robusto com menos esforço.

Se quiser experimentar meus exemplos, você pode encontrar o código em meu repositório php7-strict-types-testing.

O que você acha? Você acha o strict type do PHP 7 também vai ajudar você a escrever um código mais robusto? Quais as outras vantagens (ou desvantagens) que você vê? Basta postar um comentário com seus pensamentos.

***

Joseluiz Laso faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://www.phpclasses.org/blog/post/271-How-PHP-7-Can-Help-You-Write-Better-Tests.html