¡Hola a todos!
Mi nombre es Anton y ahora (no hace tanto tiempo, aproximadamente un año) estoy desarrollando en PHP en un proyecto grande y antiguo. Para garantizar la calidad del proyecto, utilizamos pruebas automáticas en el marco PHPUnit. Pero, desafortunadamente, sucedió que la mayoría de nuestras pruebas automáticas son funcionales. Recientemente, nos dimos cuenta de que si continuamos usando las pruebas automáticas de la misma manera, pronto el tiempo dedicado a resolver problemas al no pasarlos a CI será más que el tiempo dedicado a escribir código útil (en realidad no, pero crece y no es bien).
Para una solución sistemática a este problema, elegimos un enfoque que implica escribir muchas pruebas unitarias y un número mínimo de pruebas funcionales. También decidimos que, como parte de los cambios necesarios en el código existente de acuerdo con la regla de exploración, actualizar las pruebas existentes. Por un tiempo, todo este tiempo vimos nuevas características, todo estuvo bien con nosotros. Pero recientemente, una tarea voló hacia mí para corregir un error en el código anterior. Comencé a escribir pruebas unitarias y me di cuenta de que el antiguo legado del mal despertó. Quedó claro que este es el camino a ninguna parte:
- Escribí un par de casos de prueba durante varias horas;
- Me di cuenta de que durante este tiempo escribí un código muy, muy repetido;
- en realidad no hizo ningún trabajo útil;
- En el caso de un cambio en la DI, tendrá que dedicar mucho tiempo a la adaptación inútil del código de prueba.
, . — ,
— , — .
<?php declare(strict_types=1);
namespace Company\Bundle\Tests\Unit\Service;
use PHPUnit_Framework_MockObject_MockObject;
class TestedServiceTest extends Intelligent_PHPUnit_Framework_TestCase
{
public function test_getData_DataDescripion_ResultDescription(): void
{
$this
->diConstParam
->set("value")
;
$this
->dependencyRepository
->expects($this->once())
->method('findData')
->with(10, 10)
->will($this->returnValue([]))
;
$clazz = $this->getTestedClass();
$res = $clazz->getData();
}
}
. , -. , , - .
, PHPUnit 3.x. , 2014(!) , , , .
, — , , . , — (, , 20) -, , , unit- , .
, , « » :
- setUp.
- tearDown. , ,
- -.
Considere repetir fragmentos de código.
Simulacro de generación
Fragmento de código duplicado:$this->service = $this
->getMockBuilder(Service::class)
->disableOriginalConstructor()
->getMock()
;
PHP tiene un buen candidato para ocultar monótono este trabajo monótono, una función mágica:
__obtener
Pero aún necesitamos transmitir el tipo que estamos mojando. Y luego se encontró una herramienta adecuada: phpDoc:
@property Type $ service
En nuestro caso, especialmente teniendo en cuenta las capacidades de los IDEs modernos, resultó más conveniente configurarlo en el siguiente formato:@property \ FQCN \ Service | \ PHPUnit_Framework_MockObject_MockObject $ service
Dentro de la función __get de la clase abstracta Intellegence_PHPUnit_Framework_TestCase, se produce la metaprogramación, que analiza phpDoc y forma simulacros sobre la base, que están disponibles como $ this-> propertyName en la clase de prueba. Atención, todo el código dado en el artículo es un pequeño pseudocódigo, la versión completa está en el repositorio:
abstract class Intelligent_PHPUnit_Framework_TestCase extends PHPUnit_Framework_TestCase {
public function __get(string $name)
{
$property = $this->getPropertyBag()->get($name);
if ($property === false) {
throw new Exception("Invalid property name: ${name}");
}
return $property;
}
private function getPropertyBag(): PropertyBag
{
if ($this->propertyBag === null) {
$this->propertyBag = (new DocCommentParser())
->parse(
get_class($this),
function (string $className): PHPUnit_Framework_MockObject_MockObject {
return $this->createDefaultObjectMock($className);
}
);
}
return $this->propertyBag;
}
}
, phpDoc . :
class DocCommentParser
{
public function parse(
string $className,
callable $mockCreator
): PropertyBag {
$r = new ReflectionClass($className);
$docComment = $r->getDocComment();
if ($docComment === false) {
return new PropertyBag([]);
}
return $this->parseDocString($docComment, $mockCreator);
}
private function parseDocString(
string $docComment,
callable $mockCreator
): PropertyBag {
$map = [];
$constructorParams = [];
preg_match_all('/@property\s*(?<mock>(\S*)\|(\S+)\s*\$(\S+))/', $docComment, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
if (array_key_exists('mock', $match) && !empty($match['mock'])) {
if ($this->isMockObject($match[2])) {
$map[$match[4]] = $mockCreator($match[3]);
continue;
}
if ($this->isMockObject($match[3])) {
$map[$match[4]] = $mockCreator($match[2]);
continue;
}
}
}
return new PropertyBag($map);
}
}
? , — FQCN phpDoc property . — , .
-class ServiceTest extends PHPUnit_Framework_TestCase
{
private $repository;
protected function setUp()
{
parent::setUp();
$this
->repository = $this->getMockBuilder(DependencyRepository::class)
->disableOriginalConstructor()
->getMock()
;
}
protected function tearDown()
{
$this->repository = null;
parent::tearDown();
}
public function test_test()
{
}
}
class ServiceTest extends Intelligent_PHPUnit_Framework_TestCase
{
public function test_test()
{
}
}
3-4-5 ( ) — . , , , 7 — . , 20 — -.
. ! -, , .
:
public function test_case()
{
$this->clazz = $this->createTestClass(
$dependency1,
$dependency2,
$dependency3
);
}
, . php __call, phpDoc method.
, , ?! , property! , . , property — . — , .
, :
class ServiceTest
{
public function test_case()
{
$clazz = $this->getTestedClass();
}
}
__call:
abstract class Intelligent_PHPUnit_Framework_TestCase extends PHPUnit_Framework_TestCase
{
private const GET_TESTED_CLASS_METHOD_NAME = 'getTestedClass';
public function __call(string $method, array $params)
{
if ($method !== self::GET_TESTED_CLASS_METHOD_NAME) {
throw new Exception("Invalid method name: ${method}");
}
if (!array_key_exists(self::GET_TESTED_CLASS_METHOD_NAME, $this->getMethodsMap())) {
throw new Exception('Method ' . self::GET_TESTED_CLASS_METHOD_NAME . ' not annotated');
}
$params = $this->getPropertyBag()->getConstructorParams();
$paramValues = [];
foreach ($params as $param) {
$property = $this->__get($param);
$paramValues[] = $property;
}
$reflection = new ReflectionClass($this->getMethodsMap()[self::GET_TESTED_CLASS_METHOD_NAME]);
return $reflection->newInstanceArgs($paramValues);
}
}
? property:
class DocCommentParser
{
public function parse(
string $className,
callable $mockCreator
): PropertyBag {
$r = new ReflectionClass($className);
$docComment = $r->getDocComment();
if ($docComment === false) {
return new PropertyBag([], []);
}
return $this->parseDocString($docComment, $mockCreator);
}
private function parseDocString(
string $docComment,
callable $mockCreator
): PropertyBag {
$map = [];
$constructorParams = [];
preg_match_all('/@property\s*(?<mock>(\S*)\|(\S+)\s*\$(\S+))/', $docComment, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
if (array_key_exists('mock', $match) && !empty($match['mock'])) {
if ($this->isMockObject($match[2])) {
$map[$match[4]] = $mockCreator($match[3]);
$constructorParams[] = $match[4];
continue;
}
if ($this->isMockObject($match[3])) {
$map[$match[4]] = $mockCreator($match[2]);
$constructorParams[] = $match[4];
continue;
}
}
}
return new PropertyBag($map, $constructorParams);
}
}
. , . , DI - , , , — ,
-class ServiceTest extends PHPUnit_Framework_TestCase
{
private $repository;
protected function setUp()
{
parent::setUp();
$this
->repository = $this->getMockBuilder(DependencyRepository::class)
->disableOriginalConstructor()
->getMock()
;
}
protected function tearDown()
{
$this->repository = null;
parent::tearDown();
}
public function test_test()
{
$this->clazz = $this->createTestClass(repository);
}
private function createTestClass() {
return new TestedService($repository)
}
}
— . .
ConstDependencyInjectionParameter. , .