D8FTW: Unit testing for realsies
Code testing: It's like exercise. It's good for you, but most people really don't want to do it. Unless you develop a habit for it and feel the benefit, most people will try to avoid it. And if it's too hard to do it becomes easy to avoid.
That's really unfortunate, as good testing can have a huge improvement on the quality of a system and, over time, even improve how fast a system can be developed because you spend less time finding and fighting old bugs and more time building things right the first time.
Drupal 7 introduced testing to the Drupal world for the first time, but not really unit testing. Unit testing, specifically, is testing one "unit" of a larger system in isolation. If you can verify that one unit works as designed you can confirm that it doesn't have a bug and move on to finding bugs elsewhere. If your tests are automated, you can get notified that you have a bug early, before you even commit code.
The problem with Drupal 7 is that it doesn't really have units. Drupal's historically procedural codebase has made isolating specific portions of code to test extremely difficult, meaning that to test one small bit of functionality we've needed to automate installing the entirety of Drupal, create for-reals content, test that a button works, and then tear the whole thing down again. That is horribly inefficient as well as quite challenging to do at times, especially given the poor home-grown testing framework Drupal 7 ships with. As a result, many many developers simply don't bother. It's too hard, so it's easier to avoid.
Now fast forward to Drupal 8 and we find the situation has changed:
- That historically procedural codebase is now largely object-oriented, which means every class forms a natural "unit" candidate. (Class and "testable unit" are not always synonymous, but it's a good approximation.)
- Because most of those classes leverage their constructors properly to pass in dependencies rather than calling out to them we can easily change what objects are passed for testing purposes.
- In addition to Simpletest, Drupal now includes the PHP-industry standard PHPUnit testing framework.
All of that adds up to the ability to write automated tests for Drupal in less time, that run faster, are more effective, and are, in short, actually worth doing.
Smallest testable unit
Let's try and test the custom breadcrumb builder we wrote back in episode 1 (The Phantom Service). Here it is again, for reference:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">// newsbreadcrumb/src/NewsBreadcrumbBuilder.php<br></span><span style="color: #007700">namespace </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">newsbreadcrumb</span><span style="color: #007700">;<br>use </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Core</span><span style="color: #007700">\</span><span style="color: #0000BB">Breadcrumb</span><span style="color: #007700">\</span><span style="color: #0000BB">BreadcrumbBuilderBase</span><span style="color: #007700">;<p>class </p></span><span style="color: #0000BB">NewsBreadcrumbBuilder </span><span style="color: #007700">extends </span><span style="color: #0000BB">BreadcrumbBuilderBase </span><span style="color: #007700">{<br> </span><span style="color: #FF8000">/**<br> * {@inheritdoc}<br> */<br> </span><span style="color: #007700">public function </span><span style="color: #0000BB">applies</span><span style="color: #007700">(array </span><span style="color: #0000BB">$attributes</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">$attributes</span><span style="color: #007700">[</span><span style="color: #DD0000">'_route'</span><span style="color: #007700">] == </span><span style="color: #DD0000">'node_page'</span><span style="color: #007700">) {<br> return </span><span style="color: #0000BB">$attributes</span><span style="color: #007700">[</span><span style="color: #DD0000">'node'</span><span style="color: #007700">]-></span><span style="color: #0000BB">bundle</span><span style="color: #007700">() == </span><span style="color: #DD0000">'news'</span><span style="color: #007700">;<br> }<br> }<p> </p></span><span style="color: #FF8000">/**<br> * {@inheritdoc}<br> */<br> </span><span style="color: #007700">public function </span><span style="color: #0000BB">build</span><span style="color: #007700">(array </span><span style="color: #0000BB">$attributes</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$breadcrumb</span><span style="color: #007700">[] = </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">l</span><span style="color: #007700">(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Home'</span><span style="color: #007700">), </span><span style="color: #0000BB">NULL</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$breadcrumb</span><span style="color: #007700">[] = </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">l</span><span style="color: #007700">(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'News'</span><span style="color: #007700">), </span><span style="color: #DD0000">'news'</span><span style="color: #007700">);<br> return </span><span style="color: #0000BB">$breadcrumb</span><span style="color: #007700">;<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
We want to test that the logic in this class works as expected but without testing the entirety of Drupal, or even the entirety of the breadcrumb system if we can avoid it. We just want to test this code. Can we? With good OOP design and PHPUnit, we can!
First, let's create a new testing class in our newsbreadcrumb module. This class does not go in the same directory as our code itself. Instead, it goes in a separate "tests" directory but follows the same file/name pattern.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">// newsbreadcrumb/tests/src/NewsBreadcrumbBuilderTest.php<br></span><span style="color: #007700">namespace </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">newsbreadcrumb</span><span style="color: #007700">\</span><span style="color: #0000BB">Tests</span><span style="color: #007700">;<p>use </p></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Tests</span><span style="color: #007700">\</span><span style="color: #0000BB">UnitTestCase</span><span style="color: #007700">;<p>use </p></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">newsbreadcrumb</span><span style="color: #007700">\</span><span style="color: #0000BB">NewsBreadcrumbBuilder</span><span style="color: #007700">;<p></p></span><span style="color: #FF8000">/**<br> * Tests the News breadcrumb builder.<br> */<br></span><span style="color: #007700">class </span><span style="color: #0000BB">NewsBreadcrumbBuilderTest </span><span style="color: #007700">extends </span><span style="color: #0000BB">UnitTestCase </span><span style="color: #007700">{<p> public static function </p></span><span style="color: #0000BB">getInfo</span><span style="color: #007700">() {<br> return array(<br> </span><span style="color: #DD0000">'name' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'News breadcrumb tests'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'description' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Tests the news breadcrumbs'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'group' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'News breadcrumb'</span><span style="color: #007700">,<br> );<br> }<p>}<br></p></span><span style="color: #0000BB">?></span></span>
It should look fairly familiar for anyone who's written a Drupal 7 test. The main difference is that we're extending UnitTestCase
, which is a very thin extension of PHPUnit's main base class for tests. Now we can define some test methods. As in Drupal 7, a test method is one that begins with test
. Rather than assume we have a complete Drupal install and making actual HTTP calls against it, though, we'll just test our class directly.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #007700">public function </span><span style="color: #0000BB">testApplicablePage</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$node_stub </span><span style="color: #007700">= </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">getMockBuilder</span><span style="color: #007700">(</span><span style="color: #DD0000">'\Drupal\node\Entity\Node'</span><span style="color: #007700">)<br> -></span><span style="color: #0000BB">disableOriginalConstructor</span><span style="color: #007700">()<br> -></span><span style="color: #0000BB">getMock</span><span style="color: #007700">();<br> </span><span style="color: #0000BB">$node_stub</span><span style="color: #007700">-></span><span style="color: #0000BB">expects</span><span style="color: #007700">(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">once</span><span style="color: #007700">())<br> -></span><span style="color: #0000BB">method</span><span style="color: #007700">(</span><span style="color: #DD0000">'bundle'</span><span style="color: #007700">)<br> -></span><span style="color: #0000BB">will</span><span style="color: #007700">(</span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">returnValue</span><span style="color: #007700">(</span><span style="color: #DD0000">'news'</span><span style="color: #007700">));<p> </p></span><span style="color: #0000BB">$attributes </span><span style="color: #007700">= [<br> </span><span style="color: #DD0000">'node' </span><span style="color: #007700">=> </span><span style="color: #0000BB">$node_stub</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'_route' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'node_page'</span><span style="color: #007700">,<br> ];<p> </p></span><span style="color: #0000BB">$builder </span><span style="color: #007700">= new </span><span style="color: #0000BB">NewsBreadcrumbBuilder</span><span style="color: #007700">();<p> </p></span></span>