Unit testing a checkout controller

Has anyone unit tested a checkout controller?

Currently to test that checkout works we either:

  1. Use a live card
  2. Use a test card with the gateway in test mode
  3. Use a live card with the gateway switched out with a dummy gateway that always processes.

However, all of those require adding products to the card, filling out checkout forms, typing in a credit card, etc. and there is a lot of room for human error in the testing process.

I’m starting to think about how a unit test could be constructed. I’d have to provide a mock customer, mock products, mock gateway, mock mailer, and somehow ensure that the database schema I’m testing against is an exact copy of the live database.

I’m wondering if anyone has experience with doing this, so they can advise on pitfalls and best practices?

Also, there will probably be lots of emails flying around (at least with the acceptance tests) so fakemail could come in handy.[URL=“http://www.lastcraft.com/fakemail.php”]

Only the parts that might break :wink:

That would be awesome, if you’d be so inclined.


I’ve built a subscription one. What part are you trying to test? Or do you want a high level plan for the whole thing?

yours, Marcus

Off Topic:

Great question! subscribed :slight_smile:

If you’re testing the entire process, it’s an integration test and using the gateway in test-mode would make sense.


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 :eek:.

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 :rolleyes:. 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()->expectsPost($this->payments()->payPacket('1234 5678 1234 5678', ...));
    $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->setField('email', 'marcus@lastcraft.com');
    $this->setField('cc', '1234 1234 1234 1234');
    $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

I’m not testing the entire process… just what happens after the user hits “Check out”. There’s a lot of logic in a fairly procedural chunk that I’d like to tease out into one or more unit tests.

Seriously? Do all gateways do this, or only specific ones? I don’t remember ever running into the situation where a card was accepted and then the payment later failed.

I’m mostly concerned with (a) and (e), but that’s not to say the other information isn’t useful.

I’ve never tried fakemail. I currently have the site set up so that when it’s in test mode, all emails go to me. What would the benefit of fakemail be over that system?


I would expect all.

You find out the fine details when you use them through their APIs. If you are using an API, such as XPay (Secure Trading) amongst others, you get this explained in the docs small print. If customer services are using the admin interface supplied by the provider, then they are probably already dealing with this and taking sales figures post fraud check. You might want to cross check the rejects to see if all the figures tally with your part of the system.

Note that often the banking fraud checks are not always enabled by default. The payment gateway service will be doing simple checks (such as blocking some parts of China or Nigeria) and known blacklists of dodgy cards.

Ask the provider.

yours, Marcus

It allows you to bring emails into the acceptance test. If they’re important - I expect they are - they should be tested.

Fakemail gives you the bare bones but you’ll also need to write some custom email expectations which examine the contents of the fakemail dir, and something to empty the dir between tests.

I’m not a user of FakeMail, but I’d imagine using it gives you two (rather important) benefits: first of all, I think it would be faster to send an e-mail to a local “fake” server than to an actual live SMTP server. The second benefit is automation: there’s still a chance that you’re misinterpreting the output, for example, because there’s a link in the e-mail that needs to be verified (e.g. a link to an “activate account” page, which needs to have the correct user-id). Automated tests will probably catch a glitch faster than you, especially if your last night was a combination of bad films and cheap wine. (-: