Search code examples
phpunit-testingphpunit

How to write unit test for code calling a class from a library with public constants and constructor?


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()?


Solution

  • 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?

    • Find the offending parts (e.g. new not in creation only code, static method calls into libraries and third party frameworks etc.)
    • List them, look how they are used
    • Introduce a seam at those hurting places - possible?
    • Only test your code, not the third library code - get library code as far away as possible -> from 4 places to 1 place (75% of the original problem reduced just by that, it is much smaller now).
    • Only you can mock something must not mean that you should mock all that (!) - Train your nose. Otherwise greetings from Mocking Hell.

    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.