تحية للجميع!
اسمي أنطون والآن (منذ وقت ليس ببعيد ، أقوم بالتطوير في PHP في مشروع واحد كبير وقديم. لضمان جودة المشروع ، نستخدم الاختبارات التلقائية في إطار عمل PHPUnit. ولكن ، لسوء الحظ ، حدث أن معظم الاختبارات الذاتية لدينا تعمل. لقد أدركنا مؤخرًا أنه إذا واصلنا استخدام الاختبارات الذاتية بالطريقة نفسها ، فإن الوقت الذي يقضيه في حل المشكلات مع عدم تمريرها إلى CI سيصبح قريبًا أكثر من الوقت المستغرق في كتابة التعليمات البرمجية المفيدة (في الواقع لا ، لكنه ينمو ولا بشكل جميل).
من أجل حل منهجي لهذه المشكلة ، اخترنا نهجًا ينطوي على كتابة الكثير من اختبارات الوحدة والحد الأدنى من الاختبارات الوظيفية. قررنا أيضًا أنه كجزء من التغييرات على الشفرة الحالية اللازمة وفقًا لقاعدة الكشفية ، قم بتحديث الاختبارات الحالية. لفترة من الوقت - كل هذا الوقت رأينا ميزات جديدة - كل شيء كان على ما يرام معنا. ولكن في الآونة الأخيرة ، توجهت إلي مهمة لإصلاح خطأ في التعليمات البرمجية القديمة. بدأت كتابة اختبارات الوحدة وأدركت أن إرث الشر القديم استيقظ. أصبح من الواضح أن هذا هو الطريق إلى العدم:
- كتبت بضع حالات اختبار لعدة ساعات.
- أدركت أنه خلال هذا الوقت كتبت رمزًا متكررًا جدًا ؛
- في الواقع لم يقم بأي عمل مفيد ؛
- في حالة حدوث تغيير في DI ، سيكون عليك قضاء الكثير من الوقت على التكيف غير المجدي لرمز الاختبار.
تتناول هذه المقالة كيفية حل مشكلة رفض كتابة رمز لا قيمة له. تحت القطع - الشكل النهائي لحالة الاختبار ، باستثناء معظم الشفرة الميكانيكية
يشير عنوان المقالة إلى الأتمتة في أسلوب kanban - من ناحية ، قمنا بأتمتة ، ومن ناحية أخرى ، يتم اتخاذ جميع القرارات الرئيسية من قبل شخص.
كيف فعل
<?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. , ,
- -.
ضع في اعتبارك تكرار أجزاء التعليمات البرمجية.
الجيل الوهمي
مقتطف رمز مكرر:$this->service = $this
->getMockBuilder(Service::class)
->disableOriginalConstructor()
->getMock()
;
PHP لديه مرشح جيد لإخفاء هذا العمل الرتيب - وظيفة سحرية:
__احصل على
لكننا ما زلنا بحاجة إلى نقل النوع الذي نراهن عليه. ثم تم العثور على أداة مناسبة - phpDoc:
property اكتب $ service
في حالتنا ، خاصة مع مراعاة قدرات IDE الحديثة ، اتضح أنه الأكثر ملاءمة لتعيينها بالتنسيق التالي:property \ FQCN \ Service | \ PHPUnit_Framework_MockObject_MockObject $ service $
داخل وظيفة __get للفئة المجردة Intellegence_PHPUnit_Framework_TestCase ، تحدث عملية البرمجة ، التي تحلل 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. , .