Model-based testing (MBT) has emerged as a powerful strategy for maintaining high standards of quality in an efficient and systematic manner. MBT transforms the development process by allowing teams to derive scenarios from models of the system under test. These...
While TDD is often treated as a monolith whose adoption constitutes a boolean choice, there are two specific and closely related challenges when it comes to the realization of TDD: mocking and dependency injection. Clarity around these aspects, including their costs and techniques for addressing them, gives development teams at least an opportunity to plan more accurately — and perhaps even practice a more rewarding variant of TDD.
Automation and integration
First, a few definitions. TDD is the practice of software coders writing executable tests before implementing requirements. A number of concepts, terms of art, tools, canons and other cultural items, including the “red, green, refactor” slogan, commonly accompany TDD.
As TDD enters at least its third decade, it also has controversies that have themselves become conventional: how to apply TDD to legacy work, whether TDD makes sense for GUIs, what TDD means for mobile or real-time apps, and so on. This article leaves all those subjects aside, as others have already covered them adequately. The focus here is entirely on the contrast between two “flavors” of computing: automation and integration.
Automation encompasses algorithms, like Euclid’s algorithm, the wavelet transform, and the traveling salesman problem. Automation generally looks more or less like scholastic mathematics, and TDD works great for automation. TDD is an appropriate way to express and validate edge cases and essentials and variations of automation. Algorithms and related automations constitute typical curricula of formal computing education, and they dominate the industry employment interviews of folklore.
Integration is intellectually far shallower. It’s mostly just recognition that information here is needed there, and flanging together a few parts to effect that move. TDD sometimes gives us only clumsy help with integration, and it often appears to be more trouble than it’s worth. That’s unfortunate, because a growing fraction of programming work looks like integration.
An example helps. Imagine a requirement for a particular function to retrieve a sales index from a database. There’s nothing deep about this; the database lookup is intended to be as simple as possible. An implementation might look like this:
def get_sales_index(): handle = connect(DATABASE, USER, PASSWORD) cursor = handle.cursor() cursor.execute("SELECT special_value FROM special_table") datum = cursor.fetchone()[0] handle.close() return datum
While this is Python, the corresponding implementations in Java, Ruby or JavaScript look quite similar. This is a good implementation in several ways: it’s brief, it doesn’t leak memory, it’s correct (at least to the extent that exception-handling hasn’t been specified), and it’s readable to those familiar with database culture.
It’s bad for TDD, though. TDD teaches that unit tests are small and self-contained. To test get_sales_index(), though, requires connection to DATABASE. DATABASE is outside this source. Potentially worse, DATABASE might be a live production database, to which tests or testers aren’t allowed to connect.
Solution?
Things rapidly become more complicated at this point. The usual treatments of TDD introduce mocks and dependency injection, and our original get_sales_index() becomes something like
def get_sales_index(): return get_sales_index_kernel(get_cursor(DATABASE, USER, PASSWORD)) def get_cursor(DATABASE, USER, PASSWORD): return connect(DATABASE, USER, PASSWORD).cursor() def get_sales_index_kernel(cursor): cursor.execute("SELECT special_value FROM special_table") datum = cursor.fetchone()[0] handle.close() return datum
Each of these three definitions admits at least the possibility of a realistic test, in principle. Notice the tests themselves, which this article omits in the interest of brevity, have become rather arcane, with dependence on mocks.
This example is entirely typical for what happens when testing integrations. Single entry points multiply into two to four linked methods, with associated mocks and dependency handles also proliferating in the associated test code. It’s easy to conclude that the cure is worse than the disease.
But we need some solution! More and more of day-to-day programming is integration, and it needs to be reliable.
Clarify processes
There is no single solution. The best we have, at least for now, is more clarity about how to make the most of TDD. Keep these tips in mind:
- Expect to test all your automations without mocks.
- Recognize that construction of mocks to test integrations can require more effort than writing the integrations themselves.
- Consider configuration of testing resources — a testing database, testing message queue, testing network, and so on — as mocks. It’s increasingly cheaper to bring up another database instance than to try to specify enough database behavior to define a useful database mock.
- Share mocks among tests. Define as few of them as necessary, but have those few be robust.
- At an architectural level, consider relaxing TDD’s usual tenets. Work out a good dependency injection style for the project, make sure all the coders understand it, and establish an explicit target that combines unit tests, acceptance tests, static code analysis and peer review to achieve high quality in the integration parts of the project.
- The real cost and benefit of mocking aren’t in first implementing functionality, but in maintaining it. If your mocks help maintain invariants during refactoring, you probably have good mocks. If, however, you find that you have to update the mocks each time you write new functionality in order to keep regression tests green and passing, then your mocks might be more trouble than they’re worth. It’s probably time to adjust your testing strategy in that specific area of integration.
Related Posts:
Model-Based Testing with Ranorex DesignWise
Model-based testing (MBT) has emerged as a powerful strategy for maintaining high standards of quality in an efficient and systematic manner. MBT transforms the development process by allowing teams to derive scenarios from models of the system under test. These...
What Is OCR (Optical Character Recognition)?
Optical character recognition technology (OCR), or text recognition, converts text images into a machine-readable format. In an age of growing need for efficient data extraction and analysis processes, OCR has helped organizations revolutionize how they process and...
Support Corner: API Testing and Simple POST Requests
Ranorex Studio is renowned for its robust no-code capabilities, which allow tests to be automated seamlessly across web, mobile, and desktop applications. Beyond its intuitive recording features, Ranorex Studio allows custom code module creation using C# or VB.NET,...