[Example] test-first bug-fixing in JavaScript

[Example] test-first bug-fixing in JavaScript

Writing tests slows me down, let me just fix the issue

This is a sentence I often hear when advising people to start their bug-fixing by writing an automated test that illustrates the problem before changing any production code.

The kicker is: even though this approach might feel slower than just hacking away at the live code base and redeploying it a few times to see if you solved the issue, it usually ends up being faster.

This is especially true in environments where deployments take substantial amounts of time. As an example, let's consider a large enterprise environment with deployment pipelines that take 4-5 minutes to compile, repackage, validate, and complete the installation on multiple mirrored servers.

No alt text provided for this image


“Okay, you might have a point. How do I do this?”

Before I continue to explain my bug-tackling process, I first need to warn you: Do not apply this technique in a high-pressure environment without practicing it first. Many developers have made the mistake of trying a new technique or tool on the job without using it in a safe-to-fail environment first. Save yourself the anger of your team, boss, and customers, by trying new practices on your personal projects before using it in the wild.

Now, without further ado, let's dive into the nitty-gritty of resolving a bug by writing a specific test for it. In this article, I will share some code snippets from a very simple open-source project I have been working on for a while.


Code-Along Walkthrough

Project description

Docsify is a Node.js project that allows its users to generate static documentation websites from simple markdown files. It is designed to be easily extensible by installing plugins. Various plugins exist that allow users to mix-and-match functionality. This gets them a documentation site that fits their particular needs.

A while back, I was looking for a way to add a glossary (a fancy name for dictionary) to my personal documentation wiki. I stumbled across a plugin that mostly did what I wanted to achieve, but it soon turned out it did not really fit all of my requirements. So I did what any developer would do, and changed the code base. You can find my spin-off version of the plugin on GitHub.

No alt text provided for this image
The glossary plugin in action


A wild bug appears

I managed to extend the original plugin to fit my needs, including the option to configure an alternate location for the list of definitions that is used to replace words in all other pages. Unfortunately, I messed this up. The generated links still included the relative filesystem path to my dictionary file, resulting in the site navigation to go haywire.

No alt text provided for this image
The link to the glossary incorrectly contains the ' ./ ' characters


As shown in the screenshot above, the 'context' link points to:

https://localhost:3000/#/./X_Appendix/Glossary/HOME?id=context        

It should point to:

https://localhost:3000/#/X_Appendix/Glossary/HOME?id=context        


Hunting down the bug

Can we test it? Yes we can!

Let's start by writing a test that illustrates what we expect the link replacement to do, rather than what it is doing now. If done well, the test will fail until we solve the issue in our code.

it(' can handle relative link configurations', () => {
    let glossaryLocation = './alternate/location/glossary/README.md';
    config = glossifyConfig()
            .withGlossaryLocation(glossaryLocation)
            .withLinkPrefix('#')
            .build();

    dictionary = loadTerminology(sourceText, config);
    expect(dictionary['API']).toBeTruthy();

    let result = addLinks(
        textWithWordsToReplace,
        dictionary,
        config
    );
  let expectedLink = '/#/alternate/location/glossary/README\\?id=api';
  expect([...result.matchAll(expectedLink)]).toHaveLength(2);
});
        


The test first sets up a custom glossary definition location, using a relative file path. It loads these word definitions, and checks whether this went well. This little validation step after setting up our experiment is called precondition checking. Precondition checks ensure that our starting point is what we expect it to be. I do this as it makes no sense to evaluate the result if the set-up was wrong to begin with (junk in, junk out).

Next up, we let the code add links to the original text. You might notice that the variables `sourceText` and `textWithWordsToReplace` are not included in this test. The reason is that they are defined earlier in the test specification as a shared field, as you can see in the snippet below.


 
let dictionarySource;
let textWithWordsToReplace;
 
beforeEach(() => {
      dictionarySource = `
  ##### API
  
  Application Program Interface. Specifies a set of software functions that are made available to an application
  programmer. The API typically includes function names, the parameters that can be passed into each functions, and a
  description of the return values one can expect.
`;

textWithWordsToReplace = `
 # This is an API title
      
 This is a paragraph of text, explain stuff and mentioning the term API.
`;

});        

When we run this test specification, it should fail. Do not try to fix the code before making sure that your test accurately illustrates the problem.

No alt text provided for this image
The automated test fails as expected.

Now, to make sure our test is working correctly, we will quickly change the test to make it expect the incorrect link value (the one containing `./` in its path).


let expectedLink = './alternate/location/glossary/README\\?id=api';
expect([...result.matchAll(expectedLink)]).toHaveLength(2);        


If this makes the test run without failing, we are sure that it fails for the correct reason, and not because we made some mistake while writing it. Yes, I am in fact testing my test. Yes, this sounds silly. And yes, failing to do this has cost me countless hours of chasing ghost bugs in the past. Don't repeat my mistakes, you are smarter than that!


No alt text provided for this image
The test passes when we are asserting the WRONG behaviour.

Now, let's revert our specification back to what it should be, so our test fails again.

We are now ready to fix the bug! At this point, I usually do an intermediary commit to my git repository. This way, I do not lose my work in case my computer decides to malfunction at an inopportune moment.

Please note that it takes a lot longer to write this down and to read through my description of using the technique than it does to actually do it. Everything described up until now took me less than 5 minutes in real time.


