Laravel 5 Package Testing with Continuous Integration

Written by Christophe Limpalair on 07/30/2016

Whether you've written a package before or not, this post will guide you through the process of testing packages using Continuous Integration.

The process can be a bit tedious because of dependencies. To avoid wasting time trying to figure out why you keep getting a "Class Not Found" exception, this post will show you how to structure directories, load dependencies, and use namespacing.

Directory Structure

This assumes you are developing a package in a /packages/ directory placed in the root directory of a Laravel project and not in /vendor/. I recommend that you do it that way to keep your vendor clean of unfinished packages. Here's how I structure it: (if you already have a structure you're happy with, skip forward)

1) Create a packages directory
2) Structure your package directories like this:

packages/
vendor/
package/
src/
tests/
composer.json
phpunit.xml
circle.yml (or .travis.yml or .magnum.yml, etc..)


vendor is typically your GitHub account name or organization name.
package is your package name and the root directory.
src is where your business logic goes.
tests is where our testing logic goes.
composer.json is where we manage dependencies and autoloading. We'll configure this in a moment.
phpunit.xml is where we configure PHPUnit.
.xxxxx.yml is our configuration file for our CI service of choice.

Here's what mine looks like:

packages/
limpalair/
stripe/
...


Once you have this structure, we need to autoload it in our main composer.json.

Composer Configuration

Find the composer.json in your main Laravel project directory (not your package's composer.json).

You should already have something that looks like this:
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
},


We just need to add our package below that:
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
],
"psr-4": {
"Limpalair\\Stripe\\": "packages/limpalair/stripe/src"
}
},


This alone will give your main Laravel project access to the
Limpalair\Stripe
namespace which is mapping to
packages/limpalair/stripe/src
. How easy was that?

Now we need to do something similar in the package's composer.json file to load dependencies and autoload our /src/ directory. So go back in your package directory and open up composer.json.

Here's what a typical file might look like:
{
"name": "limpalair/stripe",
"description": "Limpalair Stripe provides a simple and non-intrusive interface to Stripe's payment system in Laravel.",
"keywords": ["laravel", "stripe"],
"license": "MIT",
"authors": [
{
"name": "Christophe Limpalair",
"email": "christf24@gmail.com"
}
],
"require": {
"php": ">=5.4.0",
"illuminate/support": "~5.0",
"stripe/stripe-php": "~3.0",
},
"require-dev": {
"phpunit/phpunit": "~3.0",
"orchestra/testbench": "~3.0"
},
"autoload": {
"psr-4": {
"Limpalair\\Stripe\\": "src/"
}
}
}


In this example, I am building a simple Stripe implementation package for Laravel. This is a good example because my package needs a few dependencies to work.

The first thing I want you to notice is the "autoload" section. This should look very similar to what we just did in our other composer.json file. If you want a better explanation of how autoloading works, read it here, but it basically allows you to define a mapping from namespaces to directories. This means I can now access classes in src/ by using the Limpalair\Stripe namespace.

If this is the first time you really think about how autoloading works, take a moment to think how powerful it is. You can drop in third party code and start using it just by adding a namespace to your code.

