Khanh Hoang - Kenn
Kenn is a user experience designer and front end developer who enjoys creating beautiful and usable web and mobile experiences.
PHP is a language that traditionally has not had unit testing as part of the development process. A large part of that was that way back when, there just weren’t any good tools for doing testing. Another major factor is that many of the learning resources for PHP do not mention doing things like unit tests, which is a major contrast to languages like Ruby. I personally think this has helped lead to the notion that PHP code is much harder to maintain than other languages.
PHP is nearly 20 years old now, and today we have the tools to do proper unit and functional testing without having to go through the pain of writing those kind of systems ourselves. At The Brick Factory, we tend to use the Drupal CMS quite a bit, and thankfully Drupal has a proper testing system built right in.
Unit Testing allows you to make sure that your code works as you expect it to. Let’s say you have a class that calls an API and returns customer information. If you always expect to get an array, you should test situations and make sure that you always get an array back. You don’t want your code to rely on that array and break all of a sudden when a boolean is returned, or a string is returned. By testing your code you are better able to say that your code works.
On the flip side, when you do discover a bug you can reproduce it in a test, and then once that test passes you know that you fixed the bug. Does that API you are calling sometimes error out when you don’t put dashes in the phone number? Well, you can reproduce that response and have your code handle it properly, and document that the bug is fixed with your tests.
The other nice benefit of unit testing code is that you make sure your code is not directly dependent upon any other code. To make unit testing easier the chunks of code should be separated out so that you can quickly mock up interfaces you aren’t testing. In our API example, we would have another object that generates fake API results for us to test since we are testing how we handle responses, not the actual API itself. If we decide to change the service we are using down the road, we can quickly swap out the API since it isn’t tightly coupled to our code.
To properly test something, you need to write your tests, watch them fail, fix the failures, and then watch them pass. The reason for this is your tests set up the expectations for how you think your code will work, and then you need to make the code work against those expectations. If you build your tests after you already do your code you tend to ‘fix’ your tests instead of fixing your code when you find problems. In PHP you have to bootstrap your tests a bit and actually build classes and methods as you start to write your tests since PHP tends to not like classes that don’t exist.
There are a few different ways that you can run your tests. At the Brick Factory we tend to use Drupal for many of our projects, and Drupal ships with an implementation of SimpleTest. SimpleTest is a web-based test runner, and the Drupal module provides a way for modules to register tests with SimpleTest and then run them from inside Drupal. SimpleTest also works with drush, so you can run your unit tests through the drush command line program.
When building a module for Drupal you can include a mymodule.test
file that the SimpleTest runner will launch to run your tests. You add a class inside this file that contains your tests and SimpleTest takes care of the rest.
Writing a test is actually really easy (writing testable code is an entirely different issue though). To create a test, you need to create a test file. Let’s create a module that generates a block, and this block will output a list of repositories for a user on Github, a popular code hosting site. We’ll need to build something to talk to the Github API, and something to parse the results. Our module will be called tbf_github
. Let’s look at building that second part.
The first thing we will need to do is create our test suite. A test suite is a class that contains a series of tests, and for Drupal it will either extend DrupalWebTestCase for functional tests, or DrupalUnitTestCase for unit tests. It will also need to need to implement the getInfo() method, which is a static method that returns some information about the test itself. Let’s build our first test suite by creating the file tbf_guthub.test
inside of our module directory, and adding the code below.
class TbfGithubParserTestCase extends DrupalUnitTestCase { static public function getInfo() { return array( 'name' => 'TBF Github Parser Unit Tests', 'description' => 'Unit tests for the Github parser', 'group' => 'tbfgithub' ); } public function testGetRepoNames() { } }
I mentioned that there are two different classes that extend your own test suite from. This is because Drupal allows you to do Functional as well as Unit Testing. The big difference is that Functional testing via DrupalWebTestCase bootstraps Drupal entirely. This allows you to do things like log in users, access things in the database, or render pages to test output. The downside is that functional tests are slow because Drupal is completely torn down, reconstructed, and invoked every test. It is very resource intensive.
Unit tests do not do anything like that. They strictly test the code you need, but that means you don’t have access to the database or users or anything like that. That’s OK! Unit testing shouldn’t rely on multiple systems.
When we created the test suite, we added a testGetRepoNames()
method to our class. Tests in SimpeTest are methods that begin with ‘test’ for the method name, so you can also have extra methods that aren’t run automatically. Let’s write the first test, which should test to see if we get back the repo names we are expecting.
Before we do that, we need to create a skeleton for our class. PHP doesn’t like if if you call a class that doesn’t exist, so let’s create the class for our parser and stub out a method for parsing repos.
// In file Tbf_GithubParser.php class Tbf_GithubParser { function parseRepos($response) { } } // Include our class require_once 'Tbf_GithubParser.php class TbfGithubParserTestCase extends DrupalUnitTestCase { // ... public function testGetRepoInfo() { // Get a stored response from Github so we don't have to hit the real API $response = file_get_contents('_tests/response_repo.json'); $githubParser = new Tbf_GithubParser(); $repos = $githubParser->parseRepos($response); $this->assertEquals(3, count($repos), 'Should have had 3 repos but ended up with '.count($repos)); $this->assertEquals('myrepo', $repos[0]['name'], 'First repo should be named "myrepo" but it was named '.$repos[0]['name']); } }
If we run this test right now, it will fail. parseRepos()
doesn’t return anything, and that’s OK. The big point here is that now we know this doesn’t work, so let’s fix it.
// In file Tbf_GithubParser.php class Tbf_GithubParser { function parseRepos($response) { return json_decode($response, true); } }
Now if we run the test, it should pass if our $response
JSON is correct. Since we’re using a canned response from a file we can make sure that the response has three repos and the first one has the correct name of ‘myrepo’.
That’s it. That’s unit testing as well as Test Driven Development.
One thing you will notice with the code is that the our parser doesn’t work directly with Github and just parses the result. Our code still doesn’t talk with Github. There’s a reason for that, and it’s called Separation of Concerns. Our parser shouldn’t interact with Github directly, just parse the results. Our parser shouldn’t care if we use curl, or streams, or whatever to get the response. It’s something else’s job to interact with Github.
By the same token, whatever object actually talks to Github shouldn’t care about the output. It should deal with talking with Github and just returning the response. By splitting processing out of the API interaction we can manipulate the responses any way we want. In our case, we’re turning the JSON into an array that we can get data from.
Properly separating out your responsibilities makes it much easier to test. In our test we didn’t need to actually interact with the Github API so our test was very quick. We’re also safe because of the API is down, our tests would break, or our tests will break if a repo we are testing is deleted. By keeping these systems separate we can make sure that our code still works. This is different than Functional testing, where we do want to test multiple systems at once to see if they are working together properly.
I glossed over this in the previous section, but in Drupal you can run tests two ways. One way is through the web test runner, and the other is from the command line using drush.
To run the tests via the web, go to the administration, Configuration, and then ‘Testing 2.x’. You will see a list of test groups to run. Just put a check next to the tests you want to run. Click ‘Run Test’ and everything will start to work.
The tests will run, and you will get a nice little readout about their status. Once all the tests are done, you can see which ones failed and fix them. If you have functional tests that extend DrupalWebTestCase these will take longer to run. Failures are red, warnings are yellow, and green is passed. You want everything green.
To run tests with drush, drop to a command line and run:
$ drush test-run --uri=http://mysite.local
and replace the –uri portion with the URL of your site. This will list all of the tests that are available in your site, and you can run them with:
$ drush test-run --uri=http://mysite.local NameOfTest
of you can use –all instead of a test name to run everything. This isn’t much faster than running it through the web interface, and it is a bit harder to read, but you can sometimes seem more information than what the web runner does. It also doesn’t break as easily as the web test runner.
With a little bit of time invested up front, we can automatically know at any point if our code is working as we expect. It provides us with better structured code that can be extended quicker, and when we find bugs we can identify them quicker. Knowing that changes can quickly be tested against a larger codebase with the click of a button helps developers have confidence that their code is working.
Drupal makes it easy to get going with testing by providing various interfaces for writing and running your tests. Next time you need to write module code, take a look at making unit testing part of that process.