How TDD Mocking Can Enhance Test-Driven Development
TDD mocking allows your team to find bugs earlier which results in more efficient and proactive identification and remediation of potential issues.
Mocking is the creation of an object that mimics another and sets expectations concerning its behavior.
In testing, mocking replicates the behavior of a service, object, or process. There are a number of benefits that result from replacing an object with a mock for testing purposes.
There’s confusion regarding the differences in a mock, a fake, and a stub. In this article, we’ll take a brief look at each, and tighten the focus on mocking. Also, we’ll look at how mocking can improve TDD. Take a few minutes to consider how mocking can help your team improve code design and testing, and how to shift testing further to the left.
What is Mocking?
Mocking is an approach to testing isolation that simulates interactions among components and systems. Typically, the aim is to verify interface functionality and test for correct behavior.
Viewpoints differ over whether a mock must verify behavior. Fowler writes that mocks are “objects pre-programmed with expectations which form a specification of the calls they are expected to receive.” Pivotal views them as “self-verifying spies.” Both of these are valid perspectives. However, the main objective is early verification, and this is done through isolation.
Isolation is an important upstream testing activity. When testing a specific component or object, other objects might not be ready for exhaustive integration testing. Isolation separates the test subject by means of mocks, stubs, or fakes that simulate external dependencies.
Consider a common example: a developer is building an object that will interact with a database. In most scenarios, it’s entirely impracticable to insist that the actual database is a test dependency. The database might not be ready for quite some time, or security restrictions limit testing availability. The solution is to create an object that duplicates the interface and simulates database responses.
It’s not enough to isolate only from external dependencies. Good development practice is to arrange unit tests to focus on one object at a time. Though it’s not always practicable, it’s generally best to approach unit testing this way. Fixing problems is much easier when testing can distinguish all of the functionality of each object and components.
Another example: Imagine switching over to a new compiler version, which promptly breaks a number of tests. If each test corresponds to a single object, then it’s likely to be easy to fix those broken tests. Conversely, any tests that correspond to three or more objects will take much more time to repair.
Test doubles: stubs, fakes, and mocks
In testing automation, it’s common to use interactive test objects that appear to function the same as their production equivalents. These test objects are actually quite simple, and useful only for testing.
This approach reduces complexity. It’s much easier and faster to reach a point of verifying code independently from the rest of the system. A test double is the name for such test objects. Although there are many types of test doubles, the term mock is commonly used in reference to all of these types. However, it’s important to avoid misunderstanding and improper mixing of test double methods. Failure to do so can adversely affect test design and increase test fragility. Likely, a bad approach to implementation of test-doubles will inhibit code refactoring.
A fake is an object that has some level of internal function that is somewhat less than the corresponding production object. Commonly, a fake results from taking one or more shortcuts on a copy of production code.
An example of such a shortcut is an in-memory repository or data access object. A fake will not actually engage any database at all. Instead, it will employ a simple collection scheme to store test data. The main benefit here is the ability to perform an integration test of services. It’s unnecessary to initiate a database connection and submit time-consuming requests. The effort spent to create the fake will be repaid in simplicity and performance.
A stub is a test object containing predefined data. Some or all of this data is returned to calling objects during tests. This is useful for situations in which a developer cannot readily engage with objects that would respond with actual data.
An example: To respond to a method call, an object needs to grab some data from a database. Because the real object is not yet accessible, a stub is built that contains the data in some type of collection structure. The stub will access this internal data and return the data for the method.
Separating command and query method testing
In deciding what type of test double to use, it’s important to realize the need for two different types of methods: query methods and command methods.
Consider this development scenario: Instead of calling an academic database that retrieves actual student grades, a developer creates a stub that contains a set of student profiles along with semester grades for each. It’s only necessary for the developer to create enough representative data to meet all of the test conditions.
A query method returns a specific result and does not change system state. The developer in our current example might be testing a GPA method. This method returns the numerical average of all grades for a particular student. Such methods return a value or a data set and exhibit no other functionality or behavior. A stub returns a value or set and takes no other action.
Command methods don’t return a value
Another type of method is a command. A command performs one or more actions that change system state. Also, a command does not return a value. It’s always good practice to distinguish the methods of an object in two separate categories. This is known as a command-query separation.
In testing a query method, it’s often preferable to use a stub to verify all of the correct return values. Of course, it’s all also important to have a means to test a command method, such as a method that sends an e-mail message. What’s the best approach to testing a method that doesn’t return any value? For the answer, we turn to mocking.
A mock is an object that registers any call it receives. A mock is most useful when there is to be no return value—and no practicable means for checking change of state.
One broadly representative example of a need for a mock is a call to an email messaging service. It’s unnecessary to send an email message each time a test is run. Moreover, it may not be easy—or necessary—to verify the content of the email message. However, it is to verify that interactive functionality is working properly. In our example, this means simply verifying that the e-mail messaging service is called.
Using test mocks to enable TDD
Simply said, test-driven development (TDD) is a methodology in which developers write the tests first, which seems counter-intuitive because the tests will obviously fail. Then, the developers write just enough code to satisfy the requirements of those tests. When all of the code passes all of those tests, developers work to refactor their code to improve code quality.
The key advantages of TDD are:
- Developers are motivated to carefully think through all feasible test cases that will satisfy all functional and business requirements. Though this can be challenging, it prevents many bugs that would otherwise be found by QA staff (or users!).
- Developers work closely with business analysts and test engineers. This cultivates teamwork, improves implementation, and increase quality.
- Code refactoring is typically much easier
With TDD, the primary objective is to make code simple, clear, and self-testing. However, many teams discover—when they move into writing code—that a variety of dependencies and unfinished collaborators prevent tests from being written, much less passed..
Mocking can shift testing further left
Broadly, the two options are to mock—or not to mock.
- Avoid mocking — Put all of the code into the same class and achieve a pass on all tests. Then, refactor the code by decomposing the large class into constituent classes and methods. Then work really hard to ensure all tests pass.
- Employ mocking — Simplify and implement the main workflow by quickly constructing simple dependent classes. Ensure all tests pass. The approach here is to simply define the dependent classes without coding the internals. These classes serve as mocks for unit testing.
Mocking bests other strategies by verifying and simulating both state and behavior. These behaviors and integrations can be tested early—by writing a minimal amount of code. While it is true that using too much mocking is a “code smell’ - the process of molding your code to fit the mocks, rather than the mocks to fit your code - it is also true that far too much code goes untested. Using mocking properly, therefore, is a critical balance.
The use of mocks and other test doubles moves more testing further upstream in the development pipeline. Finding bugs earlier equates to eliminating them more easily—and less expensively. This makes your entire team more agile in their testing efforts.