Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

rohan0793's avatar

How to unit test abstract classes the right way?

So I am trying to write unit tests for my repository package. It is a small package which takes out the functionality of the repositories and I have a BaseRepository.php abstract class which basically performs all the eloquent related stuff. All the eloquent repositories extend this class. This is what it looks like:

<?php

namespace Uppdragshuset\AO\Repository\Eloquent;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;

use Uppdragshuset\AO\Repository\Contracts\Repository;
use Uppdragshuset\AO\Repository\Contracts\Criteria;

abstract class BaseRepository implements Repository, Criteria
{
    use AuthorizesRequests;

    protected $model;
    protected $criteria;
    protected $skipCriteria;
    protected $fieldSearchable;

    public function __construct() {
        $this->criteria = new Collection;
        $this->makeModel();
        $this->boot();
    }

    public function all($columns = array('*')) {
        $this->applyCriteria();
        if(!$this->model instanceof \Illuminate\Database\Eloquent\Builder){
            $this->authorize('index', $this->model);
            $results = $this->model->all($columns);
        }
        else{
            $results = $this->model->get($columns);
        }
        return $results;
    }

    public function lists($value, $key = null) {
        $this->authorize('index', $this->model);
        $lists = $this->model->lists($value, $key);
        if(is_array($lists)) {
            return $lists;
        }
        return $lists->all();
    }

    public function paginate($perPage = 10, $columns = array('*')) {
        $this->applyCriteria();
        if(!$this->model instanceof \Illuminate\Database\Eloquent\Builder){
            $this->authorize('index', $this->model);
        }
        return $this->model->paginate($perPage, $columns);
    }

    public function create(array $data) {
        $this->authorize('store', $this->model);
        return $this->model->create($data);
    }

    public function update(array $data, $id, $attribute="id") {
        $this->authorize('update', $this->model->find($id));
        $this->model->find($id)->update($data);
        return $this->model->find($id);
    }

    public function delete($id) {
        $this->authorize('destroy', $this->find($id));
        return $this->model->destroy($id);
    }

    public function find($id, $columns = array('*')) {
        $this->applyCriteria();
        if(!$this->model instanceof \Illuminate\Database\Eloquent\Builder){
            $this->authorize('index', $this->model);
        }
        return $this->model->find($id, $columns);
    }

    public function findBy($attribute, $value, $columns = array('*')) {
        $this->applyCriteria();
        if(!$this->model instanceof \Illuminate\Database\Eloquent\Builder){
            $this->authorize('index', $this->model);
        }
        return $this->model->where($attribute, '=', $value)->first($columns);
    }

    public function findAllBy($attribute, $value, $columns = array('*')) {
        $this->applyCriteria();
        if(!$this->model instanceof \Illuminate\Database\Eloquent\Builder){
            $this->authorize('index', $this->model);
        }
        return $this->model->where($attribute, '=', $value)->get($columns);
    }

    public function findWhere($where, $columns = ['*'], $or = false)
    {
        $this->applyCriteria();
        if(!$this->model instanceof \Illuminate\Database\Eloquent\Builder){
            $this->authorize('index', $this->model);
        }
        $model = $this->model;
        foreach ($where as $field => $value) {
            if ($value instanceof \Closure) {
                $model = (! $or)
                    ? $model->where($value)
                    : $model->orWhere($value);
            } elseif (is_array($value)) {
                if (count($value) === 3) {
                    list($field, $operator, $search) = $value;
                    $model = (! $or)
                        ? $model->where($field, $operator, $search)
                        : $model->orWhere($field, $operator, $search);
                } elseif (count($value) === 2) {
                    list($field, $search) = $value;
                    $model = (! $or)
                        ? $model->where($field, '=', $search)
                        : $model->orWhere($field, '=', $search);
                }
            } else {
                $model = (! $or)
                    ? $model->where($field, '=', $value)
                    : $model->orWhere($field, '=', $value);
            }
        }
        return $model->get($columns);
    }

    public function with($relations)
    {
        if(!$this->model instanceof \Illuminate\Database\Eloquent\Builder){
            $this->authorize('index', $this->model);
        }
        $this->model = $this->model->with($relations);
        return $this;
    }

    public abstract function model();

    public function makeModel() {
        $model = app()->make($this->model());
        if (!$model instanceof Model)
            throw new RepositoryException("Class {$this->model()} must be an instance of Illuminate\\Database\\Eloquent\\Model");
        return $this->model = $model;
    }

    public function boot()
    {
        //
    }

    public function skipCriteria($status = true){
        $this->skipCriteria = $status;
        return $this;
    }

    public function getCriteria() {
        return $this->criteria;
    }

    public function getByCriteria(BaseCriteria $criteria) {
        $this->model = $criteria->apply($this->model, $this);
        return $this;
    }

    public function pushCriteria(BaseCriteria $criteria)
    {
        $this->criteria->push($criteria);
        return $this;
    }

    public function applyCriteria() {
        if($this->skipCriteria === true)
            return $this;
        foreach($this->getCriteria() as $criteria) {
            if($criteria instanceof BaseCriteria)
                $this->model = $criteria->apply($this->model, $this);
        }
        return $this;
    }

    public function getFieldsSearchable()
    {
        return $this->fieldSearchable;
    }
}

So I am new to testing and I am certainly going haywire with all these new concepts about different types of testing, and then stubs and mocks and there are so many frameworks. So currently I am taking one thing at a time and using phpunit to write some simple unit tests, some functional tests and so on. And I am discussing whatever problems that are arising along to get a better understanding. So my BaseRepository test looks like so:

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

use Illuminate\Support\Collection;
use Uppdragshuset\AO\Repository\Eloquent\BaseRepository;

class BaseRepositoryTest extends TestCase
{
    public function test_all_method()
    {
        $stub = $this->getMockBuilder(BaseRepository::class)
                     ->getMock();

        $collection = new Collection;

        $stub->method('all')
             ->willReturn($collection);

        $this->assertEquals($collection, $stub->all());
    }
}

Works fine and passes and I also understand what is going there. So I am creating a stub of the abstract class and telling phpunit what will it return if I call a certain method and then asserting the same. All this is cool but how is this testing. I mean what is the purpose of this. I know the asset will pass.

Shouldn't I be testing things like the all method should call applyCriteria and authorize methods. And if yes, how to do so. And is this the right way of doing this? Or should I create a stub of another class which extends the base repository? This is pretty confusing. Any thoughts are welcome.

0 likes
1 reply
Darival's avatar

hi, i'm currently doing something similar, when i want to test an abstract class i make use of a new class that extends from the abstract class, it would be something like this:

<?php

use App\Http\Controllers\ApiControllerTester;

class ApiControllerTest extends TestCase
{
    /** @test */
    public function api_get_status_code_method() {
        $api = new ApiControllerTester;
        $response = $api->getStatusCode();
        $this->assertEquals(200, $response);
    }
}

where ApiControllerTester is a new class that extends from the abstract Class ApiController just for testing purposes.

1 like

Please or to participate in this conversation.