Testing private methods. How? Should we?

Testing public methods is straight forward, since they are accessible from anywhere. So they will be, in our test cases too.

But what about when it comes to private methods? This is a common question.

First of all, I will show how we can test them, and secondly if we should.

As always, let's start with some dummy code as our example:

<?php

namespace App\Services\Dummy;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class UserService
{
    public function createUser(string $email, string $name): User
    {
        $user = new User();
        $user->email = $email;
        $user->name = $name;
        $user->password = Hash::make(Str::random());
        $user->save();

        $user->secondary_id = $this->generateSecondaryId($user);
        $user->save();

        return $user;
    }

    private function generateSecondaryId(User $user): string
    {
        return $user->id . '@' . Str::random(10);
    }
}

And one small test for our createUser method which is public:

<?php

namespace Tests\Feature;

use App\Services\Dummy\UserService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class UserServiceTest extends TestCase
{
    use DatabaseTransactions;

    #[Test]
    public function createUser(): void
    {
        $user = app(UserService::class)
            ->createUser('randomuser@example.com', 'Random User');

        $this->assertEquals('randomuser@example.com', $user->email);

        // more assertions
    }
}

Everything works fine.

We have one more method, generateSecondaryId which is private.

We decided to make it private because it is only used inside our UserService class, so we increase our code security and do not expose useless methods on our class public API.

But because the method is private, this is not going to work:

#[Test]
public function generateSecondaryId(): void
{
    $user = new User;
    $user->id = '100';

    $secondaryId = app(UserService::class)
        ->generateSecondaryId($user);

    $this->assertStringStartsWith('100@', $secondaryId);
}

Since the method is private, we get an error.

Error: Call to private method App\Services\Dummy\UserService::generateSecondaryId()

But with PHP, we can easily "hack" it, to make the method publicly accessible on runtime. We can achieve that using PHP's Reflection API, which allow us to reverse engineer our code and change how it behaves, on runtime.

We can easily make our method accessible like that:

/**
 * @throws \ReflectionException
 */
#[Test]
public function generateSecondaryId(): void
{
    $user = new User;
    $user->id = '100';

    $userService = app(UserService::class);
    $userServiceReflection = new \ReflectionObject($userService);
    $method = $userServiceReflection->getMethod('generateSecondaryId');

    $this->assertStringStartsWith('100@', $method->invokeArgs($userService, [$user]));
}

This will run & pass.

But should we really do it? Should we care about testing private methods?

In my opinion, no. Looks like code smell. Not because it seems kind of hacky to test private methods, but because on tests, we should only care about the public API of a class. If we want to ensure that the private method works, we can do that by testing the public methods that consume private ones. For example:

#[Test]
public function createUser(): void
{
    $user = app(UserService::class)
        ->createUser('randomuser@example.com', 'Random User');

    $this->assertEquals('randomuser@example.com', $user->email);

    // This will cover our private method
    $this->assertStringStartsWith("$user->id@", $user->secondary_id);
}

If you find your self to really want to test private methods, then it's probably time to refactor your code, maybe your class does too much. But just know that if you really want to do so, you still can, using Reflection API.

Subscribe to Lioy

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe