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 💨
Code Smell 18 — Static Functions
Maxi Contieri ・ Nov 6 '20
Code Smell 21 - Anonymous Functions Abusers
Maxi Contieri ・ Nov 10 '20
Code Smell 177 - Missing Small Objects
Maxi Contieri ・ Nov 5 '22
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 almost every time a mistake.
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 |
|---|---|
| ChatGPT | ChatGPT |
| Claude | Claude |
| Perplexity | Perplexity |
| Copilot | Copilot |
| You | You |
| Gemini | Gemini |
| DeepSeek | DeepSeek |
| Meta AI | Meta AI |
| Grok | Grok |
| Qwen | Qwen |
Tags 🏷️
- Testing
Level 🔋
[X] Intermediate
Related Refactorings 🔄
Refactoring 010 - Extract Method Object
Maxi Contieri ・ Nov 7 '22
Refactoring 020 - Transform Static Functions
Maxi Contieri ・ Dec 15 '24
See also 📚
Laziness I: Meta-programming
Maxi Contieri ・ Jan 30 '21
Credits 🙏
Image by Steffen Salow on Pixabay
This article is part of the Refactoring Series.
Top comments (0)