Moving from Legacy Code without Tests to Test-Driven Development in React

Moving from Legacy Code without Tests to Test-Driven Development in React

Moving Towards Test-Driven Development: A Comprehensive Look at Refactoring in React

The Unavoidable Transition

Diving into the transition from unruly legacy code to Test-Driven Development (TDD) in React is no easy task. As I navigate this transformation, I'm taking a moment to reflect on my coding journey, realizing that my original understanding of TDD might have slipped through the cracks. While juggling a full-stack project with dual backends and a frontend, the stark absence of a single test became a glaring issue.

The journey to this point began with the creation of my wind forecast app in a casual playground environment. Initially, my focus was on experimentation, testing assumptions, and validating ideas—typical in the early stages. However, as the project evolved and my codebase grew, the oversight of neglecting tests in favor of rapid development became apparent.

Now, with all three apps deployed into production, the consequences of my approach are stark. Changes are pushed without the safety net of tests, leading to a reliance on browser checks and a lingering sense of uncertainty. The realization has dawned that adopting TDD is not just an option. For me it's a necessity.

As I commit to embracing TDD, a crucial question surfaces: What to do with the existing legacy codebase? Opting to apply TDD solely to new features seems impractical, leaving the core concepts vulnerable without test coverage. Thus, the decision to write tests for the legacy code becomes inevitable. With that in mind, lets go to work and make this happen.

Restructuring for a Test-First Approach

The first step in this transformation involves reorganizing the app's structure. A plethora of helper functions and an absence of hooks characterize the existing code. While a quick fix might be to create a test folder and start writing tests, a comprehensive approach demands a structural overhaul.

Legacy Folder Structure:

.
└── src
    ├── assets
    │   ├── icons
    │   │   ├── Back.svg
    │   │   └── ...
    │   └── maps
    │       ├── baseMap.json
    │       └── ...
    ├── components
    │   ├── Info.jsx
    │   └── ...
    ├── methods
    │   ├── convertImageToData.js
    │   └── ...
    ├── styles
    │   ├── App.css
    │   └── ...
    ├── App.jsx
    └── main.jsx

In my project, I recognized the need for a more organized file structure. Having a single 'components' folder containing numerous files was becoming unmanageable, particularly with the risk of mixing up test and source files. To address this, I explored various React file structuring approaches and adopted a hybrid strategy that aligns with my development style.

lagacy component folder

The key change involved dividing the 'components' folder into two separate entities: 'pages' and 'features.' The 'pages' folder houses components related to different pages of the application, while the 'features' folder contains subfolders dedicated to various features.

This restructuring was not just about aesthetics; it laid the foundation for future features. In anticipation of upcoming enhancements, I also refactored existing helper functions into custom hooks, streamlining the code and preparing the project for seamless integration of new features.

However, the transformation was not without challenges. Rewriting imports and making necessary adjustments were crucial steps to ensure the new structure integrated smoothly with the existing codebase. Despite the hurdles, the effort put into refining the React file structure, including the introduction of 'pages' and 'features' folders, has paved the way for a more scalable and maintainable project. Valuable lessons were learned throughout this process, contributing to a smoother development experience moving forward.

New Folder Structure:

.
└── src
    ├── assets
    │   ├── icons
    │   │   ├── Back.svg
    │   │   └── ...
    │   └── styles
    │       ├── App.css
    │       └── ...
    ├── data
    │   └── maps
    │       ├── baseMap.json
    │       └── ...
    ├── features
    │   ├── forecastOverview
    │   │   └── forecastOverview.jsx
    │   └── ...
    ├── hooks
    │   ├── useConvertImageToData.js
    │   └── ...
    ├── pages
    │   ├── Info.jsx
    │   └── ...
    ├── utils
    │   ├── checkNightTime.js
    │   └── ...
    ├── App.jsx
    └── main.jsx

Choosing the Right Testing Suite: A Pragmatic Approach

Selecting the appropriate testing suite is crucial. With an emphasis on simplicity and effectiveness, I've chosen Jest over other options like Mocha. Although I have prior experience with Mocha, I find Jest appealing because of its comprehensive features right out of the box. To get started, we simply install Jest using npm:

npm i jest --D

With Jest now installed as a development dependency, the testing framework is prepared to facilitate the transition to Test-Driven Development. It provides a solid foundation for testing React components and functionalities..

Optimizing Test File Organization: A Neat and Accessible Approach

To maintain a clean and accessible structure, I've decided to store test files alongside their corresponding source files. This one-to-one alignment ensures everything is in one place, promoting easy navigation. As the project expands, introducing subfolders within directories, such as "features" and "utils," will help manage potential clutter.

.
└── src
    ├── ...
    ├── features
    │   ├── forecastOverview
    │   │   ├── forecastOverview.jsx
    │   │   └── forecastOverview.test.js
    │   └── ...
    ├── hooks
    │   ├── useConvertImageToData.js
    │   ├── useConvertImageToData.test.js
    │   ├── useGenerateForecastArray.js
    │   ├── useGenerateForecastArray.test.js
    │   └── ...
    ├── pages
    │   ├── Forecast.jsx
    │   ├── Forecast.test.js
    │   ├── Info.jsx
    │   ├── Info.test.js
    │   └── ...
    ├── utils
    │   ├── checkNightTime.js
    │   ├── checkNightTime.test.js
    │   ├── getWindSpeed.js
    │   ├── getWindSpeed.test.js
    │   └── ...
    └── ...

Embracing the TDD Journey: From Pain to Progress

Transitioning to Test-Driven Development (TDD) was not without its challenges, but the meticulous process proved to be invaluable. By starting with tests for helper functions, progressing to hooks, and finally addressing pages, the step-by-step approach ensured comprehensive coverage.

The effort put into writing tests revealed hidden bugs, emphasizing the crucial importance of testing. By addressing these issues early on, potential headaches were avoided later, which highlights the significance of thorough testing.

TDD Unveiled: A Methodical Approach

In Test-Driven Development, tests are written before implementing features. By intentionally allowing tests to fail at first, developers create features that satisfy the defined tests. This iterative process, which includes testing, implementation, and refactoring, ensures a strong and thoroughly tested codebase.

The appeal of TDD lies in attaining near 100 percent test coverage. By starting with well-defined requirements, developers incrementally build features, promoting a systematic approach to development. Breaking tasks into manageable steps leads to testable code from the very beginning.

Looking Ahead: Embracing a Feature-Rich Future

Revamping old code isn't exactly a walk in the park, but committing to top-notch quality through thorough testing? Now, that's a total game-changer. Sure, writing tests upfront might take some time, but let's face it—the alternative, allowing problems to slip through to production, is a cost none of us want to deal with.

Here's the real deal: Test-Driven Development (TDD) isn't just about having stable code for today. It's about investing in a future where you can trust your existing codebase and introduce new changes without constantly worrying about things breaking.

And you know what? If you're disciplined enough, it's absolutely fine to write tests after each new feature. I mean, kudos to those self-disciplined folks—they're making sure tests get added consistently, building a robust and continuously tested codebase. Whether you're diving into TDD right from the get-go or weaving in tests after each feature, the end goal is the same: creating a future where you can rely on your codebase with confidence, knowing that changes won't turn everything upside down.

Thanks for joining me on this coding adventure! Your thoughts and feedback are invaluable, so feel free to share. Until next time, happy coding!

-Tobias

Did you find this article valuable?

Support Tobias Steinhagen by becoming a sponsor. Any amount is appreciated!