1
\$\begingroup\$

I ran into some issues with unit testing in that getting the response body etc from the curl handle was tricky. I didn't want to necessarily use a mock or abstract the curl functionality.

A colleague of mine suggested that I just use the localhost as an endpoint and reflect back the request for unit testing. I thought this was a novel approach.

The directories in question are /test/echo/ and /test/unit/

I'm also interested in seeing if anyone had any opinions on the assertions.

GitHub

The echo endpoint:

    <?php
header('Content-Type: application/json');
$data = array(
'headers' => getallheaders(),
// 'server' => $_SERVER,
'request_method' => $_SERVER['REQUEST_METHOD'],
'get' => $_GET,
'post' => $_POST,
'put' => $_POST,
);
//If the request is a put then get the file contents and try to parse the string into an array
if($data['request_method'] == 'PUT')
{
parse_str(file_get_contents("php://input"), $put_data);
$data['put'] = $put_data;
}
echo json_encode($data);

An example of one of the unit tests:

<?php
class TransactionTest extends PHPUnit_Framework_TestCase{
static $endpoint = 'http://localhost/payjunctionphp/test/echo';
public function setUp()
{
$options = array(
'username' => 'pj-ql-01',
'password' => 'pj-ql-01p',
'appkey' => '2489d40d-a74f-474f-9e8e-7b39507f3101'
);
parent::setUp();
$this->client = new TransactionClient($options);
$this->client->setEndpoint(self::$endpoint);
}
private function getRequestPath($client = null)
{
if(!isset($client)) $client = $this->client;
return str_replace($client->baseUrl,'',curl_getinfo($client->curl)['url']);
}
/**
* Ensure that the correct verb and path are used for the create method
*/
public function testCreate()
{
$data = array(
'achRoutingNumber' => '987654321',
'achAccountNumber' => '123456789',
'achAccountType' => 'CHECKING',
'foo' => 'bar'
);
$transaction = $this->client->create($data);
$this->assertEquals($data, get_object_vars($transaction->post),'Passed variables are not correct');
$this->assertEquals('POST', $transaction->request_method,'The PHP Verb Is Incorrect');
$this->assertEquals('/transactions', $this->getRequestPath(), 'The path is incorrect');
}
/**
* Ensure that the correct verb and path are used for the read method
*/
public function testRead()
{
$transaction = $this->client->read(543);
$this->assertEquals('GET', $transaction->request_method,'The PHP Verb Is Incorrect');
$this->assertEquals('/transactions/543', $this->getRequestPath(), 'The path is incorrect');
}
/**
* Ensure that the correct verb and path are used for the read method
*/
public function testUpdate()
{
$data = array(
'foo' => 'baz'
);
$transaction = $this->client->Update(654,$data);
$this->assertEquals($data, get_object_vars($transaction->put),'Passed variables are not correct');
$this->assertEquals('PUT', $transaction->request_method,'The PHP Verb Is Incorrect');
$this->assertEquals('/transactions/654', $this->getRequestPath(), 'The path is incorrect');
}
/**
* Ensure that the correct verb and path are used for the read method
*/
public function testAddSignature()
{
$data = array(
'foo' => 'baa'
);
$transaction = $this->client->addSignature(655,$data);
$this->assertEquals($data, get_object_vars($transaction->post),'Passed variables are not correct');
$this->assertEquals('POST', $transaction->request_method,'The PHP Verb Is Incorrect');
$this->assertEquals('/transactions/655/signature/capture', $this->getRequestPath(), 'The path is incorrect');
}
}

The base model from which the various clients extend from:

    <?php
