I am writing some unit test for some legacy code as a preparation for some refactoring.
One section of the code is calling a class from a third-party library (I cannot change the code in it).
The code is using its constructor and calling some static variable.
May I ask how can I mock the method for testing? (or how do I test the method without mocking?)
I have tried to simplify my code for an example:
/////////////////////////////////////////////////////////////////////
// This is from the library code that I am unable to change
class ResponseMessage
{
const STATUS_ERROR = 500;
const STATUS_SUCCESS = 200;
function __construct( $responseMessage, $responseCode ) {
$this->responseMessage = $responseMessage;
$this->responseCode = $responseCode
}
public function sendMessage($sendToAdmin = false) {
if ($sendToAdmin) {
$targetEmailAddress = $this->emailAddresses['admin'];
$targetAPISettings = $this->apiSettings['admin'];
}
else {
$targetEmailAddress = $this->emailAddresses['customer'];
$targetAPISettings = $this->apiSettings['customer'];
}
$this->triggerSendEmail($targetEmailAddress);
$this->triggerAPI($targetAPISettings);
}
//.... Some other methods and variables
}
/////////////////////////////////////////////////////////////////////
// This is my class to be tested. If possible I would like to avoid
// changing any code, but if it is necessary this code can be changed
// a bit for injecting mock class.
class myClass
{
// ... some other methods where I have no problem testing....
function handle($input) {
//The actual comparison is much more complicated. Simplified for my question
if (empty($input)) {
// Some other error handling, no response message is generated
return;
}
if ( $input == 1 )
$responseMessage = new ResponseMessage('The input is 1', ResponseMessage::STATUS_SUCCESS );
else if ( $input == 2 )
$responseMessage = new ResponseMessage('The input is 2', ResponseMessage::STATUS_SUCCESS );
else
$responseMessage = new ResponseMessage('Unexpected input', ResponseMessage::STATUS_ERROR );
$respnoseMessage->sendMessage();
$responseMessageToAdmin = new ResponseMessage('The comaparison is triggered', RespnoseMessage::STATUS_SUCCESS);
$responseMessageToAdmin->sendMessage(true);
}
}
///////////////////////////////////////////////////////////////////
// This is my PHPUnit testing class
class testMyClass
{
public function testProcessWithInputOne {
// how should I mock ResponseMessage here?
/////////////////////////////////////////////////////////
// These are not going to work, but tried to write some code to demo what I want to test.
$mockResponseMessage = Mockery::mock('alias:ResponseMessage');//used alias for simplicity, but mocking it normally and inject the mocked object is also acceptable.
$mockResponseMessage->setConst('STATUS_SUCCESS', 200);
$mockResponseMessage->shouldReceive('__construct')->with('The input is 1', 200)->once()->andReturn($mockResponseMessage);
$mockResponseMessage->shouldReceive('sendMessage')->with(false)->once(); //here with(false) is testing no argument is passed as the default argument value is false
$mockResponseMessage->shouldReceive('__construct')->with('The comaparison is triggered', 200)->once()->andReturn($mockResponseMessage);
$mockResponseMessage->shouldReceive('sendMessage')->with(true)->once();
/////////////////////////////////////////////////////////
$myClass = new MyClass();
$myClass->handle(1);
}
}
How to write unit test testMyClass
for code (class myClass
) calling a class from a library ResponseMessage
with public constants (ResponseMessage::STATUS_ERROR
, ...::STATUS_SUCCESS
) and constructor ResponseMessage::__construct()
?
You certainly want to make all the problems of new
go away:
if ( $input == 1 )
$responseMessage = new ResponseMessage('The input is 1', ResponseMessage::STATUS_SUCCESS );
###################
else if ( $input == 2 )
$responseMessage = new ResponseMessage('The input is 2', ResponseMessage::STATUS_SUCCESS );
###################
else
$responseMessage = new ResponseMessage('Unexpected input', ResponseMessage::STATUS_ERROR );
###################
$respnoseMessage->sendMessage();
$responseMessageToAdmin = new ResponseMessage('The comaparison is triggered', RespnoseMessage::STATUS_SUCCESS);
###################
$responseMessageToAdmin->sendMessage(true);
I found four places.
The first three places create a closure for the ->sendMessage() protocol.
Same for the fourth place.
What your class is certainly missing is a seam for this:
public function responseSendMessage($responseMessage, $responseCode, $sendToAdmin = false)
{
(new ResponseMessage($responseMessage, $responseCode))->sendMessage($sendToAdmin)
}
Introduce it at the places:
switch ($input) {
case 1: $this->responseSendMessage('The input is 1', ResponseMessage::STATUS_SUCCESS); break;
case 2: $this->responseSendMessage('The input is 2', ResponseMessage::STATUS_SUCCESS); break;
default: $this->responseSendMessage('Unexpected input', ResponseMessage::STATUS_ERROR);
}
$this->responseSendMessage('The comaparison is triggered', RespnoseMessage::STATUS_SUCCESS, true);
Now in testing decide which part you'd like to replace because all those parts that you may want to mock, are yours.
You can introduce the test and you can easily refactor locally with the seam only, then on the large scale later.
And you already have improved your code and this refactoring is local (only one method affected).
If you already have upgraded the PHP version, make use of match expressions instead of the switch/case.
How did I do it?
Yes, your question is simplified. Check if you can apply this or how. But also see what is happening here: Due to the simplification you can find those parts. So this is based on your work asking the question.
The seam idea is from Feathers refactoring legacy code book first edition. If you have some serious parts in front of you, get a couple of copies for your whole team and send everyone in the reading room, then discuss your reading experiences and learning together.
While what was shown is a Refactoring, you refactor pretty specifically here. That is, you need to make this go hand-in-hand with writing the test (not shown here), and best you write the test-code first (e.g. for mocking this one method) and then let it crash as the method not yet exists.
This is important because you want to have a test, too, that responseSendMessage() is working, but technically, you can fully render it void, it's a single method under your control.
The problem is not there any longer.
Happy testing & maintaining.