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
Related Code Smells ๐จ
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 ๐ฃ
- Identify a private method that needs testing.
- Name the real responsibility behind that logic.
- Extract the logic into a new class.
- Pass the needing objects explicitly through method arguments.
- 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
Related Refactorings ๐
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
