Testing is becoming an essential keyword and toolkit for developers and development teams who seek to architect and implement successful and performant websites. Thanks to the unprecedented growth in automated testing tools and continuous integration (CI) solutions for all manner of web projects, testing is now table stakes for any implementation. That said, many developers find automated testing to be an altogether intimidating area of exploration. Fortunately, when paired with a development culture that values quality assurance (QA), you can focus on adding business value instead of fixing issues day in and day out.
Three years ago, Yuriy Gerasimov (Senior Back-End Engineer at Tag1 Consulting) gave a talk at DrupalCon New Orleans about some of the key ideas that Drupal developers need to understand in order to implement robust testing infrastructures and to foster a testing-oriented development culture that yields unforeseen business dividends across a range of projects. In this four-part blog series, we summarize some of the most important conclusions from Yuriy’s talk. And in this third installment, we’ll take a closer look at two of the most essential parts of any testing toolkit: unit testing and functional testing.
Unit testing is a particularly fascinating topic, not only because many developers already know what user testing entails, but also because it enjoys a range of possible technologies that are readily available and that development teams can easily leverage. After all, unit testing is a commonly taught concept in universities. In its most reduced form, unit testing refers to the verification of a feature’s robustness by feeding it a variety of arguments, all of which test the limits of what is possible within the feature. Drupal developers, for instance, have access to a variety of unit tests for both core and contributed modules. And the best part of unit testing is that you can test functions and classes in isolation rather than evaluating large parts of the code.
The most optimal locations for unit tests are functions that are responsible for calculating some result. For instance, if you have a function that receives a set of arguments and that performs a series of calculations based on those arguments, you can feed unit tests arguments that evaluate whether the function works for a variety of possible inputs.
Unit tests as documentation
This also reveals one of the most interesting characteristics of unit tests. Because unit tests are focused on stretching the outer limits of what functions are capable of, they are also a convenient source of robust documentation. Yuriy admits that when he downloads external libraries for the first time in order to integrate them with his existing code, he often scrutinizes the unit tests, because they indicate what the code expects as input. When documentation is challenging to read or nonexistent, unit tests can be an ideal replacement for developers seeking to comprehend the purpose of certain code, because the unit tests are where developers ensure that logic operates correctly.
As an illustration of how unit tests can yield outsized benefits when it comes to well-documented code, consider the case of Drupal’s regular expressions. In Drupal 7, the regular expressions responsible for parsing .info files, which are used across both modules and themes, are extremely lengthy owing to the myriad demands on the files. Regular expressions, after all, are easy to write but difficult to understand. Though we are privileged as Drupal developers in that Drupal breaks regular expressions into separate, assiduously commented lines, many developers in the contributed ecosystem will avoid taking this additional step. For this reason, Yuriy recommends that all developers write unit tests for regular expressions that parse critical input.
Writing testable code is difficult but useful
Unit tests help developers think in a very different way about how to write software in a legible and understandable way. In Drupal 8, for instance, the new dependency injection container is particularly well-suited for unit tests, because we can swap services in and out of it. If you work with a database as an external service, for example, you can easily mock objects and ensure that your services work for those mock objects successfully.
To illustrate this, Yuriy cites the real-world example of the Services module in Drupal 7, which originally consisted of classical Drupal code but was subsequently coupled with unit tests. Yuriy witnessed through his addition of unit tests that he was able to parse different arguments entering certain Services functionality or inspect how routing functioned. With unit tests, ensuring the security of functions is much easier, because they can help you determine all of the arguments necessary to the code so that a call to globals or static variables is rendered unnecessary. Though unit testing requires considerable effort, Drupal 8 makes the process much easier for developers.
And now, with the introduction of PHPUnit into Drupal testing infrastructures, it is now even easier to test your code. The biggest part that was added is the capability to use mock objects that are presently the industry standard for unit testing.
As the name suggests, functional testing analyzes how actual users will interact with your software from a concrete rather than abstract standpoint. If you have an unlimited budget for your project, you can test every single last page on your website, but this is seldom—if ever—feasible from the perspective of project budgets. Whereas unit testing only requires perhaps ten to twenty percent of your development time, functional tests will require several days to write and noticeably more effort to implement.
Selling functional testing
In many of Yuriy’s previous projects, he was able to sell functional testing to customers by justifying the need to ensure that the software would be functional irrespective of the time of day or night. His teams had particular success selling functional testing in commerce projects, because stakeholders in commerce implementations are strongly invested in consumers successfully checking out at all times.
Often, the most challenging aspect of functional testing in commerce projects is the credit card details that customers must inevitably provide to the commerce platform. If you have a testing environment with all possible payment providers, however, customers no longer need to check to make sure that the checkout functions properly while they are at home or at the office. Your clients can simply click a single button in the continuous integration (CI) server and witness for themselves that the user flow is functional or configure notifications so that they are issued solely when that process breaks.
Maintenance costs of functional testing
Functional testing requires considerable maintenance, because it is primarily based on the document object model (DOM) of your website rather than abstract code. If you modify something on the website, you will need to revise your functional tests accordingly, including the selectors that you are targeting with functional tests. Yuriy warns that many may operate under the misconception that functional tests only require a mere twelve hours of implementation, but due to the unique attributes of each project, functional tests may require several days to implement.
As such, automating the process of functional testing is of paramount importance. Yuriy suggests that functional tests should not be made difficult for developers and especially project managers who are concerned about project quality. And as with other types of tests, if functional tests are not run on a regular basis, they are of limited usefulness.
Tools for functional testing
Luckily, there are many tools available for functional testing, the most notable among them being Behat. The Behat ecosystem makes a variety of extensions available, but it is by no means the only functional testing solution available in the landscape. Other solutions exist in the software-as-a-service (SaaS) space, including Selenium, which records clicks and provides a more visual representation of functional tests. For this reason, Yuriy recommends Selenium for junior developers with less exposure to functional testing.
Nevertheless, functional testing can fall victim to the vagaries of project budgets, and prospective expense remains among the most important considerations for customers interested in functional testing. To account for this, it is essential to consider which user flows are the most critical to the success of your implementation and to provide the appropriate level of coverage for those components. It is also a good idea to discuss at length with your customer what outcomes they would like to see and to ensure that the user flows they consider most crucial enjoy adequate coverage.
Thanks to the dual innovations engendered by unit testing and functional testing, you can have both an abstract and concrete view into how your code is performing in terms of quality with little to no overhead. With unit testing, which ensures that each designated function works properly with a variety of inputs that stretch the limits of what it can do, you can protect yourself from potential security vulnerabilities such as a lack of input sanitization. Functional testing, meanwhile, allows you to perform automated trials of your implementation to guarantee that your users are traversing the experience you intend.
In this blog post, we explored the boundaries of what unit testing and functional testing can offer you when it comes to a modern testing infrastructure and test-oriented development culture. In the fourth and final installment of this four-part blog series, we turn our attention to two of the areas of testing experiencing some of the most innovation as of late: visual regression testing and performance testing. As demands on our implementations shift, it is increasingly more important that we understand not only when changes impact user experiences but also how they perform under realistic loads.