Refactoring 037 - Testing Private Methods

Written by mcsee | Published 2025/12/09
Tech Story Tags: software-development | clean-code | ai | common-code-smells | refactor-legacy-code | refactoring | testing-private-methods | code-smells

TLDRYou can and should test private methodsvia the TL;DR App

Turn hidden private logic into a real concept without using AI.

TL;DR: You can and should test private methods

Problems Addressed ๐Ÿ˜”

  • Broken encapsulation
  • Hidden rules
  • White-box Testing Dependencies
  • Hard testing
  • Mixed concerns
  • Low reuse
  • Code Duplication in Tests
  • Missingย Small objects

https://maximilianocontieri.com/code-smell-112-testing-private-methods

https://maximilianocontieri.com/code-smell-22-helpers

https://maximilianocontieri.com/code-smell-18-static-functions

https://maximilianocontieri.com/code-smell-21-anonymous-functions-abusers

https://www.linkedin.com/pulse/code-smell-177-missing-small-objects-maximiliano-contieri

Context ๐Ÿ’ฌ

I was pair programming with an AI Agent and asked it to create some unit tests for a private method I was about to modify,ย TDD Way.


The proposed solution usedย metaprogramming,ย which is a mistake almost every time.


You need to be in control and notย trust AI blindly.

Steps ๐Ÿ‘ฃ

  1. Identify a private method that needs testing.
  2. Name the real responsibility behind that logic.
  3. Extract the logic into a new class.
  4. Pass the needing objects explicitly through method arguments.
  5. Replace the private call with the new object.


This is a special case for theย Extract Method refactoring

Sample Code ๐Ÿ’ป

Before ๐Ÿšจ

<?php

final class McpMessageParser {
    private $raw;

    public function parse() {
        return $this->stripStrangeCharacters($this->raw);
    }

    // This is the private method me need to test 
    // For several different scenarios
    // Simplified here
    private function stripStrangeCharacters($input) {
        return preg_replace('/[^a-zA-Z0-9_:-]/', '', $input);
    }
}

Intermediate solution by AI

This is a wrong approach usingย Metaprogramming.

<?php

use PHPUnit\Framework\TestCase;

final class McpMessageParserTest extends TestCase {
    private function invokePrivateMethod(
        $object, 
        $methodName, 
        array $parameters = []
    ) {
        $reflection = new ReflectionClass(get_class($object));
        // This is metaprogramming.
        // That generates fragile and hidden dependencies
        // You need to avoid it
        $method = $reflection->getMethod($methodName);
        $method->setAccessible(true);
        return $method->invokeArgs($object, $parameters);
    }

    public function testStripStrangeCharactersRemovesSpecialChars() {
        $parser = new McpMessageParser();
        $result = $this->invokePrivateMethod(
            $parser, 
            'stripStrangeCharacters', 
            ['hello@world#test']
        );
        $this->assertEquals('helloworldtest', $result);
    }

