Retrofitting Tests into a ReactJS Codebases without tests

Last updated Feb 1, 2025 Published Dec 28, 2024

The content here is under the Attribution 4.0 International (CC BY 4.0) license

Testing software and make it easy to do so has been an interest of mine for a few years now, this interest is now more focused on frontend applications that used to be. More specifically for reactjs applications. Its been a few months now that I am diving into a five to ten years old reactjs code that is prividing me insights and chalenges.

I personally have been developing reactjs code for a few years now, one of the first open source projects I have shared is called testable, it was released in 2020 or so and since then I have dedicated part of my learning path to reactjs.

ReactJs patterns

Around the same time I was diving into reactjs world, I published a study that used google search to find the common reactjs patterns used by developers.

Another interesting point is that I have published a broader strategy to work on testing for legacy applications.

Recently I have been working on other open source projects focusing on the testability part of things, such as json-tool, and text-tool, both applications are open source and are deployed on snapcraft. In addition to those, I frequently run experiments in a reposirory called reactjs-playground it is the place I experiment with reactjs features and dedicate my learning hours to it.

The experience I have got in those years in close sourced projects and open source projects gave me a foundation that allows me to identify some common pitfals and advantages.

Pitfals

Developers that join the reactjs pattern and tooling realize that the building blocks that the library offers is easy to understand and to compose. The most abstract concept of a component can be used to compose user interfaces quickly. However, it can also be a source of mistakes when it comes to structure a components hierarchy. The decomposing of systems is a subject that has been studied for years [1].

Big components

The size of a unity of abstraction in software has been a subject of a debate as well, some argue that methods and classes should have a small number of lines, others prefer to have a bigger well structured piece. As in any software project regardles of the size I intent to prefer the size that provides more context and produce less friction on the cognitive load while reading the code.

Coincidence or not, in my experience I find this possible when I have well set boundaries for the piece of code I am working on along with the business context. I haven’t yet found the magic number for that, however, my measurement became the number of jumps I have to do between files to understand what I need to. As much jumps I have to do, more I need to hold on my head the context and more information, this becomes hard while maintaining the code base.

Decomposing those components is a challenge, however it needs to ba taken into account for a better maintanence of the code base.

Global state

For professional development, the global state is a basic requirement, for reactjs applications it is no different. The global state is easily spotted by users of the application. If you are buying something and you add the product in your cart you can see the number of items you have added, this is the global state.

In reactjs applications, the once widely used global state management package redux has now decreased its usage in favor of smaller contexts around boundaries in the application.

Do you need redux?

The community has shared different opinions on the need to use redux for global state management, this led some blogs on the subject:

Despite push back from the community, react-redux which is a library used to bind redux to reactjs components has it adoption growing in the last five years according to npm trends. In February 01, 2025 the downloads shown in the npm for redux is 6,752,764.

If you are wondering what is the alternative to that then, the answer is reactjs context and hooks with queries.

For applications that are dependent on the redux package, it is a challenge in itself to get away with it. The global state is one of the dependencies that decrease the testability of the application. In my experience, the required global context often comes with increase complexity of the domain knowledge. While you might want to test just a particular component or a slice of you application, you wont be able without sharing the global dependencies that this slice requires.

Advantages

Adopting reactjs for enterprise applications as the time of the writing of this piece has been a correct decision for maintainability (despite of the critial moment that facebook had when changed the library licensing model). Reactjs provides retrocompatibility for apis that are not more being used, which makes the long term adoptin one of the key advantages.

Contexts

As if components are ot enough as an bstract concept to get around, contexts are also one of the advantages that reactjs provides in regards to testing. I elaborate more on this subject in a blog post dedicated to reactjs context testing.

The level of encapsulation that reactjs context provides is one of the key benefits for retrofiting tests and enabling refactoring in reactjs code bases. As the decomposition of components and business logic are a challenge, contexts are the tool that enable a better restructuring of the code.

In the testability part of things, this is also an advantage as it is a mechanism that enable the reduced number of test-double used, thus, leading to a more testable code.

Looking for learning materials about reactjs?

I have dedicated a space for the learning resources I have used to get a grasp of reactjs and to dive deep into the components world.

Retrofiting tests

Given such list so far the testing part hopefully has become clear that depends on how the source code is structure and how the boundaries of the application have been organized. However, as much as the testing code depends on the production code, the test-doubles can be used to help on delimiting the scope. The thought process for retrofting tests is composed of a few simple steps.

The approach described here is similar to what is described as Characterization test.

Characterization tests are used to capture the current behavior of a system. They help ensure that any changes made t the system do not introduce unintended side effects. By writing tests that describe the existing behavior, developers can confidently refactor or modify the code, knowing that the tests will catch any deviations from the expected behavior [2]

The advent of LLMs can also provide insights while writing the first tests and retrofiting code bases without any tests. Each step in this strategy is meant to be iterative, it is possible to combine different styles of TDD as well. The proposed approach is not to stop doing everything and retrofit all possible test cases and all possible issues that the code base has, it is a puzzle game. Each functionality tested is a piece that fit into the puzzle.

The same approach is suggested for refactoring. Developers should be able to constant refactor a piece of code. It is not a different project and it is not aimed at only dedicated for it. Its main goal is to make the code base better than it was. Iterativelly. There are a few aspects that can be used to retrofit tests in code bases that have none:

  • Fixing a bug - This is the opportunity to get exposed to the code and to understand how it behaves. Fixing the bug is part of the issue, however, this is also a oportunity to caracterize the application at hand.
  • Implementing a new functionality - This is the same as fixing a bug however the approach taken shifts a bit. It is the same approach as Kent Beck one suggested: “Make the change easy, then make the easy change”. A new functionality is a place that exposes the code base for making a change before introducing the new feature.

At the heart of this approach is the learning process, every step for learning the code base is taken into account.

LLMs

Copilot can be used to automate the first exploratory test described in the previous section, following three rules it can getting started with identifiying dependencies and writing a set of comprehensive tests to be used as a base.

Let’s take Testable as an example, it is an application that was written more than five years ago in reactjs and uses enzyme for testing. For today’s standard, the library that took over is vitest or jest along with testing library. To retrofit the test cases for testable and taking advantage of LLMs we can use copilot to help us on the heavy lifting.

Testable is composed of a gamified experience that has state, levels and user progression through different challenges. The component described in the following image is the component used to show dialogs and navigate forward in the history of the experience. Using copilot for vscode, I asked it to write a test with the production code we are interested at:

Asking copilot to write a test case

I am purposelly with the file I am interested at to write the test case for me, once it loads the question and analyse the code, it provides the answer.

Copilot answer on the test case

Along with the answer, copilot noticed that I am not using yet testing library and it suggests me to do that, and after that the test case generated is shown. Running the test case as it is shows makes the test not to pass and marks it as red.

Failing test from generate code

This point is important, because it shows that additional setup that need to take into account and copilot couldn’t figure it out, and they are as follows:

  • The import is not correct, it needs fixing to point to the correct file
  • setup for content is not correct, from the file was not possible to make the content to be valid for the test case

The flow described earlier in this section applies here as well, the feedback loop now is to start fixing those issues run the tests until the TDD cycle is completed.

References

  1. [1]D. L. Parnas, “On the criteria to be used in decomposing systems into modules,” Communications of the ACM, vol. 15, no. 12, pp. 1053–1058, 1972.
  2. [2]M. Feathers, Working effectively with legacy code. Prentice Hall Professional, 2004.

Resources

You also might like