Mocks, When You Should Not Use It? (虚拟对象,什么情况下不应该使用它?)
Under the circumstance of Test Driven Development (TDD), mocks play very important roles, because mock objects make it very easy to simulated the object behaviors of the other objects that one specific software unit dependents on. This is particularly useful when the real implementation of the dependent classes is not implemented, or hard to be used for the unit test. Furthermore, mock objects isolate the unit under test from the real implementation, which make it easy to debug issues if any unit test failed.
There’re many types of mocks, such as mock class, fake class, stubs etc. Each type of those mocks is essentially different, and would be used for different purpose. However, I don’t want to discuss the difference between those mock classes(see Mocks Aren't Stubs) in detail here, what I’d like to talk about is when you should not use mocks in the development.
In order to facilitate the discussion, I would like to divide the problem space from two dimensions: the main part of the development, and the determinacy of those parts.
- Main parts of software deliverables: in an object-oriented world, the main parts to be developed by every software engineer would be interfaces and implementations. Where an interface defines the public services of a software unit provided to its’ external world, and its’ implementation contains the detailed programming logic of each of the public services as well as its’ private methods.
- Determinacy: software development is hard, it is because many of the things in the problem domain as well as in the solution domain are not fixed, so for each of the part, it is either determined or undermined.
The problem space could be divided into below four blocks according above two dimensions:
- Interface and implementation are determined, this is in the top right block, where both the interface and implementation are very clear and fixed. In this area, the software tests, be it unit test or integration test, should use the actual implementation as more as possible. Mocks should be used only when the real implementation can not be used, therefore is very limited.
- Interface determined while implementation is not. This is the case in bottom right block, where the interfaces are defined clearly, however, the implementation is not ready yet. This is the very typical scenario of TDD where mocks are used a lot to isolate the real implementation in order to test the functionality of the software unit under test (SUUT).
- Interface and implementation are not determined. This is shown in bottom left block, where neither interface nor implementation are fixed. In this case, mocks should not be used at all, or in other words, if mocks are used for this case, the program will have big trouble.
- Interface is undermined but implementation is determined. This is the case that implementation is fixed while interface is not, as shown in top left block. In my opinion, this is the case that rarely happened in software engineering, because it would be very hard to understand how to implement a software unit, if the interface is not defined.
So, based on this four-block analysis, we can conclude below principles of using mocks:
- Mocks should only be used for the scenarios that interfaces are defined but implementations are not.
- Mocks should be limited when both interfaces and implementation are very clear.
- Mocks should not be used when interfaces are not yet clearly defined.
The third principle suggests that we should not use mocks if the interface are not determined, and in this case, the most important thing is to define the interfaces of the software unit.
Then the question is, what if people want to use mocks in their development, even if the interfaces are not yet determined?
Before moving to answer this question, it may be worthy to review when this case could occur, based on the analysis of a typical software development process.
At the earlier phase of a development program, the software system will be broken into several subsystems, and then further broken into software units which are dependent on each other, and then those software units will be owned by individual people or teams for them to start their work. As pointed out by Conway’s Law, the structure of a software system will eventually be designed as the structure of the team, or how the teams communicate with each other determines how the software unit (and subsystem) talk to each other. For the sake of easy discussion, let’s assume there’re two software units (unit A, unit B) that are owned by two people from two different team (team A, team B), see below diagram:
In the very ideal situation, software unit A should communicate with software unit B through the interface which defines how those two software units communicate with each other. And in order to have the two software units communicate with each other, the two person who owns each individual software units should talk to each other at the very beginning.
But in some cases, the two people who own those software units may not want to communicate with each other, especially when the team is new and everybody is new to each other. Then what would be the easiest way, if team need to move forward? Using the mocks is the answer! See below diagram:
- Software unit A will communicate with mock object A
- Mock object B will be used to simulate software unit A to communicate with software unit B
- Team member A will not talk with team member B
The good expectation using this approach is that each team won’t be depend on each other, because they can use mocks, so that each software unit could be developed in parallel. But is that true? The assumptions behind this approach are quite interesting:
- Interface between software unit A and mock object A is correct;
- Interface between software unit A and mock object A will be implemented by software unit B;
- Interface between mock object B and software unit B is correct;
- Interface between mock object B and software unit B will be implemented by software unit A;
Are those assumption with this approach is always correct? Probably not, I would say, unfortunately, most likely, none of those assumption will be true. But before moving on the discussion of what could be the result, let’s take a look at it from the team perspective.
Every team be developed through four different phases, i.e. forming, storming, norming and performing, there’re some team development modes add another phase, which is adjourning, and then call it as five stages team development model. But for our discussion, we’ll take the four stages model, which is shown in below picture.
To explain each of those stages in detail may take some time, but basically what we can learn are:
- Every team has to go through those four stages, not matter you’re willing to do it or not.
- The storming stage may look scaring, because there might be a lot of challenges, conflicts, different opinions generated, and it may make people feel uncomfortable.
- If team want to move to the performing phase earlier, they should start to experience each phase earlier, the earlier the better.
For a new setup team, every body are new to each other, they have different background, different believes, different way of thinking, and may talk in different languages and live at different city in different countries, all those may be the reasons that people don’t want to communicate with each other, and avoid conflict may be the human nature. However, if people avoid those challenges, they can only avoid those for a time, because conflicts will eventually jump in, they’re inevitable, but deferred. Therefore, the storming stage is deferred, and the time the team takes to become mature and starts to perform becomes very long.
Going back to the technical discussion, assume that different teams continue their development in parallel using mocks, when should they get rid of those mocks? that’s when they start integration, which puts different software units together and see whether they work or not. I promised you, that those software units won’t work, they won’t communicate with each correctly, because all the assumptions with this approach is wrong, and team will pay huge efforts to redo all those activities that they tried to avoid at the beginning and get all the issues fixed, which includes:
- Start to communicate with each other on the interface definition
- Fix the interface
- Replace mocks with real interfaces
- Test whether the interfaces work or not, fix all issues
- Test the implementations and fix all the issues
Everything seems familiar? Everything should actually happen before first line of code was written at the very beginning, except the activity of “replace mocks with real interfaces” which should not be existing, if all those activities occurred at the beginning phase.
Integration is typically performed at later phase of the program, especially the system level integration, and at this time, the team may move to the second stage which is storming stage, because people start to formally communicate with each other, and there would be lots of conflicts. One the other hand, team members are exhausted with removing mocks, debugging and fixing bugs in the interfaces and real implementation.
Those two factors add together, there would be only one result, without any exception: delayed program, bursting bugs and frustrated team.
Fortunately, the way to avoid this disaster is also very simple:
- Never use mocks in the development when the interfaces are not clearly defined
- Encourage team members to communicate with each other, at the first day when they start the program
This way, you may experience the pain of storming earlier, but you will also enjoy the fruit of high quality of outcome being delivered on time because the team is able to perform earlier.
Reiterate the principle as the close to this article: do not use mocks, when you don’t have defined interfaces.