Hi…
I don’t exactly know what you want, so I’ll blurt out everything I can remember. I’ll assume you are using some kind of API (gateway) to the payment provider.
You want to test the following after every change:
(a) I write my model classes correctly.
(b) I wrote PaymentGateway correctly.
(c) The payment provider hasn’t changed their formats or processes lately.
(d) It works as a payments system.
(e) All the happy and failure paths work as the business intended.
(a) Is pretty easy and it’s the usual mock objects thing. We had a PaymentGateway we would pass in from the top through DI. It had methods like pay(), refund(), reverse(), preAuthorise(), etc. We’d either get an exception, or some authorisation data for storing in the DB.
(b) You can usually get a test account or special testing interface. It will respond with success on a special credit card number, such as all 7’s. Often an account will start in a testing phase, and be switched later. You need a test mode account, so get two accounts if necessary and just keep one in test mode. This is the account you use for getting the basic XML packets right and the authentication to the server. It won’t be enough by itself, but it’s a start.
Here’s the first big problem…
When you authorise a credit card, the other end will usually send only a very primitive response. Usually it’s simple regex fraud checks. Mostly it will say it’s authorised. This is a lie.
Due to volume and the slow response of the banks, the actual request will enter a queue. Typically the authorisation will be completed ten minutes later. What this means it that the status you got back as successful will possibly be rewritten as rejected. You have to do some kind of query status call against the API to get the true result.
This plays havoc with your testing, as to test each process (e.g. buy something, then reverse it, then buy something else using the original authorisation) can take a half hour. That means you have to run these things manually at first, then overnight.
You then hit the second problem…
You actually need a real credit card for a lot of these tests. Of course you don’t want to have something as security sensitive as the company credit card in your SVN/git repository
.
What you’d really like is a credit card with limited liability, say a hundred quid of credit tops. These do exist, but they are the very credit card types that are not allowed on the internet
. If you know anyone in the banking industry that can fix this, please do tell them. It’s really, really annoying.
So we had a test suite which took the details of a CC on the command line and then ran for a while. Quite a while.
(c) Banking processes, especially timings and fraud checks, do change a bit. It’s worth running the real payments test script monthly as well as when there is a customer problem.
(d) Getting all this stuff to work as a module means writing a test suite the developers can run, either as part of cruise or manually before check-in. This rules out real credit cards and the like.
Your payments model will interact with two components: the payment gateway and whatever system you use to read the payment status ten minutes later. That latter system could be a cron job or a message queue with a message delay.
We used a cron job, but the scripts are a bit messy and changing them was awkward. I think next time I would use a simple message queue such as Beanstalk. Just send a message that such and such a payment needs to be verified, set the message to be delayed ten minutes and then have the message arrive at a specialist script for the task. Works for authorisation reversals too (for repeat billing). Learning beanstalk takes a couple of hours and there are some great PHP interfaces to it.
Anyways, to test our models we need a fake gateway.
This is just a localhost URL (part of the test suite) that spits out a packet given a file. We just had a PHP script that that compared the incoming GET/POST information with a temporary file (called get and post respectively, because that’s how creative we are) and if matching it then output another temporary file. This sounds super generic for testing any API, and indeed it was.
The test fixtures just had to save these files during setup() and clean them out at teardown(). Something like…
function testSuccessfulPaymentResponseResultsInPurchaseMatchingTheCart() {
$this->api()->responseWillBe($this->payments()->successPacket());
$this->api()->expectsPost($this->payments()->payPacket('1234 5678 1234 5678', ...));
...
$cart->checkout();
$this->assertEqual($cart->items(), $account->lastPurchase()->items());
}
It will take a good three days to get all these fixtures working properly, but it will pay for itself many times over.
Anyways, set the gateway endpoint in your configurations so that developer setups use the local fake gateway. For the delayed stuff have either the message delay also set in configuration, so that you can set it to zero when testing (the fake server will need to be able to queue responses), or for cron jobs just run the cron target script in the test.
(e) Most businesses think you get the CC check done instantly, and get a very nasty surprise when you explain the details. Your acceptance tests will have to include all these failure conditions where the web site told the customer that all was OK, but they failed a fraud check ten minutes later and we had to email them a link to try again, etc, etc.
This is all high level business stuff and will probably change a lot. We just used web based acceptance tests again using configuration to set the endpoint to be the fake server…
function testFailedFraudCheckCausesRetryLinkToBeSentByMail() {
$this->api()->responseWillBe($this->payments()->successPacket());
$this->get($this->home());
$this->click('Products');
...
$this->click('checkout');
$this->setField('email', 'marcus@lastcraft.com');
$this->setField('cc', '1234 1234 1234 1234');
...
$this->click('Confirm');
$this->assertText('Congratulations');
$this->api()->responseWillBe($this->payments()->declinedPacket());
$this->runVerificationCronJob();
$this->get($this->mail('marcus@lastcraft.com')->linkWith('/retry'));
$this->assertText('Please try a different credit card');
}
This meant we could sit down with the stakeholder (usually customer services) while we stepped through the acceptance test. Having it web based meant we could make changes on the fly in sufficiently detailed pseudo code to have it clear to both parties.
I don’t know if that’s too much information. I can fill in details if you only want to know part of this story. We had a complicated repeat billing subscription model and it took about 6 weeks to get it all done. Probably just over a week of that was setting up all these various test fixtures and another two weeks just reading docs and getting acceptance tests matching what was wanted.
yours, Marcus