class PayjunctionClient
{

public $liveEndpoint = 'https://api.payjunction.com';
public $testEndpoint = 'https://api.payjunctionlabs.com';
public $packageVersion = '0.0.1';
public $userAgent;
public function __construct()
{
$this->userAgent = 'PayJunctionPHPClient/' . $this->packageVersion . '(BrandedCreate; PHP/)'; //@todo add process.version
$this->baseUrl = $this->testEndpoint;
}
public function setEndpoint($endpoint)
{
$this->baseUrl = $endpoint;
}
/**
* @description initializes the curl handle with default configuration and settings
* @param null $handle
* @return $this
*/
public function initCurl($handle = null)
{
$this->curl = curl_init();
curl_setopt($this->curl, CURLOPT_SSL_VERIFYPEER, false); //Don't worry about validating ssl @todo talk about security concerns
curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
//if we have a password and username then set it by default to be passed for authentication
if (isset($this->defaults['password']) && isset($this->defaults['username'])) {
curl_setopt($this->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
curl_setopt($this->curl, CURLOPT_USERPWD, $this->defaults['username'] . ":" . $this->defaults['password']);
}
//if we have default headers to pass then pass them
if (isset($this->defaults['headers']) && is_array($this->defaults['headers'])) {
$headers = array();
foreach ($this->defaults['headers'] as $key => $value) {
array_push($headers, $key . ': ' . $value);
}
curl_setopt($this->curl, CURLOPT_HTTPHEADER, $headers);
}
return $this;
}
/**
* @description generates a new client
* @param null $options
* @return $this
*/
public function generateClient($options = null)
{
$this->baseUrl = isset($options['endpoint']) ? $options['endpoint'] : $this->baseUrl;
$this->defaults['username'] = isset($options['username']) ? $options['username'] : '';
$this->defaults['password'] = isset($options['password']) ? $options['password'] : '';
$this->defaults['headers']['X-PJ-Application-Key'] = isset($options['appkey']) ? $options['appkey'] : '';
$this->defaults['headers']['User-Agent'] = $this->userAgent;
$this->initCurl();
return $this;
}
/**
* @description takes the response from our curl request and turns it into an object if necessary
* @param $response
* @param null $contentType
* @return array|mixed
*/
public function processResponse($response)
{
$contentType = curl_getinfo($this->curl, CURLINFO_CONTENT_TYPE);
if ($contentType == 'text/html' || is_null($contentType) || !isset($contentType) || $contentType = '' || $contentType == FALSE) {
return $response;
}
try {
$object = json_decode($response);
return $object;
} catch (Exception $e) {
return array(
'errors' => array(
0 => 'Invalid Response Type, Error In Processing Response From Payjunction'
)
);
}
}
/**
* @description processes a curl post request
* @param $path
* @param null $params
* @return array|mixed
*/
public function post($path, $params = null)
{
curl_setopt($this->curl, CURLOPT_POST, TRUE);
curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . $path);
if (is_object($params) || is_array($params)) {
curl_setopt($this->curl, CURLOPT_POSTFIELDS, http_build_query($params));
}
return $this->processResponse(curl_exec($this->curl));
}
/**
* @description processes a curl get request
* @param $path
* @param null $params
* @return array|mixed
*/
public function get($path, $params = null)
{
//create the query string if there are any parameters that need to be passed
$query_string = "";
if (!is_null($params)) {
$query_string = "?" . http_build_query($params,'','&');
}
curl_setopt($this->curl, CURLOPT_HTTPGET, TRUE);
curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . $path . $query_string);
return $this->processResponse(curl_exec($this->curl));
}
/**
* @description processes a curl put request
* @param $path
* @param null $params
* @return array|mixed
*/
public function put($path, $params = null)
{
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, "PUT");
if (is_object($params) || is_array($params)) {
curl_setopt($this->curl, CURLOPT_POSTFIELDS, http_build_query($params));
}
curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . $path);
return $this->processResponse(curl_exec($this->curl));
}
/**
* @description processes a curl delete request
* @param $path
* @param null $params
* @return array|mixed
*/
public function del($path, $params = null)
{
curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, "DELETE");
if (is_object($params) || is_array($params)) {
curl_setopt($this->curl, CURLOPT_POSTFIELDS, http_build_query($params));
}
curl_setopt($this->curl, CURLOPT_URL, $this->baseUrl . $path);
return $this->processResponse(curl_exec($this->curl));
}
}

The TransactionClient related specifically to this unit test:

<?php
class TransactionClient extends PayjunctionClient
{
public function __construct($options)
{
parent::__construct();
$this->generateClient($options);
}
/**
* @description create a new transaction
* @param $params
* @return array|mixed
*/
public function create($params)
{
return $this->post('/transactions',$params);
}
/**
* @description read from an existing transaction
* @param $id
* @return array|mixed
*/
public function read($id)
{
return $this->get('/transactions/'.$id);
}
/**
* @description update an existing transaction
* @param $id
* @param null $params
* @return array|mixed
*/
public function update($id, $params = null)
{
return $this->put('/transactions/'.$id, $params);
}
/**
* @todo this does not appear to be working 405 Method Not Allowed
* @description add a signature to an existing transaction
* @param $id
* @param $params
* @return array|mixed
*/
public function addSignature($id, $params)
{
return $this->post('/transactions/'.$id.'/signature/capture',$params);
}
}
\$\endgroup\$
  • 1
    \$\begingroup\$ When testing with external services, mocks are the way to go. \$\endgroup\$ – Dincho Todorov Aug 27 '14 at 18:38
  • \$\begingroup\$ Sure I agree that is a way to go, and an option that I looked at. Thanks Dincho. \$\endgroup\$ – ajameswolf Aug 27 '14 at 21:32
  • \$\begingroup\$ I highly suggest you to start indenting your code. \$\endgroup\$ – Kid Diamond Sep 22 '14 at 17:21
3
\$\begingroup\$

Personally, I'd refactor the CURL functionality out of the PayJunctionClient class. That way you could easily test the functionality of the PayJunctionClient class by feeding it a mock CURL wrapper class. (Defining an interface could be of even more help.)

Having the CURL function in it's own class would also have the advantage of not having more than one curl_setopt call, making the code a lot more readable.

That said, I am one of those people that will go to great length to make sure my unit tests don't touch the filesystem or network.

In regards to testing a class wrapping CURL functions, I would suggest using PHPT. A good example of how this could look is this test.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.