    public function testStripStrangeCharactersKeepsValidCharacters() {
        $parser = new McpMessageParser();

After ๐Ÿ‘‰

<?php

final class McpMessageParser {
    private $raw;

    public function parse() {
        // Step 5: Replace the private method call 
        // with the new object
        $stripper = new CharacterStripper($this->raw);
        return $stripper->strip();
    }
}

// CharacterStripper.php
// Step 2: Create a new class (Method Object)
final class CharacterStripper {
    private $input;

    // Step 4: Pass all necessary data as constructor 
    // parameters
    public function __construct($input) {
        $this->input = $input;
    }

    // Step 3: Move the private method logic 
    // to the new class
    public function strip() {
        return preg_replace(
            '/[^a-zA-Z0-9_:-]/', 
            '', 
            $this->input
        );
    }
}

// CharacterStripperTest.php

use PHPUnit\Framework\TestCase;

final class CharacterStripperTest extends TestCase {
    public function testStripRemovesSpecialChars() {
        $stripper = new CharacterStripper('hello@world#test');
        // No metaprogramming needed
        $this->assertEquals('helloworldtest', $stripper->strip());
    }

    public function testStripKeepsValidCharacters() {
        $stripper = new CharacterStripper('valid_Name-123:test');
        $this->assertEquals(
            'valid_Name-123:test', 
            $stripper->strip()
        );
    }

    public function testStripHandlesEmptyString() {
        $stripper = new CharacterStripper('');
        $this->assertEquals('', $stripper->strip());
    }

    public function testStripRemovesSpaces() {
        $stripper = new CharacterStripper('hello world test');
        $this->assertEquals('helloworldtest', $stripper->strip());
    }

    public function testStripRemovesUnicodeChars() {
        $stripper = new CharacterStripper('cafรฉโ„ข');
        $this->assertEquals('caf', $stripper->strip());
    }

    public function testStripKeepsUnderscores() {
        $stripper = new CharacterStripper('test_method_name');
        $this->assertEquals(
            'test_method_name', 
            $stripper->strip()
        );
    }

    public function testStripKeepsColons() {
        $stripper = new CharacterStripper('namespace:method');
        $this->assertEquals('namespace:method', $stripper->strip());
    }

    public function testStripKeepsHyphens() {

Type ๐Ÿ“

[X] Semi-Automatic

Safety ๐Ÿ›ก๏ธ

This refactoring is safe if you keep the same transformations and follow the Extract Method procedure.

Why is the Code Better? โœจ

You expose business rules instead of hiding them.


You can also test sanitation and other small rules without breaking encapsulation.


You remove the temptation toย test private methods.


All these benefits without changing the method visibility or breaking the encapsulation.

How Does it Improve the Bijection? ๐Ÿ—บ๏ธ

In the real world, complex operations often deserve their own identity.


When you extract a private method into a method object, you give that operation a proper name and existence in your model.


This creates a betterย bijectionย between your code and the domain.


You reduceย couplingย by making dependencies explicit through constructor parameters rather than hiding them in private methods.


Theย MAPPERย technique helps you identify when a private computation represents a real-world concept that deserves its own class.

Limitations โš ๏ธ

You shouldn't apply this refactoring to trivial private methods.


Simpleย getters,ย setters, or one-line computations don't need extraction.


The overhead of creating a new class isn't justified for straightforward logic.


You should only extract private methods when they contain complex business logic that requires independent testing.

Refactor with AI ๐Ÿค–

You can ask AI to create unit tests for you.


Read the context section.


You need to be in control, guiding it with good practices.

Suggested Prompt: 1. Identify a private method that needs testing.2. Name the real responsibility behind that logic.3. Extract the logic into a new class.4. Pass the needing objects explicitly through method arguments.5. Replace the private call with the new object.

Without Proper Instructions ๐Ÿ“ต

With Specific Instructions ๐Ÿ‘ฉโ€๐Ÿซ

Tags ๐Ÿท๏ธ

  • Testing

Level ๐Ÿ”‹

[X] Intermediate

https://www.linkedin.com/pulse/refactoring-010-extract-method-object-maximiliano-contieri

https://www.linkedin.com/pulse/refactoring-002-extract-method-maximiliano-contieri-mqubf

https://www.linkedin.com/pulse/refactoring-020-transform-static-functions-maximiliano-contieri-jmbif

See also ๐Ÿ“š

http://shoulditestprivatemethods.com/

https://maximilianocontieri.com/laziness-i-meta-programming

Credits ๐Ÿ™

Image byย Steffen Salowย onย Pixabay


This article is part of the Refactoring Series.

https://www.linkedin.com/pulse/how-improve-your-code-easy-refactorings-maximiliano-contieri



Written by mcsee | Iโ€™m a sr software engineer specialized in Clean Code, Design and TDD Book "Clean Code Cookbook" 500+ articles written
Published by HackerNoon on 2025/12/09