The motivation of this post is to share what problem I faced when I wrote a mock test and how I solved it. It would be nice to have a basic understanding of the Php Unit Test and Dependency Injection to grasp this article.
The requirement was to write a unit test for one of the API endpoints which job is to change the status of the order placement.
API endpoint name: PATCH v2/orders/{orderId}/status
; requesting Ordecontroller@changeOrderStatus()
OrderController Code:
app/Http/Controllers/v2/OrderController.php
<?php
namespace App\Http\Controllers\v2;
use App\Model\OrderRepository;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class OrderController extends Controller
{
public function changeOrderStatus(Request $request, int $orderId)
{
$orderRepositoryObj = new OrderRepository()
$updateStatus = $orderRepositoryObj->changeOrderStatus(
$orderId,
$status
);
if ($updateStatus) {
$response['data'][] = array(
'orderId' => $orderId,
'message' => "Order status updated and marked as {$updateStatus}.");
return response()->json($response, Response::HTTP_OK);
} else {
$response['errors'][] = array(
'title' => 'Order status update failure',
'detail' => $statusMsg['statusErrorMsg']
);
return response()->json($response, Response::HTTP_PRECONDITION_FAILED);
}
}
}
OrderRepository Model
app/Model/OrderRepository.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class OrderRepository extends Model
{
protected $table = 'order';
// disable the default timestamp column, required for eloquent
public $timestamps = false;
public function changeOrderStatus(int $orderId, string $status)
{
$affected = DB::table('order')
->where('id', $orderId)
->update(['status' => $status]);
return $affected;
}
}
I used Mockery framework to write the unit test for the API endpoint.
tests/app/Http/Controllers/v2/OrderTest.php
<?php
use App\Model\OrderRepository;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Http\Response;
class OrderTest extends TestCase
{
public function testChangeOrderStatus()
{
$this->withoutMiddleware();
$orderRepoMock = Mockery::mock(OrderRepository::class);
$orderId = $this->faker->randomNumber(4);
$status = $this->faker->randomElement($array = array ('0','1','3'));
// Create mock of the changeOrderStatus method behavior with its
// arguments. As the method returns number of updated rows so we are
// returning 1 by assuming there is a row updated.
$orderRepoMock->shouldReceive('changeOrderStatus')
->andReturn(1);
// Bind Order Model to the mock object, so that the mock will be injected
// instead of the actual model class inside the controller where the
// dependency has been injected
$this->app->instance(OrderRepository::class, $orderRepoMock);
$params = array(
'status' => $status,
);
$this->patch("v2/orders/{$orderId}/status", $params);
$this->seeJsonStructure(
[
'data' => array(
'*' => array(
'orderId',
'message'
)
)
]
);
$this->seeStatusCode(Response::HTTP_OK);
}
}
Now, the above setup to test the endpoint PATCH v2/orders/{orderId}/status
did not work. I investigated and found my controller class did not have a dependency injection of the OrderRepository model for which the above mock test did not work. So, I had to refactor the OrderController Class like below.
<?php
namespace App\Http\Controllers\v2;
use App\Model\OrderRepository;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class OrderController extends Controller
{
protected $orderRepoModel;
// Injected OrderRepository Model
protected $public function __construct(
OrderRepository $orderRepoModel
) {
$this->model = $orderRepoModel;
};
public function changeOrderStatus(Request $request, int $orderId)
{
$updateStatus = $this->model->changeOrderStatus(
$orderId,
$status
);
if ($updateStatus) {
$response['data'][] = array(
'orderId' => $orderId,
'message' => "Order status updated and marked as {$updateStatus}.");
return response()->json($response, Response::HTTP_OK);
} else {
$response['errors'][] = array(
'title' => 'Order status update failure',
'detail' => $statusMsg['statusErrorMsg']
);
return response()->json($response, Response::HTTP_PRECONDITION_FAILED);
}
}
}
Also, learned how to write a mock test for API endpoint and what needs to be in the place to achieve testing in isolation. This style of coding can be more optimized by using interface injection instead of injecting the concrete class in the controller. I will write another post to write a mock test where the interface will be injected.
Hope you find it useful!