DEV Community

Cover image for Unit testing like a Hacker🧪
Batuhan Ipci
Batuhan Ipci

Posted on • Updated on

Unit testing like a Hacker🧪

Inspiration

In one of my CS courses, we were taught to write code implementation just enough to pass the unit tests. This is called Test-driven development (TDD) and it is all about reassuring yourself/your team that the code behaves as intended for particular use cases. Primarily, you would have to write the unit tests first and then write the implementation to pass the tests.

Writing unit tests overall and keeping good maintenance on them was not an easy concept to grasp at first, but it is one of the crucial skills I learned as a junior developer.

HacktoberTest - haha, it's a pun...

Being in and out with unit tests and dealing with labs/assignments daily actually helped me find proper tickets to contribute to during Hacktoberfest!

My first 2 PRs(1st & 2nd) were likely newbie level and I wanted to contribute to something more meaningful, something that would challenge me intellectually. I spent a good week or so stressing over what to contribute to next on the issues page, only to stress even more. What became clear to me was that if you are scrolling and scrolling through the issues page just to find the perfect issue for your use case, you're doomed... there are another 20K people doing the same just to get swags. That way, you are probably spending more energy searching than you would have spent on writing a new feature from scratch or a tough bug fix.

Luckily, issues that required writing unit tests were not as high in demand as other issues, so I was able to find a few of them. This issue and this one gave me the opportunity to write unit tests and let me raise my PRs for a project that was indeed meaningful and challenging.

Hacking

The project I contributed to was a web app called pr-approve-generator that generates encouraging messages for PRs. It is intended to be used by project maintainers in GitHub to hearten their contributors.

Image description

Every time you click on Refresh button, you get a new message that welcomes the PR and encourages the contributor to keep up the good work. The messages are stored in an array and the app randomly picks one of them to display. Here is the function that handles this logic:

getRandomMessage() {
    const { messagesState } = state;
    const index = Math.floor(Math.random() * messagesState.length);
    let newMessage;
    let newMessagesState;
    if (messagesState.length !== 0) {
    newMessage = messagesState[index];
    newMessagesState = [
        ...messagesState.slice(0, index),
        ...messagesState.slice(index + 1),
    ];
    } else {
    newMessage = messages[index];
    newMessagesState = [
        ...messages.slice(0, index),
        ...messages.slice(index + 1),
    ];
    }
    state.messagesState = newMessagesState;
    return { newMessage, newMessagesState };
},
Enter fullscreen mode Exit fullscreen mode

||:--:|| You can check randomizer.js for better understanding

The first issue I tackled was to write unit tests for the getRandomMessage function. The function is responsible for picking a random message from the array and returning it, also it removes the message from the array so that the same message is not picked again. If there are no more messages left in the array, the empty array gets replaced with the messages array again, and so on. This function is called every time the Refresh button is clicked. (There is also a new feature recently added to the app that allows you to change emojis using getRandomEmoji function and works in a very similar logic described above. I also raised a PR to write a test for this feature here).

Unit testing framework was already implemented, using Vitest so I started hacking by setting up a coverage provider to explicitly identify the covered/uncovered lines and mentioned this to the maintainer in the comments. I used Istanbul 🇹🇷 for this purpose.

Unit testing is therapeutic and painful

I started mocking the messages & emojis array and called the getRandomMessage() with the mocked array. Depending on the index that was picked, I asserted the returned message not to be equal to the new message state since the message was removed from the array (i.e. they need to be unique).

  it("should always return unique message", () => {
    const messages = ["1", "2", "3", "4", "5"];
    const emojis = ["1", "2", "3", "4", "5"];
    const randomizer = buildRandomizer(messages, emojis);

    const { newMessage, newMessagesState } = randomizer.getRandomMessage();
    expect(newMessagesState).not.toContain(newMessage);
  });
Enter fullscreen mode Exit fullscreen mode

Notice that this test follows the AAA (Arrange-Act-Assert) pattern. The Arrange part is where you set up only the data to be operated in the test.