Find and fix the code issue

Using my test to trigger the code execution, I can now make use of my code editor's built in debugger to step through the code and identify where the defect is. For the sake of maintaining some brevity to this article, I will not describe this in this article.

Soon, I found that these lines of code are the culprits.

let compiledLink = config.glossaryLocation.replace('.md', '');

let link = ` [$1](/${compiledLink}?id=${linkId})`;

let replacement = contentLine.replace(reComma, link + ',')
        .replace(re, link + ' ')
        .replace(reFullStop, link + '.');        


Now to fix the problem

“Past me” has neglected to take into account that the location of the glossary could be provided as a relative path. Let's add a sub-string replacement that gets rid of any occurrences of './' in our file path.

Our code now reads:

let compiledLink = config.glossaryLocation
        .replace('./', `${config.linkPrefix}/`)
        .replace('.md', '');        

You might now be thinking about edge cases where this would go wrong. Such as: “What if one of my directories end in a dot (full stop) ?”.

To which I say: “You are absolutely correct, but I can't be bothered to support weird directory naming fetishes. Don't use dots at the end of your directory names if you want to use my plugin.”.

Let's run our test and see if we actually managed to fix the problem!

No alt text provided for this image
The test passes!

Looks like we did a good job. Kudos to us!

But wait! Let's first make sure we did not accidentally ruin other functionality by making our changes. Luckily, this can easily be done by running all existing tests.


No alt text provided for this image

Now that we know we resolved the issue, we can test it on the live system to be extra sure everything works as intended. In general, when all of your automated tests succeed, you can be fairly sure that you did not break anything. But as we all know: “better to be safe, than sorry”. You do not want to be the person to single-handedly bring down your company's flagship product.


In a lot of real-life scenarios, the code you are working on will not be accompanied by a good set of tests. In that case, it can be tricky to know if your change had an unintended side effect. So be extra careful to check it yourself. Alternatively, there are ways to quickly create your own safety net by writing a few tactical automated tests (either manually, or with techniques such as 'approval testing' or 'snapshot validation').


Cleared for take-off!

We fixed the bug, so let us commit the code change and merge our change into the mainline branch (or create a Pull Request if you are working on a project that uses them).

We managed to do all that in about a quarter of an hour. Not too shabby, right?

Your turn to try!

I hope you enjoyed this write-up of how I tackled a small bug in one of my hobby projects using a test-first approach.

Now get out there, and try doing it yourself!

If you want, you can use the same source code as I did. This way, you can repeat my steps to get a feel for the technique before you try to apply it on one of your own codebases. You can find the full source code used in this article here: github.com/stijn-dejongh/docsify-glossary

If you learned something new or tried your hand at using this technique, please leave a comment below and tell me how it went. I am curious to know your experiences with the test-first bug fixing approach.

Thank you for reading, and as always: happy coding



Acknowledgements and References

Thanks to these wonderful people

Many thanks to Tim S. , Kobe Vanlaere , and Gert Vanautgaerden for proofreading this article and helping me make it more coherent.

This code-along style article was inspired by this post, in which Ebenmelu Ifechukwu talks about the challenges they faced when using test-first approaches.

References

Jun Chatani

Cloud Engineer at KBC

9 个月

Good reminder to let tests explicitly fail and make sure the test is actually testing the bug! During redgreen testing, I also catch myself not letting the test properly fail and immediately changing the implementation to let it pass. I will keep it in mind for next time ??

回复
Kobe Vanlaere

Employeneur at TMC

1 年

"An article about unit testing from a developer? That is strange." A qoute that I got when speaking about this article. Nice to see how you try to fix a system instead making a workaround for a bug.

Stijn Dejongh

Hands-on software solution architect | freelancer | husband | father | human

1 年

It looks like something went wrong with the initial publishing of the article, part if it became invisible on mobile. This should be resolved now.

回复

要查看或添加评论,请登录

Stijn Dejongh的更多文章

  • Learning for the Long Haul: A Tale of Two Approaches

    Learning for the Long Haul: A Tale of Two Approaches

    Introduction Have you ever wondered what you should be learning to get ahead in your career? In today’s rapidly…

    7 条评论
  • Getting hired in software.

    Getting hired in software.

    You learned how to code. Awesome! Companies will start fighting over you, throwing cash at your feet and asking you to…

  • How big should a "Unit" be?

    How big should a "Unit" be?

    This article is an extended version of my thoughts on the topic of Unit Testing, originating from a discussion with…

    10 条评论
  • Become a hero in the Bash Shell

    Become a hero in the Bash Shell

    Why do I like my Command Line Interface tools so much? The reasons are remarkably simple, actually: You have control:…

    4 条评论
  • How easily include file content into your github README.md

    How easily include file content into your github README.md

    Writing documentation is somewhat of a hassle, keeping your repository README.md file up to date even more so.

  • Using "eat what you cook" API testing

    Using "eat what you cook" API testing

    I was inspired by a section of the book "97 things every developer should know" ~ Kevlin Henney et al. Currently, I am…

  • Why should you be concerned about the rise of the Agile?Empire?

    Why should you be concerned about the rise of the Agile?Empire?

    A long time ago, in software teams not so far away Around the turn of the century a lot of things were looking…

    13 条评论

社区洞察

其他会员也浏览了