Как создать тесты PHPUnit для объектно-ориентированного программирования?

Как можно написать тесты для проверки результатов методов, которые имеют ограниченный доступ (private и protected) в классах PHP, не изменяя их доступа? Какие правила вы используете для создания классов и написания тестов, чтобы обеспечить максимальное покрытие кода?
  • 30 января 2024 г. 20:58
Ответы на вопрос 2
PHPUnit - это фреймворк для модульного тестирования приложений на PHP. В объектно-ориентированном программировании классы и методы инкапсулируют состояние и поведение. Чтобы протестировать классы, написанные в объектно-ориентированном стиле, вам следует создать тестовые случаи, которые проверяют публичные интерфейсы классов. Тесты должны проверять корректность работы публичных методов, а также их взаимодействие с зависимостями и изменение внутреннего состояния объектов.

Чтобы проверить методы с ограниченным доступом (private и protected) без изменения их модификаторов доступа, существует несколько подходов:

1. **Тестирование через публичные методы**: Лучший способ - это структурировать код таким образом, чтобы всё поведение класса можно было протестировать через публичные методы. Основной принцип здесь - тестировать поведение, а не реализацию.

2. **Рефлексия**: PHP предоставляет возможность использовать Reflection API для доступа к методам и свойствам с ограниченным доступом. Это может быть полезно для тестирования, но следует использовать этот способ аккуратно, так как он может нарушить инкапсуляцию и принципы проектирования классов.

```php
public function testPrivateMethod()
{
    $object = new ClassWithPrivateMethod();
    $reflector = new ReflectionClass($object);
    $method = $reflector->getMethod('privateMethod');
    $method->setAccessible(true);
    $result = $method->invokeArgs($object, [$arg1, $arg2]);

    $this->assertEquals($expected, $result);
}
```

3. **Внедрение тестовых подклассов (Test Subclasses)**: Можно создать подкласс тестируемого класса в тестовом пространстве имен и расширить доступность защищенных методов, изменяя их на публичные. Затем можно протестировать эти методы напрямую. Это также нарушает инкапсуляцию и может привести к ложной уверенности в корректности тестов, так как поведение подкласса может отличаться от поведения базового класса.

```php
class TestableClass extends ClassWithProtectedMethod
{
    public function protectedMethod()
    {
        return parent::protectedMethod();
    }
}

// В тесте:
public function testProtectedMethod()
{
    $object = new TestableClass();
    $result = $object->protectedMethod();

    $this->assertEquals($expected, $result);
}
```

На практике, следует избегать тестирования приватных и защищенных методов напрямую, поскольку они часть внутренней реализации и могут быть изменены или удалены без влияния на публичный интерфейс класса. Тестирование должно быть направлено на проверку внешнего поведения объектов.

Для создания классов и написания тестов, которые обеспечивают максимальное покрытие кода, рекомендуется следовать нескольким принципам:

- **Одиночная ответственность**: Классы должны быть максимально простыми и отвечать только за одну задачу.
- **Высокий уровень когезии**: Методы в классе должны быть тесно связаны с данными класса.
- **Низкая связанность**: Классы долж
Вам не надо тестировать приватные методы. 
Но как же тогда быть, вы скажете?
Публичные методы используют эти приватные методы, так и проходит тестирование.
Из этого вытекает, что класс должен быть небольшим и выполнять только одну задачу. Поэтому его приватные методы будут всегда задействованы при тестировании публичных.
Но вы скажете, что у вас там в публичных методах обращения к базе данных и внешним API, как же тут быть?
А быть тут просто: все зависимости вашего класса должны включаться в качестве аргументов конструктора, и через использование интерфейсов в аргументах. (Dependency Injection)
Таким образом в тесте все зависимости класса заменяются теми же моками, и вы легко тестируете не просто сферического корня в вакууме (приватный метод), а именно то, что и нужно тестировать: функциональность самого класса, а не его частей, потому что в OOP именно класс является как бы атомом функционала.
Но... если вам всё же таки очень надо, то можно это сделать при помощи рефлексии:
Хорошей практикой является использовать всегда protected вместо private.
Пишете себе такую вот утилиту:

И используете ее в тестах

Есть ещё вариант.
Похожие вопросы