If you're using fakes, you should be using them in your tests, not in your actual code.
How to go about replacing testing classes with real ones in production while using Version Control
I am little confused about taking the right approach for this. I have a project where i have used Fake Classes for third-party services i am using. All seems good until i have to deploy the project on the server for production. How do i go about replacing the fake test classes with the real classes in Controllers? Should i replace the test classes with the real ones and then commit it to repo and pull it on the server?
@zachleigh I am not sure if fakes is the right term i have used. You can see in the below code i am using FakeMessageService to test out the SMS sending feature. But in production, it needs to be swapped out with the real class (SomeMessageService).
class OneTimePasswordController extends Controller
{
/**
* @param FakeMessageService|MSG91MessageService $fakeMessageService
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function sendOneTimePassword(FakeMessageService $fakeMessageService)
{
$user = User::findorFail(Auth::id());
$messageResponse = $fakeMessageService->sendOTP($user->getCountryISDCode(), $user->getPhoneNumber());
$messageResponse = json_decode($messageResponse);
if ($messageResponse->status == "success") {
$user->one_time_password_sent = true;
$user->update();
return response()->json([
'status' => 'ok',
'message' => 'One Time Password Sent Successfully'
]);
} else {
return response()->json([
'status' => 'ok',
'message' => 'Something Went Wrong'
]);
}
}
public function verifyOneTimePassword(Request $request, FakeMessageService $fakeMessageService)
{
$user = User::findOrFail(Auth::id());
if ($user->isOneTimePasswordSent()) {
$oneTimePassword = $request->get('one_time_password');
$messageResponse = $fakeMessageService->verifyOTP($user->getCountryISDCode(), $user->getPhoneNumber(), $oneTimePassword);
$messageResponse = json_decode($messageResponse);
if ($messageResponse->status == 'success') {
$user->one_time_password = $oneTimePassword;
$user->one_time_password_sent = false;
$user->update();
event(new PhoneNumberVerified($user));
return response()->json([
'status' => 'ok',
'message' => 'Phone Number Verified Successfully'
]);
} else {
return response()->json([
'status' => 'ok',
'message' => 'Something Went wrong'
]);
}
} else {
return response()->json([
'status' => 'ok',
'message' => 'Something went wrong'
]);
}
}
}
So every time you want to test/deploy, you have to change all your controllers, and not only the method signature, but the method body too because in the code you are calling it $fakeMessageService. Thats a massive, massive pain in the ass.
I normally don't test controller methods because a) all my controllers do is call service classes/models which all individually unit tested and b) my acceptance tests hit all the controller methods. So to me, writing unit tests for controllers is a waste of time. I'm sure people disagree with me, but thats fine.
However, if you want to test your controllers, you need to make use of interfaces. Probably something like this (I don't test controllers, so I'm not 100% certain this will work, but I feel like it should...):
public function sendOneTimePassword(MessageServiceInterface $messageService)
Then both your real message service and your fake message service can implement the MessageServiceInterface. You will need to bind the real MessageService class to the interface in a service provider. In your real code, Laravel will inject the real MessageService class for you. When you test the controller method, however, you can create a new instance of the controller and a new instance of the fake service class. Call the controller method and pass it your fake service class.
@zachleigh I have infact created an Interface which gets implemented by both RealMessageService and FakeMessageService.
MessageService Interface:
interface MessageService
{
const TOTAL_CHANCES_AVAILABLE = 6;
public function sendOTP($countryCode, $mobileNumber);
public function verifyOTP($countryCode, $mobileNumber, $oneTimePassword);
}
FakeMessageService
class FakeMessageService implements MessageService
{
/**
* @param $countryCode
* @param $mobileNumber
* @return string
*/
public function sendOTP($countryCode, $mobileNumber)
{
$response = array(
'status' => 'success',
'response' => array(
'code' => 'OTP_SENT_SUCCESSFULLY',
'refreshToken' => 'Refresh-Token'
)
);
return json_encode($response);
}
/**
* @param $countryCode
* @param $mobileNumber
* @param $oneTimePassword
* @return mixed
*/
public function verifyOTP($countryCode, $mobileNumber, $oneTimePassword)
{
$response = array(
'status' => 'success',
'response' => array(
'code' => 'NUMBER_VERIFIED_SUCCESSFULLY',
'refreshToken' => 'Refresh-Token'
)
);
return json_encode($response);
}
RealMessageService
class MSG91MessageService implements MessageService
{
private $baseUrl = "https://sendotp.msg91.com/api";
private $oneTimePassword;
private $countryCode;
private $mobileNumber;
private $oneTimePasswordCount = 0;
private $oneTimePasswordSent = false;
/**
* @param $countryCode
* @param $mobileNumber
* @return mixed|\Psr\Http\Message\ResponseInterface|string
*/
public function sendOTP($countryCode, $mobileNumber)
{
if ($this->oneTimePasswordCount > $this->getTotalChancesAvailable()) {
$this->countryCode = $countryCode;
$this->mobileNumber = $mobileNumber;
$this->oneTimePasswordSent = true;
$this->oneTimePasswordCount++;
$client = $this->getNewClient();
$response = $client->request('POST', $this->baseUrl . "generateOTP", [
'countryCode' => $countryCode,
'mobileNumber' => $mobileNumber,
]);
$this->oneTimePassword = $response->response->code;
return $response;
} else {
return json_encode(array(
"status" => "failure",
"response" => [
"code" => "OTP_LIMIT_EXCEEDED",
],
), JSON_FORCE_OBJECT);
}
}
/**
* @param $countryCode
* @param $mobileNumber
* @param $oneTimePassword
* @return mixed
*/
public function verifyOTP($countryCode, $mobileNumber, $oneTimePassword)
{
if ($this->countryCode == $countryCode && $this->mobileNumber == $mobileNumber && $this->oneTimePassword == $oneTimePassword) {
$this->oneTimePasswordCount = 0;
$this->oneTimePasswordSent = false;
return true;
} else {
return false;
}
}
/**
* @return Client
*/
private function getNewClient()
{
$client = new Client(['headers' => ['application-Key' => env('MSG_91_KEY')]]);
return $client;
}
/**
* @return int
*/
private function getTotalChancesAvailable(): int
{
return self::TOTAL_CHANCES_AVAILABLE;
}
"Then both your real message service and your fake message service can implement the MessageServiceInterface. You will need to bind the real MessageService class to the interface in a service provider. In your real code, Laravel will inject the real MessageService class for you. When you test the controller method, however, you can create a new instance of the controller and a new instance of the fake service class. Call the controller method and pass it your fake service class.", i am not able to understand this part.
Please or to participate in this conversation.