Unit Testing Like a Hacker

Inspiration

In one of my CS courses, we were taught to write enough code implementations to pass unit tests. This is called Test-Driven Development (TDD) and is about assuring yourself/your team that the code behaves according to particular use cases. Mainly, you need to write unit test first and then write implementation to make test pass.

Overall writing unit tests and having a good maintainability on them was not an easy concept to grasp at first, but it is an important skill that I learned as a junior developer.

HacktoberTest – Haha, that’s a sentence…

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

My first 2 PRs (1st and 2nd) were probable newbie level And I wanted to contribute some more meaningful, something that would challenge me intellectually. I spent a good week stressing what to contribute next on the issues page, only to stress out even more. What became clear to me was that if you are scrolling and scrolling through the problem page to find the perfect issue for your use case, you are doomed… another 20K people doing the same Those who are just doing it to get swag. That way, you’re probably spending more energy searching than you would have spent writing a new feature from scratch or fixing a difficult bug.

Fortunately, the issues that required writing unit tests were not as in-demand as other issues, so I was able to spot some of them. This issue and it gave me the opportunity to write unit tests and increase my PR for a project that was really worthwhile and challenging.

hacking

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

every time you click Refresh button, you get a new message that welcomes PR and encourages contributors to do good work. The messages are stored in an array and the app randomly chooses 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 problem I faced was writing unit tests for getRandomMessage Celebration. The function is responsible for selecting a random message from the array and returning it, plus it removes the message from the array so that the same message is not picked up again. If there are no more messages left in the array, the empty array is again replaced with the messages array, and so on. This function is called every time Refresh The button is clicked. (Recently a new feature has also been added to the app that allows you to replace the emoji using getRandomEmoji Works and works in a very similar logic to the one described above. I even raised a PR for writing a test for this feature here).

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

Unit testing is therapeutic and painful

i started joking messages & emojis array and called getRandomMessage() With fake array. Depending on the index chosen, I insisted on the returned message not being equal to the new message state because the message was removed from the array (i.e. they have 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

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

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

exit fullscreen mode

Act part is where you will call the function you want to test.

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

exit fullscreen mode

Assert part is the expected result, it depends Act As the emphasis on the function will create a potential reaction.

expect(newMessagesState).not.toContain(newMessage);
enter fullscreen mode

exit fullscreen mode

Keeping this pattern in mind I have written the entire randomizer.test.js file and prepared a PR. My second PR was about writing unit tests for the message.test.js file to ensure the following;

  • Each message must be unique regardless of the format and emoji used.
  • Failed duplicate message test
  • LGTM message test will fail regardless of format

To meet 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 made sure too LGTM not welcome 😛

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 lines covered in green with 100%! It provides value because the report is a visual indication and document of the feasibility of the tests. It gave me a sense of accomplishment and I found it therapeutic to see that it was working as it was intended.

git trouble

Although during my second PR, I had trouble git branches, Initially, when I was writing unit tests randomizer.test.js file I had a branch named tests-for-randomizer To apply the required tests to this particular file. After deploying my work in this branch, for my second PR I created a new branch named tests-for-messages to apply the test for messages.test.js file using command git checkout -b tests-for-messages, Obviously all the work that I have implemented randomizer.test.js came up with new branch messages.test.js,

i need to update first master branch and in current branch tests-for-messages rebase to master with git rebase master -i ,-i for interactive) to remove commits that were not related messages.test.js file. The problem was that I had only practiced rebase on my own and doing it in an open-source project was intimidating. I was afraid of messing up the project and losing my job. I asked the maintainer for some help and he guided me well in the process. So in conclusion, to solve this problem I had to rebase the branch tests-for-messages To master which removed commits that were not related messages.test.js a force push to the file and remote branch tests-for-messages with git push -f origin tests-for-messages, The maintainer was happy with the result and combined the PR and I believe

pain in unit testing

making sure everything is working as it makes sense in theory, for example, i’m currently developing a static site generator in cpp, palpatine, and as i develop it, i I insist on writing unit tests for Soon whenever there is a bug I will write unit tests before debugging it. Although when writing unit tests, I have to keep in mind that they won’t last forever, my SSG tool is evolving rapidly; Refactoring, adding new features, fixing bugs and shipping new releases day by day. That said, unit tests will soon become obsolete and I may spend more time maintaining unit tests than actually developing tools. Thus my philosophy on writing unit tests is to write them when they are really needed, maybe when the consequences for breaking code are high or when they are solving a specific problem.

conclusion

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

Leave a Comment