I have said on several occasions that test-writing might eventually become a good way for new developers to contribute to the VuFind project. By increasing our test coverage (and thus future code stability), new tests are a valuable contribution to the software… and the act of studying all the code paths in order to write a test is a good way to learn how things work.
Of course, the reality of the situation is not so straightforward. There is still a lot of code in VuFind that is inherently hard to test, usually due to complex dependencies. At this point in time, the process of writing tests is often also the process of refactoring code to reduce coupling, which in turn makes it more testable — and this often requires a deeper understanding of the system than a newcomer is likely to have. (See my previous blog post for an example of the sort of refactoring I mean).
So testing isn’t always easy… but still, it can sometimes be straightforward. Here’s a relatively simple example to demonstrate some of the principles.
Getting Set Up
This post assumes you have a clean copy of VuFind 2 checked out somewhere separate from your production instance, and that you have the necessary tools (PHPUnit, Phing, etc.) installed. More background on the setup can be found in the VuFind wiki.
What We Are Testing
For this example, we are testing VuFind’s Cart view helper (VuFindViewHelperRootCart). This is a very trivial piece of glue code whose purpose is to make VuFind’s cart object (which keeps track of user selections in the optional “book bag” feature) available for use in view scripts. You don’t really gain much by writing a test for this class — it is unlikely to change much and it contains no complex logic — but since it is so simple, it’s a good candidate for this tutorial. We can get to 100% coverage very quickly.
You can view the full code of the view helper in our Git repository.
There are only two methods: the constructor (which takes a VuFindCart object as a mandatory parameter) and __invoke (a PHP magic method which returns the Cart object when the helper is invoked as if it were a function).
By convention, unit tests (i.e. all tests that exercise VuFind functionality without relying on an active test instance) reside in module/VuFind/tests/unit-tests/src. Within this directory, test classes are arranged and namespaced to correspond with the code that they test.
This means that we’re going to create a module/VuFind/tests/unit-tests/src/View/Helper/Root/CartTest.php, living in the VuFindTestViewHelperRoot namespace.
All PHPUnit test cases must be subclasses of the PHPUnit_Framework_TestCase class. VuFind includes some base test classes with additional convenience methods (see the VuFindTest namespace), but this particular test is simple enough that we can simply extend the default base class:
class CartTest extends PHPUnit_Framework_TestCase
Writing the Test
One of the keys to writing an effective test is to avoid doing any work that is not related to the task at hand. You don’t want your test to fail because of a problem in a different area of the code — this will make bugs harder to locate when something breaks.
We are trying to test something very simple here — essentially we want to be sure that when we put a cart object into the view helper’s constructor, we get the same object out when we invoke the helper.
This is where the “don’t do unrelated work” rule comes in. We could construct a real VuFindCart object for testing purposes, but then if there was a bug in the VuFindCart constructor, that might cause our test to fail, even though that has nothing at all to do with our view helper. Fortunately, we don’t have to construct a real VuFindCart object, thanks to PHPUnit’s mock object feature.
With mock objects, we can create objects that serve as placeholders for real classes in our code. They accept the same method calls and pass the same instanceof tests as real objects, but they don’t actually do anything — unless we configure them to expect particular incoming data or simulate specific responses under specific circumstances. These are a very valuable testing tool!
In our particular case, we don’t need to do anything fancy with mocks — we just need to call PHPUnit’s built-in $this->getMock() method to construct a fake Cart object.
There’s just one small issue. If you just call:
You will get an error. VuFindCart’s constructor expects a VuFindRecordLoader object, and we have to satisfy this dependency even when building a mock. Fortunately, the third parameter of getMock() accepts constructor parameters for the new mock object, and nothing stops us from creating a mock VuFindRecordLoader to satisfy the dependency. Thus, we end up with:
$cart = $this->getMock(
‘VuFindCart’, null, array($this->getMock(‘VuFindRecordLoader’))
Now that we have our fake cart, the rest is simple… Just construct a view helper:
$helper = new VuFindViewHelperRootCart($cart);
…and then test that invoke works by making an assertion that invoking the helper will return the same object that we passed to the constructor:
Assertions are the most important part of any test — these are what determine whether each test passes or fails. Never write a test without any assertions! PHPUnit includes a wide range of assertion methods, allowing you to express many different conditions.
Running the Test
Now that the code is written (full test class available here), it’s just a matter of using your VuFind test instance to run it, as described in the wiki.
After confirming that a new test passes on my local system, I push it to the Git master and then check the code coverage report in Jenkins after everything rebuilds. In this case, I’m now seeing 100% coverage for the cart helper.
I hope this has served as a helpful introduction to some fundamentals, but I realize that most real-life testing is significantly more complicated. I may try to write an article describing a more difficult test in the future if time permits. In the meantime, if you want to try your hand at test-writing, feel free to send me questions — I’ll be happy to recommend some areas that might be worth looking at, and I can help with any refactoring that may be necessary.