You are tasked with writing unit tests for a simplistic function that retrieves a character (a string containing only 1 symbol in JS) at a given index from a passed in string.
You are supplied a function named lookupChar, which should have the following functionality:
- lookupChar(string, index) - A function that accepts a string - the string in which we’ll search and a number - the index of the char we want to lookup:
- If the first parameter is not a string or the second parameter is not an integer - return undefined.
- If both parameters are of the correct type, but the value of the index is incorrect (bigger than or equal to the string length or a negative number) - return the text "Incorrect index".
- If both parameters have correct types and values - return the character at the specified index in the string.
JS Code
To ease you in the process, you are provided with an implementation which meets all of the specification requirements for the lookupChar function:
function lookupChar(string, index) { if (typeof(string) !== 'string' || !Number.isInteger(index)) { return undefined; } if (string.length <= index || index < 0) { return "Incorrect index"; } return string.charAt(index); } console.log(lookupChar("opa", "1")); module.exports = {lookupChar};//for the file from which we import
Your tests will be supplied a function named "lookupChar" which contains the above mentioned logic, all test cases you write should reference this function. Submit in the judge your code containing Mocha tests testing the above functionality.
Writing tests is all about thinking, a good first step in testing a method is usually to determine all exit conditions (all ways in which the method can end its execution - return statements, throw statements or if none of the previous are present simply running to the end). Reading through the specification or taking a look at the implementation we can easily determine 3 main exit conditions - returning undefined, returning an empty string or returning the char at the specified index.
Now that we have our exit conditions we should start checking in what situations we can reach them. We’ll start with returning undefined. Reading the specification we can see that if any of the parameters are of the incorrect type we need to return undefined. Having two input parameters we easily have our first 2 tests.
It may look like these two tests are enough to cover the first exit condition, however taking a closer look at the implementation, we see that the check uses Number.isInteger() instead of typeof(index === number) to check the index. While typeof would protect us from getting passed an index that is a non-number, it won’t protect us from being passed a floating point number. The specification says that index needs to be an integer (so it could be used for getting the char at the index), since floating point numbers won’t work as indexes, we need to make sure that the passed in index is not a floating point number.
Having added the extra test we have now covered the first exit condition, moving on we go to the next one - returning empty string. Checking the specification again we can see two distinct cases that we should check for - getting passed an index that is a negative number or getting passed an index which is outside of the bounds of the string.
Normally this would be enough to cover this condition, however the situation where the index passed is equal to the string length (known as an edge case) can be easily missed when writing the code, so it’s a good idea to test for it too.
Having cleared all the validation checks it’s time for the last exit condition - returning a correct result, when checking results there are usually a number of things to check depending on the returned value and specification of the function. In our situation we are returning a char so a simple check for the returned value will be enough, however a single test for the correct return value is akin to guessing.
In a situation where there are limited correct results (for example a method which returns true or false) getting the correct value in one test does not guarantee the correctness of the method, even if the method did not function correctly there would still be a 50-50 chance of us receiving the correct result. To counteract this problem we usually create more than one test to check for the correct result. More tests is always better, but in most situations a few tests with different input parameters and different expected return values would be enough to cover the function.
Finally, my Mocha and Chai tests:
let lookupChar = require("../03.CharLookup").lookupChar; let expect = require("chai").expect; describe("Tests for this task", function () { describe("Check the string input", function () { it("should be a string", function () { expect(lookupChar("opa", 1)).to.equal("p"); }); it("should be undefined", function () { expect(lookupChar("1", "1")).to.be.undefined; }); }); describe("Check the index input", function () { it("should be undefined", function () { expect(lookupChar(1, 1)).to.be.undefined; }); }); describe("Check both the string and the index input", function () { it("should be undefined", function () { expect(lookupChar(1, "1")).to.be.undefined; }); }); describe("Check the length", function () { it("should be Incorrect index", function () { expect(lookupChar("opa", 4)).to.equal("Incorrect index"); }); it("should be Incorrect index", function () { expect(lookupChar("opa", -1)).to.equal("Incorrect index"); }); }); describe("Check if the index is integer", function () { it("should be Incorrect index", function () { expect(lookupChar("opa", 4.5)).to.be.undefined; }); }) });