views:

237

answers:

5

I need a function which returns an array in random order. I want to ensure that it is randomish but I have no idea how one would go about writing the tests to ensure that the array really is random. I can run the code a bunch of times and see if I have the same answer more than once. While collisions are unlikely for large arrays it is highly probable for small arrays (say two elements).

How should I go about it?

+2  A: 

Basically the trick is to extract the randomness out of the class you're testing. This will allow you to test the class by injecting the formula for the randomness from your test which of course wouldn't be random at all.

C# example:

public static List<int> Randomise(List<int> list, Func<bool> randomSwap)
{
    foreach(int i in list)
    {
        if (randomSwap)
        {
            //swap i and i+1;
        }
    }
    return list;
}

Pseudo Usage:

list = Randomise(list, return new Random(0, 1));
Jonathan Parker
+1 don't test the infrastructure, test your code
tarn
+2  A: 

Cedric recommends an approach where you run the function enough times to get a statistically significant sample and verify the properties of you samples.

So for shuffling, you'd probably want to verify that the relationship between the elements have very small covariance, that the expected position of each element is N/2, etc.

Ryan
+1 absolutely right, this is pretty much the only way to test that a supposedly random process is actually random enough.
David Zaslavsky
Funnily enough getting the same result for 100 runs could actually be random. That's the whole point of random. You flip a coin 10 times and get heads each time. The probability of heads on the 11th flip is still 50%. Practically however you would very rarely get this result.
Jonathan Parker
The problem you have though is that you've now got a test that *might* be broken!
Jonathan Parker
The probability of getting the same coin flips 100 times in a row is about 8*10^-31. In comparison, the probability that a hard drive will have an undetected bit flip error is 3*10^-8. So yes, I'd say for all practical purposes you wouldn't get that result :-)
Ryan
A: 

First of all you should use a fixed seed for the random number generator, or otherwise the test may fail randomly (i.e. sometimes they may be in order - that's the problem with randomness). Then you could do some simple checks, for example that the values are not in order, and that on every run the values are different.

Here is an example of tests that I wrote for my own shuffle bag implementation.

import jdave.Specification;
import jdave.junit4.JDaveRunner;
import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

/**
 * @author Esko Luontola
 * @since 25.2.2008
 */
@RunWith(JDaveRunner.class)
public class ShuffleBagSpec extends Specification<ShuffleBag<?>> {

    public class AShuffleBagWithOneOfEachValue {

        private ShuffleBag<Integer> bag;
        private List<Integer> expectedValues = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);

        public ShuffleBag<Integer> create() {
            bag = new ShuffleBag<Integer>(new Random(123L));
            for (Integer value : expectedValues) {
                bag.add(value);
            }
            return bag;
        }

        public void onFirstRunAllValuesAreReturnedOnce() {
            List<Integer> values = bag.getMany(10);
            specify(values, does.containExactly(expectedValues));
        }

        public void onFirstRunTheValuesAreInRandomOrder() {
            List<Integer> values = bag.getMany(10);
            specify(values.get(0), does.not().equal(0));
            specify(values.get(0), does.not().equal(1));
            specify(values.get(0), does.not().equal(9));
            specify(values, does.not().containInOrder(expectedValues));
            specify(values, does.not().containInPartialOrder(1, 2, 3));
            specify(values, does.not().containInPartialOrder(4, 5, 6));
            specify(values, does.not().containInPartialOrder(7, 8, 9));
            specify(values, does.not().containInPartialOrder(3, 2, 1));
            specify(values, does.not().containInPartialOrder(6, 5, 4));
            specify(values, does.not().containInPartialOrder(9, 8, 7));
        }

        public void onFollowingRunsAllValuesAreReturnedOnce() {
            List<Integer> run1 = bag.getMany(10);
            List<Integer> run2 = bag.getMany(10);
            List<Integer> run3 = bag.getMany(10);
            specify(run1, does.containExactly(expectedValues));
            specify(run2, does.containExactly(expectedValues));
            specify(run3, does.containExactly(expectedValues));
        }

        public void onFollowingRunsTheValuesAreInADifferentRandomOrderThanBefore() {
            List<Integer> run1 = bag.getMany(10);
            List<Integer> run2 = bag.getMany(10);
            List<Integer> run3 = bag.getMany(10);
            specify(run1, does.not().containInOrder(run2));
            specify(run1, does.not().containInOrder(run3));
            specify(run2, does.not().containInOrder(run3));
        }

        public void valuesAddedDuringARunWillBeIncludedInTheFollowingRun() {
            List<Integer> additionalValues = Arrays.asList(10, 11, 12, 13, 14, 15);
            List<Integer> expectedValues2 = new ArrayList<Integer>();
            expectedValues2.addAll(expectedValues);
            expectedValues2.addAll(additionalValues);

            List<Integer> run1 = bag.getMany(5);
            for (Integer i : additionalValues) {
                bag.add(i);
            }
            run1.addAll(bag.getMany(5));
            List<Integer> run2 = bag.getMany(16);

            specify(run1, does.containExactly(expectedValues));
            specify(run2, does.containExactly(expectedValues2));
        }
    }

    public class AShuffleBagWithManyOfTheSameValue {

        private ShuffleBag<Character> bag;
        private List<Character> expectedValues = Arrays.asList('a', 'b', 'b', 'c', 'c', 'c');

        public ShuffleBag<Character> create() {
            bag = new ShuffleBag<Character>(new Random(123L));
            bag.addMany('a', 1);
            bag.addMany('b', 2);
            bag.addMany('c', 3);
            return bag;
        }

        public void allValuesAreReturnedTheSpecifiedNumberOfTimes() {
            List<Character> values = bag.getMany(6);
            specify(values, does.containExactly(expectedValues));
        }
    }

    public class AnEmptyShuffleBag {

        private ShuffleBag<Object> bag;

        public ShuffleBag<Object> create() {
            bag = new ShuffleBag<Object>();
            return bag;
        }

        public void canNotBeUsed() {
            specify(new jdave.Block() {
                public void run() throws Throwable {
                    bag.get();
                }
            }, should.raise(IllegalStateException.class));
        }
    }
}

