大家好!
我的名字叫Anton,现在(不久,大约一年),我正在一个大型项目中用PHP开发。为了确保项目的质量,我们在PHPUnit框架上使用了自动测试。但是,不幸的是,我们的大多数自动测试都可以正常运行。最近,我们意识到,如果我们继续以相同的方式使用自动测试,那么解决未传递给CI的问题所花费的时间很快就会超过编写有用代码所花费的时间(实际上不是,但是它会增长,而且不会很好)。
为了系统地解决此问题,我们选择了一种方法,其中涉及编写许多单元测试和最少数量的功能测试。我们还决定,作为根据侦察员规则对现有代码进行必要更改的一部分,请更新现有测试。一段时间以来-我们一直看到新功能-一切都很好。但是最近,一个任务飞向我,以修复旧代码中的错误。我开始编写单元测试,并意识到古老的邪恶遗产已经醒来。很明显,这是通往无处可去的道路:
- 我写了几个小时的测试用例。
- 我意识到这段时间我写了很多重复的代码。
- 实际上没有做任何有用的工作;
- 如果DI发生变化,您将不得不花费大量时间进行无用的测试代码修改。
本文是关于我如何解决拒绝编写无价值代码的问题。削减-测试用例的最终形式,大多数机械规范除外
本文的标题指的是看板风格的自动化-一方面,我们执行了自动化,另一方面,所有关键决策都是由一个人做出的。
如何做
<?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();
}
}
因为它是
不好 从产生依赖关系的暴民开始就产生了无尽的意大利面,在测试用例中冗长地调用了构造函数。总的来说,这很丑陋,并且在文章底部的某个地方给出了这种测试的明显示例。
我们的情况有点痛苦
不幸的是,我们的项目使用PHPUnit3.x。是的,我知道它甚至在2014年之前就不再受支持(!),但是,不幸的是,我们不能在当地时间范围内拒绝它。
解决的问题
, — , , . , — (, , 20) -, , , unit- , .
, , « » :
- setUp.
- tearDown. , ,
- -.
考虑重复代码片段。
模拟生成
重复的代码段:$this->service = $this
->getMockBuilder(Service::class)
->disableOriginalConstructor()
->getMock()
;
PHP为隐藏单调的工作提供了一个很好的选择-一个神奇的功能:
__得到
但是我们仍然需要传达我们正在润湿的类型。然后找到了合适的工具-phpDoc:
@属性类型$服务
在我们的案例中,特别是考虑到现代IDE的功能,事实证明将其设置为以下格式最为方便:@property \ FQCN \ Service | \ PHPUnit_Framework_MockObject_MockObject $服务
在抽象类Intellegence_PHPUnit_Framework_TestCase的__get函数内部,发生元编程,该元编程将解析phpDoc并在其基础上形成模拟,这些模拟可在测试类中以$ this-> propertyName的形式获得。注意,本文中给出的所有代码都是一些伪代码,完整版本位于存储库中:
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. , .