Understanding randomGenerator functions for testing Javascript functions

in #utopian-io6 years ago

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 operatorthree 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

  1. We started with getRandomNumber from the previous article.
  2. We used a loop function from SO and checked how it was implemented.
  3. We extended the loop so that we can also pass parameters to the desired function that needs to be invoked.
  4. We learnt using the spread operator.
  5. We used our loop to identify where all our getRandomNumber function was failing.
  6. We improved the logic of getRandomNumber to work for all the ranges.
  7. We tested out getRandomNumberand made sure that it works for all the ranges.
  8. We wrote runTestforFixedValues function to list some know edge cases and understand the nature of inputs.
  9. We created runTestforRandom function which generates a random flat json input for our testing.
  10. We used loop(runTestforRandom, 10); for running runTestforRandom 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.

Sort:  

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