const messages = ["1", "2", "3", "4", "5"];
const emojis = ["1", "2", "3", "4", "5"];
Enter fullscreen mode Exit fullscreen mode

The Act part is where you would call the function/s that you want to test.

const randomizer = buildRandomizer(messages, emojis);
const { newMessage, newMessagesState } = randomizer.getRandomMessage();
Enter fullscreen mode Exit fullscreen mode

The Assert part is the expected outcome, it is dependent on Act as the function would evoke a potential response to be asserted.

expect(newMessagesState).not.toContain(newMessage);
Enter fullscreen mode Exit fullscreen mode

With this pattern in mind I have written the entire randomizer.test.js file and raised a PR. My second PR was about writing unit tests for messages.test.js file to make sure the following;

  • Each message should be unique regardless of the format and emoji used.
  • Duplicated messages failing in test
  • LGTM message will fail in test regardless of the format

To satisfy these requirements, I used regular expressions to match the format of the message and assert that the message is unique.

it("should have unique messages regardless of the emojis", () => {
  const regex = /([a-zA-Z0-9 ])/g;
  const uniqueMessages = messages.map((message) =>
    message.match(regex).join("").toLowerCase()
  );

  expect(uniqueMessages).toEqual([...new Set(uniqueMessages)]);
  expect(uniqueMessages.length).toBe(messages.length);
});
Enter fullscreen mode Exit fullscreen mode

Oh, and I also made sure LGTM is not welcome :P

it("should never contain the message LGTM", () => {
  const lgtmMessages = messages.filter(
    (message) => message.toLowerCase() === "lgtm"
  );
  expect(lgtmMessages).toEqual([]);
});
Enter fullscreen mode Exit fullscreen mode

Now, the coverage report was proudly showing the covered lines in green with 100% ! This provides value as the report is a visual indication and documentation of the viability of the tests. It gave me a sense of accomplishment and I found it therapeutic to see it was working as it was intended to be.

Git troubles

During my second PR though, I had trouble with git branches. Initially, when I was writing unit tests for randomizer.test.js file I had a branch called tests-for-randomizer to implement necessary tests for this particular file. After I implemented my work into this branch, for my second PR I created a new branch called tests-for-messages to implement tests for messages.test.js file using the command git checkout -b tests-for-messages. Obviously all the work I have implemented for randomizer.test.js came along with the new branch messages.test.js.

I would have to first update the master branch and in the current branch tests-for-messages rebase to master with git rebase master -i (-i for interactive) to remove the commits that were not related to messages.test.js file. The problem was that I only practiced rebase on my own and it was intimidating to do it in an open-source project. I was afraid to mess up the project and lose my work. I asked some help from the maintainer and he guided me well through the process. So in conclusion, to solve this problem I had to rebase the branch tests-for-messages to master which removed the commits that were not related to messages.test.js file and did a force push to the remote branch tests-for-messages with git push -f origin tests-for-messages.

The pain in unit testing

Ensuring everything is working as intended makes sense in principle, for example, I currently am developing a static site generator in cpp, palpatine, and as I develop it I stress about writing unit tests for it. Soon enough whenever a bug occurs I will be writing unit tests before debugging it. While writing unit tests though, I need to keep in mind that they won't stick around forever, my ssg tool is rapidly evolving; refactoring, adding new features, fixing bugs and shipping new releases day by day. That said, the unit tests will be obsolete soon enough and I might end up spending more time maintaining unit tests than actually developing the tool. Thus my philosophy at writing unit tests is to write them when they are actually needed, maybe when the consequences of breaking the code are high or when they are solving a specific problem.

Conclusion

Hacktoberfest was a perfect gateway for me to start contributing to open-source projects. Being in touch with the community and learning from senior developers or experienced maintainers is thus far been the most rewarding part of the month.

Latest comments (2)

Collapse
 
mnosov622 profile image
Maxim Nosov ✪

Wow !

That was very useful to read :) You are good at blogging Batu !

Thank you :)

Collapse
 
saminarp profile image
Samina Rahman Purba

Such an insightful post! Keep up the good work. Learned so much from here.