(Note that I'm using psr-4 in this example, while many packages out there use psr-0. Read about the differences here.)

The second thing I want you to notice is that I'm requiring the stripe-php package that Stripe built so that I can access their API. I'm also requiring phpunit and orchestra/testbench.

PHPUnit will be used to run our tests.

Orchestra/testbench makes testing Laravel packages easier. It is a lot more powerful than what I'm going to show you, so I recommend you check out the documentation.

Now that we've covered all the basic requirements for composer, let's dive in some code so you can see where to use namespaces we just mapped and avoid the dreaded "Class Not Found".

Namespaces and Setting up Tests

My main class that I want to test looks like this:

StripePortal.php (in src/)
<?php

namespace Limpalair\Stripe;

use Log;
use Exception;
use Stripe\Stripe;
use Stripe\Charge as StripeCharge;
use Stripe\Customer as StripeCustomer;
use Illuminate\Support\Facades\Config;
use Stripe\Error\Card as StripeErrorCard;

class StripePortal
{
protected static $currency = 'usd';

/**
* Create a new StripePortal instance
*
* @return void
*/
public function __construct()
{
Stripe::setApiKey($this->getStripeKey());
}

/**
* Get the Stripe API key
*
* @return string
*/
public function getStripeKey()
{
$secret = Config::get('services.stripe.secret');

if ( empty($secret) )
throw new \InvalidArgumentException('Stripe API key not properly configured');

return $secret;

}

/**
* Charge a customer
*
* @return Stripe\Charge
*/
public function chargeStripeCustomer($amount, $options = [])
{
$options = array_merge([
'currency' => $this->getCurrency(),
], $options);

$options['amount'] = $amount;

if ( ! array_key_exists('source', $options) ) {
throw new \InvalidArgumentException('Missing credit card information');
}

try {
$response = StripeCharge::create($options);
} catch(StripeErrorCard $e) {
Log::error("Caught Stripe purchase failure: " . $e);
return false;
}

return $response;
}

/**
* Get currency
*
* @return string
*/
public function getCurrency()
{
return static::$currency;
}

}


It's a very simple class that sets our Stripe API key by looking it up in our services configuration file (config/services.php) with the
getStripeKey()
function.

This key is absolutely necessary to create a connection with the Stripe API, so it is required before we do anything else. If it's not properly set in your .env file or in your configuration file, then we want to throw an
InvalidArgumentException
.

There is another check in
chargeStripeCustomer()
which makes sure we're passing in the user's card information.

Finally, I have a
getCurrency()
function which returns the currency to use.

At the top of the file, I also declared
namespace Limpalair\Stripe;
. This is the namespace mapping to src/ that we are autoloading with our composer.json file. We can now access this file from
use Limpalair\Stripe\StripePortal;
.

With this in mind, lets go in our tests/ directory and create our first tests in StripePortalTest.php

<?php

use Limpalair\Stripe\StripePortal;

class StripePortalTest extends \Orchestra\Testbench\TestCase
{
protected function getEnvironmentSetUp($app)
{
$app['config']->set('services.stripe.secret', getenv('STRIPE_SECRET'));
}

public function testExpectedGetCurrency()
{
$stripePortal = new StripePortal();
$this->expectOutputString($stripePortal->getCurrency());

print 'usd';
}

public function testExceptionChargeStripeCustomerNoCard()
{
$this->setExpectedException('\InvalidArgumentException', 'Missing credit card information');

$stripePortal = new StripePortal();
$stripePortal->chargeStripeCustomer('1000', ['id' => 'cu_a212918kagjd']);
}
}


How easy is that namespace?

Alright, let's walk through this file to explain my tests.

I'm extending
\Orchestra\Testbench\TestCase
instead of
PHPUnit_Framework_TestCase
which is used with PHPUnit. Without getting into too much detail, Testbench will make things a lot easier for us.

For example, we can write a
getEnvironmentSetUp($app)
function, and set our app's configuration variables like I did in the example. Now I can access my Stripe testing API key in my CI environment when I call
Config::get('services.stripe.secret');
in StripePortal.php.

Otherwise, how would we set these configurations in our CI environment? It just makes it so easy.

(getenv('STRIPE_SECRET)' gets the key from our phpunit.xml configuration file, which I explain in the PHPUnit configuration section a few lines below.)

You could also set database configuration variables, like this:

$app['config']->set('database.default', 'testpackage');
$app['config']->set('database.connections.testpackage', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);


Moving on to the tests:

The first test,
testExpectedGetCurrency()
calls our
getCurrent()
function and expects it to match the string
'usd'
. This might seem a bit silly for a test, and it is, but I could dynamically integrate different currencies for international users as I continue to develop the package. This could be a useful test to check that only correct currencies get returned. It also gives me something to write about ;).

The next test purposefully calls
chargeStripeCustomer()
without a credit card because it checks to make sure that the right exception gets thrown --
$this->setExpectedException('\InvalidArgumentException', 'Missing credit card information');


PHPUnit configuration

One more quick step before we can move on to creating our first build in CI.

Now that we have our PHPUnit tests, lets make sure PHPUnit can find them. Create a phpunit.xml file in your package's root directory if you haven't already.

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false">
<testsuites>
<testsuite name="Package Test Suite">
<directory suffix=".php">./tests/<directory>
</testsuite>
</testsuites>
<php>
<env name="STRIPE_SECRET" value="your_stripe_secret_test_key_here" />
</php>
</phpunit>


Most of these are self explanatory, but take a look at
bootstrap="vendor/autoload.php"
,
testsuites
and
<env name="STRIPE_SECRET" ... />


Setting the bootstrap path is what loads the generated composer autoload file, which makes our life so easy.

Testsuites tells PHPUnit where to find our tests.

Env sets our STRIPE_SECRET key that we access using
getenv('STRIPE_SECRET')
in our test file. This is nice because now you don't have to hardcode a key that could change.

Continuous Integration

We're now ready to push our files to our repo and trigger a CI build. This step depends on what service you are using, but they all have similar steps.

If you're on GitHub, you can use a lot of services out there like CircleCI and Codeship. Here's a little bit more on getting started with these two tools. If you're on GitLab, you can host your own or use MagnumCI. GitLab also hosts public ones, but otherwise choices are quite limited unfortunately...

In any case, set up the right web hooks to start a build as soon as you push.

You're either done at this point, or almost there. Depending on the service you are using, you may have to install dependencies like curl or mcrypt. How do you know? Either check your service's documentation, or push to your repo and see if the build succeeds. If it doesn't, it will tell you it is missing an extension and you can add it.

Sometimes you can add it online via the service's interface, but I recommend adding it in your .yml file in your package's directory. For example, mine looks like this:

.magnum.yml
before_install:
- sudo apt-get update -y
- sudo apt-get install -y php5-curl
- sudo apt-get install -y php5-mcrypt


Your file name varies depending on the service you are using.

You can also do things post-install, as well as other modifications. You can also set up Continuous Delivery which pushes code out for you if tests succeed. If they fail, the deploy gets blocked until someone fixes errors.

Conclusion

This type of development is something I wish I'd started earlier, because it makes life easier. Instead of having to manually start tests, all you have to do is push to your repo and you can go take a stroll around town. It also makes collaboration with others easier, since everyone can use the same files and it's impossible to forget to run tests. If someone pushed buggy code, you know exactly who pushed it and when.

Happy testing!