Testing for the Brave and True: Part One
This is the second part of a series of blog posts about automated testing for Drupal. Its mission is to take you from zero testing experience to confidence in testing your custom Drupal work, from the ground up. Last time, in Testing for the Brave and True: Part Zero we defined exactly what automated testing is and discussed some of the common vocabulary of testing. It also introduced the two primary tools used by the Drupal community to test their work, PHPUnit and Behat.
Why Automated Testing Will Save You Time and Treasure
Now that we know a little about what automated testing is, I'm going to make the case that it can be a net positive to your everyday workflow and actually make you a better programmer.
If you came to this blog post with the idea that you've never done any testing or that you've tried to test and didn't succeed, you'd be wrong. Every developer tests their code. Some developers just throw that work away.
Consider what you're doing every time you clear cache and go refresh your browser. You're testing your work. You've made some change to your code and now you're asserting that your work functions as you expect. Perhaps you put a
kint() in your new code to inspect some part of your code or a variable, or maybe you're using XDebug (if not, I'd encourage you to start) to step through your code. This process is testing.
While these informal tests can be incredibly valuable, you can't commit them; you can't run them the next day and you cannot run all the tests you've ever written with just one command. Writing automated tests is simply writing code that can do some of that testing for you. It's making those informal tests you already do, explicit and formalized.
Context, context, context
Whenever you write code to do specific things, you make assumptions. Assumptions are the foundation of abstraction and abstraction is the foundation of progress. You can't write code that does anything useful without making assumptions. Especially in Drupal. Entities themselves are an abstraction writ large. But, wrong or hidden assumptions are also the root of most bugs.
Therefore, when we write code, we ought to be very aware of the assumptions we make. We ought to record those assumptions in some way, for future maintainers or simply to help us remember that we made them in the first place. Unfortunately, when we only do informal testing, we bake our wrong assumptions into our code without leaving a record of them. We can't re-assert our assumptions later without digging through code or comments or taking the time to figure out what something was actually supposed to do.
This is the first place where formal tests can be a boon to you, future you, and your successors. The act of writing formal, automated tests by its very nature is recording your assumptions for posterity. When you return to your code an hour, day, week, or year later, all the assumptions you made can be tested again. If you have a strange if/else in your code because of some edge case you discovered when you were doing your initial development, a test can ensure that that code isn't deleted when you're cleaning up or refactoring later (at least without explicitly deleting the test).
In short, you make your assumptions explicit. This reduces the cognitive burden of "getting back up to speed" whenever you need to come back to some piece of code.
This is where I first really fell in love with testing. Having formal tests for the code I was working with gave me confidence as I made changes. That can sound strange to someone who's never tested before. It sounded strange to me, too.
The confidence I'm talking about is not the confidence I have in my abilities (Lord knows I could learn a little more about being humble), it's my confidence in the codebase itself and the relative safety I have when I incorporate a change.
If you've ever been in an old, large, legacy codebase, you might recognize that feeling of mild anxiety when you've made a change and there's just no feasible way to know if you've broken something else in some obscure place of "the beast". The only thing you can do is click around and cross your fingers. This is where a well-tested codebase can create real confidence. Having a suite of automated tests means I can isolate my changes and then run all the tests ever written for that codebase and ensure that my changes haven't broken something, somewhere.
Better tests, better code
If you've been interested in the art of programming itself (and I think you must be to be reading this), then you might have heard of the SOLID design principles. Or, at least, things like "write small functions" and "do one thing and one thing well." Maybe you've heard about "dependency injection," "separation of concerns," or "encapsulation." All these words are names for the concepts that, when applied to the way we write code, make the likelihood of our code being robust, flexible, extensible, and maintainable (all good things, right?) go up.
The art and practice of testing itself can help you apply all of these concepts to your code. If you recall the term "unit testing" from the last post in this series, I said, "[unit] tests isolate very small bits of functionality." The process of identifying the one small thing that your code achieves in order to test it, helps you apply the Single Responsibility Principle. Put another way, when your tests become large and unwieldy, they're saying to you, "this code does too much and it should be refactored."
When you're testing code that has dependencies on other code or configuration, like access to the database, another service, or some credentials, it can become difficult to write useful tests. For example, if you're writing code that runs an entity query and you'd like to test how the code works when there are no results, five results or 500 results, you would have a hard time doing so with a real entity query and database connection. This is where "inversion of dependencies" or "dependency injection" come into play. Instead of running an entity query and doing processing on the results all in one function or within a single class, pass the entity query or its results into the function, method or class. This allows you to test the function with fake results, which you can then set up in your test (we'll go over the methods for doing exactly that in a later part of this series).
That inability to test code with implicit dependencies is a good thing™—it forces you to do dependency injection, whereas it's simply a ritual that you have to practice without tests (I should note, the reason inversion of dependencies is a good thing™ is because it makes your code modular and helps ensure it only "does one thing well").
I hope I've made a convincing case that writing automated tests for Drupal can save you time and treasure. In the next part of this series, we're going to begin our descent into the art of testing itself. We'll go over writing our first unit test and getting it running on the command line. Until then, feel free to comment or tweet @gabesullice if you've got questions!