Understanding randomGenerator functions for testing Javascript functions
Recently I had thrown a bounty for converting a flat json to a nested json. You can check more about it in the link below
Steem Bounty : Nestify a flat JSON object - Steemit
https://steemit.com/@mightypanda provided a solution to the same and won the bounty. I wanted to test the solution for various scenarios. Creating the inputs for the edge cases was very time consuming. I thought of using random generators for testing the same. So I started digging a little bit.
To randomise or not
There is no agreement on using random generators for testing. The argument against using random generators was that the test cases should be deterministic meaning that you should know what is the input and what is the expected output. While this makes sense from the test report and test suites, I though we could use randomised generators for inputs so that we can test the output for hitherto untested inputs every time we want. If used properly this method helps us increase the number of scenarios or inputs for which we can test.
Lets randomise
If you have read my previous article Understanding Promises in Javascript you would remember that we used the getRandomNumber
function which served us well for promiseGenerator
functions so that we were able to simulate and learn how promises work. I had added a disclaimer saying that //works when both start,end are >=1 and end > start.
Since I will be using this method more frequently I thought of cleaning it up a bit.
function getRandomNumber(start = 1, end = 10) {
//works when both start,end are >=1 and end > start
return parseInt(Math.random() * end) % (end-start+1) + start;
}
// Lets add another function which just calls getRandomNumber //function and prints it.
function printRandomNumber(start = 1, end = 10) {
console.log(getRandomNumber.apply(null, arguments));
}
I wanted it to work for positive numbers, one of the params could be zero and even for negative numbers. To find it out if it is truly random(oh I mean’t randomised enough) and if the edge cases are handled we will need to run this getRandomNumber
function multiple times. What I mean by that is that if we need to be sure that start and end values are included in the random numbers that are generated the only way forward is that run the function enough number of times until the random number generated is same as start
or end
. I would put it as occurrence is the only proof that it will occur.
Repeat
So let us create a function which can call the desired function desired number of times. I got the following example from Stackoverflow.
const loop = (fn, times) => {
{
if (!times) {
return;
}
fn();
loop(fn, times - 1);
};
//Format for invoking our loop function. Let us say we need to call // getRandomNumber function 20 times
loop(printRandomNumber, 20);
Ah that was simple enough. It would have been good if I had thought of it. I think I googled a little early. So this function uses recursion. Exit criteria is that times is not zero. Recursion criteria is that when exit condition is not met invoke the desired function and then call the recursive loop function with times
variable decremented. That was simple enough, isn’t it?
But we might have scenarios where we will need to pass parameters to the function in question as well. So let us modify our function a little.
const loop = (fn, times = 5, params = []) => {
{
if (!times) {
return;
}
fn(...params);
loop(fn, times - 1, params);
};
//Format for invoking our loop function. Let us say we need to call // getRandomNumber function 20 times with start and end values as 2 and 5
loop(printRandomNumber, 20, [2,5]);
So we have added a third parameter to our loop function which will be an array of parameters. When invoking the desired function we are using the spread operator
three dots to pass the params to the function. We just need to make sure that when passing the parameters to the desired function we pass it as an array of parameter values. Come to think of it, I think we should have named this function as repeat
instead of loop.
If we make the times
to 100 or so it will be difficult for us to look at the values at one. So let us just create a concatenation function.
outputString = "";
// I know using global variables is bad. For now let us keep it this //way. Once explore the closures completely we can use it for this //scenario.
function concatenateRandomnumber(start = 1, end = 10) {
outputString += getRandomNumber.apply(null, arguments) + ", ";
}
// This will add a trailing comma but who cares. Lets just call in //Stanford comma for now :P
So let us call the above function and with some edge cases and check.
var randomLimits = [0, 3];
loop(concatenateRandomnumber, 100, randomLimits);
console.log(...randomLimits);
console.log(outputString);
fails for zero as 3 is never appearing
var randomLimits = [-3, 3];
loop(concatenateRandomnumber, 100, randomLimits);
console.log(...randomLimits);
console.log(outputString);
Fails for negative range.
Randomise Better
Using loop
function we have identified the edge cases where our getRandomNumber
is failing. This time I didn’t google. After some though I realised that it is all about getting the range. So I changed the function as follows.
function getRandomNumber(start = 1, end = 10) {
if (start > end) {
[start, end] = [end, start];
}
let range = end - start + 1;
return (parseInt(Math.random() * range) % range) + start;
}
This seems to work for most of the edge cases. Do let me know if I missed something.
Passes for all edge cases considered
Math.random
gives random floating numbers between 0 and 1. So (parseInt(Math.random() * range) % range)
will give us a random number between 0 and range.
I am then displacing it by start
to have the radom number generated between start
and end
.
Use this approach for testing our scenario
To know the details of the problem statement checkout https://steemit.com/javascript/@gokulnk/nestify-a-flat-json-object
For this problem statement we know that we will have a flat json and the value of the pos
attribute changes only one step at a time. So the increment is only in terms of -1,0,+1 In the solution provided by mightypanda getNestedJSON
is the main function and createChild
is used internally.
Let us first define runTestforFixedValues
function. These are like static inputs for the scenarios that we already know of. Let us check the output.
function runTestforFixedValues() {
var inputSets = [];
var input = [
{ pos: 1, text: "Andy" },
{ pos: 1, text: "Harry" },
{ pos: 2, text: "David" },
{ pos: 3, text: "Dexter" },
{ pos: 2, text: "Edger" },
{ pos: 1, text: "Lisa" }
];
inputSets.push(input);
input = [
{ pos: 1, text: "Andy" },
{ pos: 2, text: "Harry" },
{ pos: 2, text: "David" },
{ pos: 1, text: "Dexter" },
{ pos: 2, text: "Edger" },
{ pos: 2, text: "Lisa" }
];
inputSets.push(input);
input = [
{ pos: 1, text: "Andy" },
{ pos: 2, text: "Harry" },
{ pos: 3, text: "David" },
{ pos: 4, text: "Dexter" },
{ pos: 5, text: "Edger" },
{ pos: 6, text: "Lisa" }
];
inputSets.map(inputJSON => {
getNestedJSON(inputJSON);
});
}
Output for one fixed input value
The solution provided worked for all three input scenarios. Instead of creating more inputs like this. I spent some time on creating the random Flat JSON generator in the required format.
function runTestforRandom() {
var inputArray = [];
let alphabetsArray = [];
for (i = "A".charCodeAt(0); i <= "Z".charCodeAt(0); i++) {
alphabetsArray.push(String.fromCharCode(i));
}
var maxNumberOfElements = getRandomNumber(3, 10);
var inputObject = [];
// All we need is -1,0,-1 just change the number of occurences to //control the likelihood of the value being used. CRUDE //IMPLEMENTATION :P
var incrementArray = [-1, -1, 0, 1, 1, 1, 1, 1];
pos = 1;
inputArray.push({ pos: pos, text: "A" });
for (var i = 1; i < maxNumberOfElements; i++) {
randomNumber = getRandomNumber(1, incrementArray.length) - 1;
increment = incrementArray[randomNumber];
tempValue = pos + increment;
pos = tempValue > 0 ? tempValue : 1;
var obj = new Object();
obj.pos = pos;
obj.text = alphabetsArray[i % 26];
inputArray.push(obj);
}
getNestedJSON(inputArray);
}
I have created alphabetsArray
that contains alphabets which we will use for text property. maxNumberOfElements
is generated using our random number generator function. We know that between adjacent objects the value of pos changes only by -1,0,+1. So an incrementArray
is stuffed with these values and we are picking one of these values randomly. Let us say you want to create a deeply nested object, then increase the occurrences of +1 in this array. We can also make the text
field of the object random as well. Since its value doesn’t affect the outcome, we are assigning alphabets in series to text
object so that it is easier for verify if the output is nestified properly. Take a look at the output below to see what I am saying. It is almost as easy as reading alphabets to check if the nesting is proper or not. Even if we had not sued the alphabets in series we could still ready the pos
property in these objects to verify the nestifying operation.
Whenever I came across an interesting pattern I just copy pasted the input array from the console and pasted it back in my code for runTestforFixedValues
function. Very soon I had a big list of fixed input values like follows.
function runTestforFixedValues() {
var inputSets = [];
var input = [
{ pos: 1, text: "Andy" },
{ pos: 1, text: "Harry" },
{ pos: 2, text: "David" },
{ pos: 3, text: "Dexter" },
{ pos: 2, text: "Edger" },
{ pos: 1, text: "Lisa" }
];
inputSets.push(input);
input = [
{ pos: 1, text: "Andy" },
{ pos: 2, text: "Harry" },
{ pos: 2, text: "David" },
{ pos: 1, text: "Dexter" },
{ pos: 2, text: "Edger" },
{ pos: 2, text: "Lisa" }
];
inputSets.push(input);
input = [
{ pos: 1, text: "Andy" },
{ pos: 2, text: "Harry" },
{ pos: 3, text: "David" },
{ pos: 4, text: "Dexter" },
{ pos: 5, text: "Edger" },
{ pos: 6, text: "Lisa" }
];
// All those involving Alphabets were generated from the random function
inputSets.push(input);
input = [
{ pos: 1, text: "A" },
{ pos: 2, text: "B" },
{ pos: 2, text: "C" },
{ pos: 2, text: "D" },
{ pos: 1, text: "E" },
{ pos: 2, text: "F" },
{ pos: 2, text: "G" }
];
inputSets.push(input);
input = [
{ pos: 1, text: "A" },
{ pos: 2, text: "B" },
{ pos: 1, text: "C" },
{ pos: 2, text: "D" },
{ pos: 2, text: "E" },
{ pos: 3, text: "F" },
{ pos: 4, text: "G" },
{ pos: 5, text: "H" },
{ pos: 5, text: "I" },
{ pos: 4, text: "J" },
{ pos: 4, text: "K" },
{ pos: 5, text: "L" }
];
inputSets.push(input);
input = [
{ pos: 1, text: "A" },
{ pos: 1, text: "B" },
{ pos: 1, text: "C" },
{ pos: 2, text: "D" },
{ pos: 1, text: "E" },
{ pos: 1, text: "F" },
{ pos: 2, text: "G" },
{ pos: 1, text: "H" },
{ pos: 1, text: "I" },
{ pos: 2, text: "J" },
{ pos: 3, text: "K" },
{ pos: 3, text: "L" }
];
{
getNestedJSON(inputJSON);
});
}
We could keep repeating the process and increase our inputSets
until we have a wide range of input values.
So now we are able to test the code for various static input values. Every time we run the tests we can also look at some random inputs and verify if the output is expected. If you like that input or if you consider that is an edge case that should be tested in general then you can just copy paste that input to your static value testing method. Isn’t that cool? At-least I think it is. I find it very useful when I want to check the logic of a particular critical function.
If you want to look at all of this code in a single place checkout here is the repo for the same — https://github.com/nkgokul/flat-to-nested/blob/master/nestify.js
Summarising
- We started with
getRandomNumber
from the previous article. - We used a
loop
function from SO and checked how it was implemented. - We extended the
loop
so that we can also pass parameters to the desired function that needs to be invoked. - We learnt using the
spread
operator. - We used our
loop
to identify where all ourgetRandomNumber
function was failing. - We improved the logic of
getRandomNumber
to work for all the ranges. - We tested out
getRandomNumberand
made sure that it works for all the ranges. - We wrote
runTestforFixedValues
function to list some know edge cases and understand the nature of inputs. - We created
runTestforRandom
function which generates a random flat json input for our testing. - We used
loop(runTestforRandom, 10);
for runningrunTestforRandom
10 times.
Please point out if I am missing something here or if something can be improved. If you liked these articles and would like to read similar articles, don't forget to upvote.
Your article is very good, my game is very fun. https://goo.gl/Dd9394
You got a 100.00% upvote from @upme thanks to @gokulnk! Send at least 3 SBD or 3 STEEM to get upvote for next round. Delegate STEEM POWER and start earning 100% daily payouts ( no commission ).
Nice piece of info there...i'd love to read more though my steam power is meaningless but still upvoting you to show my love
nice
hmm