Software testing is crucial to ensure that the system acts as expected. However, when dealing with complex systems, it can be challenging to test components in isolation since they rely on external systems like databases, APIs, or services. This is where mocking is used, a technique that employs test doubles. Mocking allows developers to simulate the behavior of real objects within a test environment, isolating the components they want to test. This blog post explains what mocking is, how it is applied in unit testing, the categories of test doubles, best practices, and more.
What is Mocking?
Mocking is the act of creating test doubles, which are copies of real objects that behave in pre-defined ways. These fake doubles stand in for actual objects or services and allow the developers to isolate chunks of code. Mocking also allows them to simulate edge cases, mistakes, or specific situations that could be hard to replicate in the real world. For instance, instead of conversing with a database, a developer can use a mock object that mimics database returns. This offers greater control of the testing environment, increases testing speed, and allows finding issues early.
Knowing Test Doubles
It is important to know test doubles to completely comprehend mocking. Test doubles are mock objects that replace actual components of the system for the purpose of testing. Test doubles share the same interface with the actual object but act in a controlled fashion. There are different types of test doubles:
Mocks: Mocks are pre-initialized objects that carry expectations for being called. Mocks are used to force particular interactions, i.e., function calls with specified arguments, to occur while the test runs. When interactions are not up to expectations, then the test would fail.
Stubs: Stubs do not care about interactions. They simply provide pre-defined responses to method calls so that the test can just go ahead and not worry about the actual component behavior.
Fakes: They are more evolved test doubles with smaller implementations of real components. For example, an in-memory database simulating a live database can be used as a fake to speed up testing without relying on external systems.
Spies: Spies are similar to mocks but are employed to log interactions against an object. You can verify the spy after a test to ensure that the expected methods were invoked with the correct parameters. Unlike mocks, spies will not make the test fail if the interactions are unexpected.
The Role of Mocking in Unit Testing
Unit testing is testing individual pieces, such as functions or methods, in isolation. But most pieces rely on external services, such as databases or APIs. These dependencies add complexity, unpredictability, and outside factors that can get in the way of testing.
Mocking enables developers to test the unit under test in isolation by substituting external dependencies with controlled, fake objects. This ensures that any problems encountered during the test are a result of the code being tested, not the external systems it depends on.
Mocking also makes it easy to test edge cases and error conditions. For example, you can use a mock object to throw an exception or return a given value, so you can see how your code handles these situations. In addition, mocking makes tests faster because it avoids the overhead of invoking real systems like databases or APIs.
Mocking Frameworks: Mockito and Beyond
Various mocking libraries are utilized by programmers in order to craft and manipulate the mocks for unit testing. Among the most commonly used libraries used in the Java community is Mockito. Mockito makes it easy for one to write mock objects, specify their behavior, and confirm interactions in an easy-to-read manner.
Highlights of Mockito include:
Behavior Verification: One can assert that certain methods were called with the right arguments.
Stubbing: Mockito allows you to define return values for mock methods so that various scenarios can be tested.
Argument Matchers: It provides flexible argument matchers for verifying method calls with a range of values.
Other than Mockito, other libraries like JMock, EasyMock, and JUnit 5 can also be used. For Python developers, the unittest.mock module is utilized. In the.NET ecosystem, libraries like Moq and NSubstitute are commonly used. For JavaScript, Sinon.js is the go-to library for mocking, stubbing, and spying.
Best Practices in Mocking
As terrific as mocking is, there is a best-practice way of doing it and having meaningful, sustainable tests. Here are a few rules of thumb to bear in mind:
Mock Only What You Own: Mock only entities you own, such as classes or methods that you have created. Mocking third-party APIs or external dependencies will lead to brittle tests, which will be broken when outer dependencies change.
Keep Mocks Simple: Don’t overcomplicate mocks with too many configurations or behaviors. Simple mocks are more maintainable and understandable.
Avoid Over-Mocking: Over-mocking your tests can make them too implementation-focused. Mock only what’s required for the test, and use real objects when possible.
Assert Behavior, Not Implementation: Tests must assert the system’s behavior is right, not how the system implements the behavior. Focus on asserting the right methods with the right arguments are called, rather than making assertions about how the system works internally.
Use Mocks to Isolate Tests: Use mocks to isolate tests from slow or flaky external dependencies like databases or networks. This results in faster and more deterministic tests.
Clear Teardown and Setup: Ensure that the mocks are created before each test and destroyed thereafter. This results in tests that are repeatable and don’t produce any side effects.
Conclusion
Mocking is an immensely valuable software testing strategy that provides developers with a way to segregate and test separate components isolated from outside dependencies. Through the use of test doubles like mocks, stubs, fakes, and spies, programmers are able to fake out actual conditions, test on the boundary, and make their tests more reliable and quicker. Good practices must be followed, like mock only what you own, keep mocks as plain as possible, and assert behavior and not implementation. Applied in the right way, mocking is a great friend in creating robust, stable, and quality software.
Personal Reflection
I find mocking to be an interesting approach that enables specific and effective testing. In this class, Quality Assurance & Testing, I’ve gained insight into how crucial it is to isolate the units being tested in order to check their functionality in real-world settings. Precisely, I’ve understood how beneficial mocking can be in unit testing in order to enable the isolation of certain interactions and edge cases.
I also believe that, as developers, we tend to over-test or rely too heavily on mocks, especially when working with complex systems. Reflecting back on my own experience, I will keep in mind that getting the balance right, mocking when strictly required and testing behavior, not implementation, is the key to writing meaningful and sustainable tests. This approach helps us ensure that the code is useful and also adjustable when it encounters future changes, which is, after all, what any well-designed testing system is hoping for.
Reference:
What is Mocking? An Introduction to Test Doubles by GeeksforGeeks, Available at: https://www.geeksforgeeks.org/mocking-an-introduction-to-test-doubles/.
From the blog Maria Delia by Maria Delia and used with permission of the author. All other rights reserved by the author.