The Art of Mocking in TDD

Best practices, drawbacks, and benefits for data mocking in TDD

Vidura Senevirathne
Bits and Pieces

--

Test-Driven Development (TDD) allows developers to reduce the number of errors and bugs in an application by ensuring that the code meets the requirements of the experiments and works as expected.

Mocking is a technique that is often used in TDD to isolate the code being tested from its dependencies. By using mock objects in place of production data, developers can test the code’s functionality without affecting the real data.

So, in this article, I will discuss the important things you need to know before mocking data for your tests.

Types of Mock Objects

As for the first step of data mocking, you must understand the types of mock objects available. There are 5 main mock object types:

  • Fakes: Fake objects are simple implementations of the actual objects they are replacing. But they do not have the same functionality as the real objects. They are typically used to stand in for real objects when they are not yet available or when their behavior is irrelevant to the test being run.
  • Dummies: Dummy objects are used as placeholders in a test. But they are not used or exercised by the test. They are often used to satisfy method signatures or other requirements of the code being tested.
  • Stubs: Stub objects provide predetermined responses to method calls during a test. They are mostly used to simulate the behavior of real objects when they are not yet available or when their behavior is not relevant to the test being run.
  • Spies: Spy objects record their interactions with the code being tested. They can be used to verify that the code is interacting with the mock object in the usual way or to capture the arguments passed to the mock object for later analysis.
  • Mocks: Mocks are another type of mock object used to verify the behavior of the tested code. They can be configured with specific expectations for how they should be used and can be used to confirm that the code being tested is interacting with the mock object in a standard way.

Overall, the choice of which type of mock object to be used will depend on the specific needs of the test being run and the goals of the testing effort.

Best Practices for Mocking in TDD

1. Create simple constructors

When writing unit tests, creating simple constructors for production classes is important to make it easier to create mock objects. Mock objects are used to ensure fast execution and to avoid external dependencies during unit tests.

However, suppose production classes have constructors that perform complex tasks, such as file system I/O, creating threads, or throwing exceptions. In that case, it becomes difficult to create mock objects because the mock object subclass must also perform these tasks or throw l same exceptions.

To avoid this issue, it is recommended to create constructors that only set fields based on input parameters or perform simple construction and to use a factory class to perform more complicated actions. This will make it easier to create mock objects and test the production class and improve the codebase’s overall design.

2. Create mock helper objects that throw runtime exceptions

Creating mock data for large classes or interfaces is a challenging task. You need to cover each function to ensure all the functionalities work as expected. The recommended way to address such a situation is to create a special mock object for the interface, or class with implementations for each method that throws a runtime exception.

Then when you want to test, you can create a new mock object based on this special mock object and only give it the methods that you want to test. This helps you to ensure that you didn’t forget to test any methods and avoid any errors that might happen when you create the mock object. Even if you forgot to write a test for a method, you could easily identify the mistake from the runtime exception.

3. Use Immutable Objects

Immutable objects are very useful in data mocking since they do not change state during the course of a test. This makes it predictable and easy to test, as the same inputs will always produce the same outputs. Furthermore, immutable objects make it easier to debug test failures, as you can see what the object’s state should have been.

However, we should not always use immutable objects for mocking since there is a trade-off to consider. Creating and managing immutable objects can be more challenging than normal objects, and you should ensure that objects do not need to be changed during the test to use immutable objects.

4. Avoid tight coupling

You should always try to avoid strong dependencies between the test and the mock data since it will make the test inflexible and difficult to maintain. Furthermore, it can lead developers to write many unnecessary code lines related to mock data within the test rather than the test itself.

To avoid this problem, you must design tests with minimum knowledge of the data used — mocking types like stubs or mocks is ideal for this purpose. This is something that should be baked into your application’s architecture. Toolchains such as Bit force you to think in independent components, where each component is designed to serve a single purpose.

5. Reset mock objects between tests

If you do not reset the mock data between tests, it will directly affect the next test’s outcome, resulting in unexpected behavior and test failures.

For instance, if you create a new user with the same attributes in 2 consecutive tests, the second test can fail due to data duplication if you do not clear the data before the second test starts. It can also lead to test dependencies, where the outcome of one test depends on the state of the mock objects from a previous trial.

You can avoid these issues by resetting mock data between tests. You can either use a mock framework or manually reset their state in the code. It will ensure that each test case is isolated and runs correctly, improving the reliability and maintainability of the test suite.

Benefits of Using Mocking in TDD

Here are some of the major benefits you can get by using mocking in TDD.

  • Faster test execution: Mock objects are usually simpler and lightweight than real data objects. This means that tests that use mock objects can execute faster than tests that depend on real data.
  • Early testing: With data mocking, developers can write and run tests for their code early in the development process, even before the application parts have been implemented. This helps to catch defects early on and ensure that the application is developed according to the requirements.
  • Improved reliability and repeatability: Mocking eliminates the need to rely on external resources that may be unavailable or behave differently on different runs.
  • Isolated testing: Mocking allows developers to test their code in isolation from the rest of the system. This behavior is especially useful when the code being tested depends on other components or external resources that are not yet implemented or are challenging to set up for testing.
  • Improved code design: The process of designing mock objects for a system can help developers identify and address potential design issues early in the development process. This can lead to a more modular and maintainable design for the system.

Challenges in Mocking in TDD

Although mocking has many benefits, it is not a silver bullet. There are several challenges in using mocking in TDD:

  • Mocking external dependencies: Mocking dependencies can be a challenging task when there are many or complex dependencies.
  • Maintaining mock objects: Mocked objects need to be updated as the code changes, which can be time-consuming and error-prone.
  • Mocking private methods: This can be difficult because private methods are not accessible from outside the class.
  • Mocking performance: Mocking can negatively impact the performance of tests if many mock objects are used. This can make tests run slower and take significant time to complete a test suite.

Popular JavaScript Mocking Libraries

There are several mock frameworks and libraries available for JavaScript; some of the popular ones are:

  • Sinon.js Sinon.js is a standalone library that provides various types of mock objects, such as spy, stub, and mocks. It also provides utility functions for creating fake HTTP responses and timers. Sinon.js has more than 3.7 million weekly NPM downloads and 9.2K+ GitHub stars.
  • Jest Jest is a full-featured testing framework with a built-in mocking library that allows you to create mocks and stubs for functions, modules, and even entire APIs. Jest has more than 19 million weekly NPM downloads and 41.1K+ GitHub stars.
  • Enzyme Enzyme is a testing utility for React applications that provides various functions for simulating events and rendering components. It also has a mocking feature that allows you to create mocks for components and functions. Enzyme has more than 2.2 million weekly NPM downloads and 20K+ GitHub stars.
  • Cypress Cypress is an end-to-end testing framework that allows you to test your application’s UI and APIs. It has a built-in mocking library that allows you to create mocks and stubs for HTTP requests and responses. Cypress has more than 4.6 million weekly NPM downloads and 42.3K+ GitHub stars.
  • Mockery — Mockery is a mock framework that allows you to create mocks for functions, modules, and even entire APIs. It provides a simple API for creating and configuring mocks, and it is compatible with various testing frameworks, such as Jest, Mocha, and Jasmine. Mockery has more than 200K weekly NPM downloads and 1.1K+ GitHub stars.

Conclusion

Mock objects are becoming increasingly essential for software testing as systems and architectures become more complex. This article discussed an overview of data mocking in TDD, including mock object types, best practices, benefits, and drawbacks. I hope this article will help you in mocking data for TDD. Thank you for reading.

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--