Today we're going to talk about something I know next to nothing about, but am working on.
Recently I came to the realization that it is time to "Code Up" and learn how to be proficient in building tests, and writing testable code. Testing is a discipline that you will be able to cross code boundaries, something you will be able to carry with you to any language, once you pick up the mindset. More importantly than that, testing allows you to verify your code is working without manually testing or worse, just hoping it works.
So now that we've touched on the why, let's talk about how.
If you're going to do it, do it right!
PHPUnit (by Sebastian Bergmann) is quite literally the gold standard of testing in PHP. It probably helps that Sebastian has taken code metrics in PHP from zero to hero. So it stands to reason that PHPUnit is where you start to look.
A lot of what I write are plugins, because I use things here, there, and everywhere. So one of the most important things to me, was that whatever I used had at least some plugin support. The two plugins that fit what I was looking for closely enough are sfPHPUnitPlugin and sfPHPUnit2Plugin. I chose sfPHPUnit2Plugin simply because I saw it had a github repository, which meant that contributing would be easy, and worst case scenario, keeping my customizations up to date with the core would be a snap.
Note: Just to clarify, both sfPHPUnitPlugin and sfPHPUnit2Plugin work with PHPUnit 3.5.x.
Long story short, I made a pull request on March 11th to add support for plugins holding on to phpunit tests, along with some other goodies. A little while later, it was incorporated! Hurrah! Power to the people. đŸ™‚
Anyway! For the purpose of this, we're going to assume you're using sfPHPUnit2Plugin, as that's what I use. It's easy. Install the plugin. Run ./symfony phpunit:generate-config, and then run phpunit. If you already have a phpunit.xml file in your root, copy phpunit.xml.dist over it, as it will be updated with the latest path set.
To make your first unit test, it's easiest to just use the plugin's tasks to build them, and work from there. This is a simple deal!
./symfony phpunit:generate-unit MyTestName |
That will set up the phpunit basics, and put your unit test in test/phpunit/unit/MyTestNameTest.php. See? EASY!
To run your test, either just run 'phpunit', or run 'phpunit test/phpunit/unit/MyTestNameTest.php'. EASY!
Now you know HOW to test, so let's look at WHAT to test. Because you can test everything but if you're not doing it in a way that makes sense, you're just going to get frustrated, and give up. Or worse, you won't realize you're testing the wrong things until disaster strikes.
JMather's Novice Testing Rules
1. Don't test things that are already being tested
Doctrine and Propel both have tons of tests to ensure they're working. You don't have to confirm that when you set something, it gets set, unless you are overriding the setter. You don't have to confirm that when you save, it is added to the database unless you are testing a database connection.
2. Test functionality, not objects
The most important thing to test is your business logic. Not every line deserves a test. Focus on complex business logic. Though, likely, you will end up testing objects if you build them to only contain specific domain logic, but short of that, test what matters. It's ten times better to have 50% code coverage testing all the really important logic than to have 20% code coverage because you're writing massive amounts of silly tests making sure that a + b = c.
3. Just start testing
Your first tests will be bad. No, scratch that, they will be horrible. Why? Because you'll be testing code that wasn't written to be tested. Want an example? Look at this little ditty from majaxDoctrineMediaPlugin. We'll do two clips of the same function, before and after.
And then the "work in progress"...
The first thing you will notice is that the new one is ~100 lines, as opposed to ~250. It is still quite long, but it has come a long way. The second is that it has been moved into it's own class. Why? So we can test it! All of the referenced builders (i.e. $this->path_builder, $this->filename_builder and the like) can be swapped out for mock objects with predictable behaviors so we can test this function in near isolation. Isolation is important because it limits your test's culpability for outside interference itself. Over time all of your pieces will be validated by tests, so you can ensure the entire system is "correct."
Take, for example, the majaxMediaFilenameBuilder. I tested it! Why? To make sure I can trust it! It should work as expected. Along the way to ensuring it worked as expected, I realized while the code worked fine, the implementation was hosed.
Now, why did I make it a separate class? It's just a filename you say? HAH! Fat chance! For many instances, sure, it's just a file name. But what if you wanted to make the filename harder to guess? Well, if that part of the code wasn't replaceable, you would be out of luck. Now you just have to extend majaxMediaFilenameBuilder, override render() to return md5(parent::render(args)).'.'.$extension; and you're golden! Don't you just love OOP?
3. Just keep working at it
1% code coverage becomes 5%, which becomes 10%, 20%, 40%, 80%. Soon enough, you'll find yourself checking your tests to make sure you haven't mucked anything up, and that's when you'll get it. That's when it will really hit home. It found a show-stopping bug you wouldn't have noticed in some other part of the system that was not really related at all to what you were working on. It saved your bacon.
And that's when it gets real.
Arlen
April 21, 2011 @ 10:47 pm
OK, tried it.
Stability is beta, so the install line needs –stability-beta, but no real problem.
Ran the generate-config without a hitch. Then I decided to test one of my simpler models, as a start for using this. I did:
./symfony phpunit:generate-unit Resource
and that’s when the problems started. I put the usual
$this->object = new Resource();
in setUp(), and it crashed with:
Doctrine_Manager_Exception: Unknown connection: doctrine
So what’s the secret to actually getting this plugin to work?
Jacob Mather
April 22, 2011 @ 7:08 am
Arlen,
I think the unit test bootstrap isn’t set up for database stuff, but I could be wrong.
I don’t actually test my doctrine objects yet (I’m working to make sure my business logic stays out of them, so it is easier to move to sf2 when I am ready).
I think, if you copy test/phpunit/bootstrap/unit.php to test/phpunit/bootsrrap/db-unit.php and then add the following to the bottom, it might work:
$databaseManager = new sfDatabaseManager($configuration);
Arlen
April 28, 2011 @ 10:54 pm
The problem is, the model is where the business logic is *supposed* to go, even in symfony2.
I’ll try that, next go-round. After I hit that error, I went back to github and pulled down the latest version of the phpunit plugin without the 2 and used it instead. While there were some bits of phpunit2plugin I liked, the other one ran with minimal effort, so I could get on with development.
Jacob Mather
May 2, 2011 @ 9:10 pm
You can put the business logic in the model, but then your tests can have a heavy cost to create to test.
I think it makes much more sense to build your business logic separately and then use the model as a data source or store. It keeps your test expense lightweight, and completely decouples you from the underlying storage solution.
Granted, I am not up on sf2 yet, and perhaps Doctrine 2 uses ‘dumb’ models which don’t actually rely on the underlying ORM. If so, then all the better!
Duncan
September 29, 2011 @ 2:47 am
Hi, I was looking for the solution to a symfony functional test issue when I found your post.
Can you help??
My problem is to click a stanard onclick confirm box which pops up on my webpage (onclick=”return confirm(‘Are you sure?’);). I include a piece of test code below. Thanks in advance!
// test the complete
click(‘Complete!’)->
!!! HOW DO I TEST FOR A JS CONFIRM BOX HERE
with(‘request’)->begin()->
isParameter(‘module’, ‘factory’)->
isParameter(‘action’, ‘complete’)->
Jacob Mather
September 29, 2011 @ 6:31 am
Hi Duncan,
I unfortunately don’t have much experience with the functional tests.
Are you sure JS even runs? I would assume that portion wouldn’t be testable in a functional test.