From chaos to order: harness the power of Mock Factories
Written by
Emil Litwiniec
Published on
February 29, 2024
TL;DR
In frontend development, proficient mock management is indispensable for effective testing. We'll delve into the pitfalls of poor mock handling and explore remedies to avoid chaos. Mocks play a vital role in simulating data and behaviors during testing. By mastering mock management, developers elevate the quality of their applications and testing procedures. We'll explore solutions like the Mock Factory pattern, which makes mock management easier and enhances application resilience during testing.
Oops! Something went wrong while submitting the form.
Share
Into the void
Our recent project experienced rapid growth, with numerous developers joining the effort, leading to a surge in the daily creation of new components and corresponding unit tests. Each test required mock data, and initially, the creation of mocks was treated as a minor concern, resulting in their definition from scratch for each test suite.
This approach seemed straightforward initially, given the dynamic and startup-like nature of the project's early stages, where practices were still evolving. However, this initial chaos led to a somewhat disorganized atmosphere, with a prevailing attitude of "we will figure it out later." And that's okay. The focus was rightly placed on addressing more critical aspects first. Minor issues could be deferred until there was ample resource capacity or until they began causing problems.
Soon, team members observed the proliferation of duplicated mocks throughout the project, prompting the idea to centralize them in a single location. Thus, creating the `mocks.ts` file was initiated, but its implementation fell short. While some mock objects were transferred there, they remained static, forcing developers to redefine mocks from scratch whenever specific edge cases required variations.
Unfortunately, the `mocks.ts` file grew into an unwieldy collection, encompassing mocks from various domains and teams. As single mock definitions expanded into multiple variants and larger datasets containing numerous entities, the file deviated from its original purpose, becoming a source of confusion rather than clarity.
It's important to note that all these mocks adhere to strict typing, derived from models generated from `openAPI`, which defines the contract between our front-end and back-end systems. Consequently, every mock must accurately reflect the current structure of the API to maintain consistency and ensure alignment between our application layers.
The approach to managing mocks was evidently flawed. While it served its purpose initially, its cumbersome nature became increasingly apparent as the codebase expanded and the API evolved. What once seemed functional and tolerable soon became unwieldy and challenging to maintain, highlighting the need for a more efficient solution.
When we requested the backend team to incorporate additional fields into our API response for a new feature, we anticipated a straightforward integration on the frontend. Adjusting a few lines seemed like a simple task. However, reality hit hard. Adding even a single new field to the model required modifications in nearly 100 files.
Moreover, while many of these files were unaffected by the new field, they still required adjustments to appease TypeScript errors. Each mock object, aligned with the modified model type, demanded manual updates for the new property—a time-consuming endeavor that nobody could afford. With the certainty of more API changes looming, we urgently needed to devise a swift and sustainable solution.
I'm beginning to see the light
Enter the Mock Factory pattern, our saving grace! In essence, it's a function designed to generate mock object based on a specified type. Easy to employ and highly adaptable, the Mock Factory simplifies the process of customizing mocks while ensuring a fresh object is returned with each invocation. Let's take a look at our mock factory creator.
Initialization couldn't be easier. Simply supply the type to the generic argument along with default data. You'll observe the use of `faker.js,` a tremendously helpful tool that furnishes meaningful content for our mocks. Moreover, it generates a unique ID each time, ensuring seamless integration with components that render data lists, for instance.
Utilizing the mock is fairly intuitive. If necessary, we can override it with static data to accommodate specific test cases. Because the `getAccountMock` function is associated with the `Account` type, any attempts to add overrides will be flagged by TypeScript if they deviate from the defined type structure, helping to prevent errors before they occur.
And it's getting brighter
In most scenarios, this mock factory serves its purpose effectively. It provides a fresh mock for each use, allowing for easy overrides with specific values. However, there's room for enhancement. Currently, the factory generates a shallow copy with overrides, preventing the modification of individual properties within nested objects without replacing the entire object.
We can optimize the utility by opting for a deep-copy approach to address this limitation. As an option in our utility, we can utilize `merge` from `lodash` for this purpose. Given the resource-intensive nature of deep copy operations, this improvement ensures its usage only when necessary.
Imagine we've added more details to the `availableBalance` in our `Account` type. Now, we can easily adjust only the `currency` without redoing the whole `availableBalance.` This might seem simple, but it's incredibly useful for dealing with more complex data structures nested several levels deep.
Let it shine
Implementing this pattern demanded significant effort, particularly considering the prevailing technical debt. Nonetheless, the investment yielded substantial benefits. Consider the previous scenario where an API alteration required manual updates across 100 files. This laborious task was minimized to just a few: the type declaration, the default object definition within the mock factory, and the components utilizing the new property.
Over time, the initial confusion disappeared, and the mocks embraced a structured organization. This not only streamlined the process but also facilitated swift access to mocks based on their API, obviating the need to retain them all within the notorious `mocks.ts.` I strongly recommend adopting this pattern early in a project. It not only enhances usability but also serves as documentation of your API, offering extra insights about your types.