Here is the implementation, in case you want to see it also:

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * @author Esko Luontola
 * @since 25.2.2008
 */
public class ShuffleBag<T> {

    private final Random random;

    /**
     * Unused values are in the range {@code 0 <= index < cursor}.
     * Used values are in the range {@code cursor <= index < values.size()}.
     */
    private final List<T> values = new ArrayList<T>();
    private int cursor = 0;

    public ShuffleBag() {
        this(new Random());
    }

    public ShuffleBag(Random random) {
        this.random = random;
    }

    public void add(T value) {
        values.add(value);
    }

    public T get() {
        if (values.size() == 0) {
            throw new IllegalStateException("bag is empty");
        }
        int grab = randomUnused();
        T value = values.get(grab);
        markAsUsed(grab);
        return value;
    }

    private int randomUnused() {
        if (cursor <= 0) {
            cursor = values.size();
        }
        return random.nextInt(cursor);
    }

    private void markAsUsed(int indexOfUsed) {
        cursor--;
        swap(values, indexOfUsed, cursor);
    }

    private static <T> void swap(List<T> list, int x, int y) {
        T tmp = list.get(x);
        list.set(x, list.get(y));
        list.set(y, tmp);
    }

    public void addMany(T value, int quantity) {
        for (int i = 0; i < quantity; i++) {
            add(value);
        }
    }

    public List<T> getMany(int quantity) {
        List<T> results = new ArrayList<T>(quantity);
        for (int i = 0; i < quantity; i++) {
            results.add(get());
        }
        return results;
    }
}
Esko Luontola
+1  A: 

Other articles have recommended using a fixed seed for the random number generator, mocking the random number generator. These are fine recommendations, and I often follow them. Sometimes, however, I will test the randomness instead.

Given a target array that you want to populate randomly from a source array consider doing the following. Load the source array with consecutive integers. Create a third array called 'sum' and load it with zeros. Now randomly populate the target and then add each element of the target to the corresponding element of sum. Do that another thousand times. If the distribution is really random, then the sums should all be roughly the same. You can do a simple -delta < expected < +delta comparison on each element of the sum array.

You can also do a mean and stdev of the elements of the sum array and do a delta comparison of them too.

If you set the limits right, and do enough iterations, this will suffice nicely. You might be tempted into thinking it can give you a false negative, but if you set the limits correctly it will be more likely for a cosmic ray to alter the execution of the program.

Uncle Bob
This is the law of large numbers @see http://en.wikipedia.org/wiki/Law_of_large_numbers
Adrian
A: 

No need to test for randomness--that's already implicit in your choice of algorithm and random number generator. Use the Fisher-Yates/Knuth shuffling algorithm:

http://en.wikipedia.org/wiki/Knuth%5Fshuffle

The Java implementation from that Wikipedia page:

public static void shuffle(int[] array) 
{
    Random rng = new Random();       // java.util.Random.
    int n = array.length;            // The number of items left to shuffle (loop invariant).
    while (n > 1) 
    {
        n--;                         // n is now the last pertinent index
        int k = rng.nextInt(n + 1);  // 0 <= k <= n.
        // Simple swap of variables
        int tmp = array[k];
        array[k] = array[n];
        array[n] = tmp;
    }
}
Aaron Zinman