Unit testovanie generátora náhodných čísiel

Tradičný unit test končí overením, či sa očakávaná hodnota zhoduje s vypočítanou, ale čo keď je vypočítaná hodnota náhodná, pretože závisí na generátore náhodných čísiel?

Zoberme si hlúpy generátor náhodných slov:

import java.util.*;

public class RandomWordGenerator {
    private List<String> word = Arrays.asList("axolotl", "wolverine", "anteater", "platypus", "dormouse");

    private Random random = new Random();

    public String getRandomWord() {
        int randomIndex = random.nextInt(word.size());
        return word.get(randomIndex);
    }
}

Generovanie náhodných čísiel rieši klasická trieda java.util.Random s mnohými metódami na integery, longy a podobne.

Unit test by mohol vyzerať:

public class RandomWordGeneratorTest {

    @Test
    public void test() {
        RandomWordGenerator generator = new RandomWordGenerator();
        Assert.assertEquals(???????, generator.getRandomWord());
    }

}

Čo uviesť namiesto otáznikov, keď hodnota môže byť pestrá?

Máme dve možnosti:

  • buď vytvoriť vlastný nenáhodný náhodný generátor čísiel
  • alebo využiť finty so seedmi.

V oboch prípadoch budeme musieť dodať do triedy generátora slov konštruktor, cez ktorý dotlačíme vlastnú implementáciu triedy Random. Najlepšie to vyriešime dodaním druhého konštruktora (ktorý môže byť pokojne protected, pretože stačí k nemu pristupovať z rovnakého balíčka). Druhá verzia RandomWordGeneratora teda je:

public class RandomWordGenerator {
    private List<String> word = Arrays.asList("axolotl", "wolverine", "anteater", "platypus", "dormouse");

    private Random random;

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

    protected RandomWordGenerator(Random random) {
        this.random = random;
    }

    public String getRandomWord() {
        int randomIndex = random.nextInt(word.size());
        return word.get(randomIndex);
    }
}

Vytvorenie vlastnej implementácie Random

Jedna možnosť je implementovať vlastnú podtriedu java.util.Random. Dokumentácia tvrdí, že stačí prekryť metódu:

protected int next(int bits)

To je veľká výhoda: nemusíme prekrývať všetkých osem (či koľko) metód typu nextXXX(), čo uľahčí implementáciu. (Tieto metódy totiž závisia na uvedenej metóde.)

Jednoduchá trieda, ktorá vždy vráti rovnaké číslo, môže vyzerať nasledovne:

import java.util.Random;

public class ConstantRandom extends Random {
    private int constant;

    public ConstantRandom(int constant) {
        this.constant = constant;
    }

    @Override
    protected int next(int bits) {
        return constant;
    }
}

Skúsme si ju otestovať:

import static org.junit.Assert.*;
import org.junit.Test;

public class ConstantRandomTest {

    @Test
    public void testFiveGeneratedNumbers() {
        int constant = 1;

        ConstantRandom constantRandom = new ConstantRandom(constant);
        for (int i = 0; i < 5; i++) {
            int random = constantRandom.nextInt();
            if(random != constant) {
                fail("Unexpected random number: " + i + ", but should be " + constant);
            }
        }       
    }

}

Unit test prejde, ak sa vygeneruje päť jednotiek: čo sa naozaj stane.

Použitie v generátore náhodných čísiel

Ukážme si použitie na unit teste generátora náhodných slov:

public class RandomWordGeneratorTest {

    @Test
    public void test() {
        ConstantRandom constantRandom = new ConstantRandom(1);

        RandomWordGenerator generator = new RandomWordGenerator(constantRandom);
        Assert.assertEquals("wolverine", generator.getRandomWord());
    }
}

Test by mal vždy vrátiť rosomáka, ten je totiž v zozname slov druhým prvkom (prvkom s indexom 1).

Využitie seedov

Nenechajte sa oklamať: trieda Random je len deterministický pseudonáhodný generátor čísiel. Prečo deterministický? Postupnosť vygenerovaných náhodných čísiel možno totiž predvídať: stačí, že počiatočný seed je rovnaký.

Vyskúšajte si to:

Random random1 = new Random(1);
Random random2 = new Random(2);

for(int i = 0; i < 10; i++) {
    System.out.println(random1.nextInt() + " " + random2.nextInt());
}

Ukážkový kód vypľuje desať dvojíc s rovnakými prvkami. Túto vlastnosť môžeme využiť pri testovaní.

public class RandomWordGeneratorTest {

    @Test
    public void testWithConstantSeedRandom() {
        Random random = new Random(1);

        RandomWordGenerator generator = new RandomWordGenerator(random);
        Assert.assertEquals("axolotl", generator.getRandomWord());
    }
}

Ak by sme generovali povedzme päť slov so seedom 1, získali by sme vždy axolotla, vtákopyska, mravcoleva, ešte jedného vtákopyska a plcha.

Algoritmy garantujú, že rovnaký seed garantuje rovnakú postupnosť: a to bez ohľadu na použitú architektúru či platformu, kde beží Java.

Kryptograficky silné generátory

Ak sa zdá, že takéto generovanie čísiel je smiešne, môžete sa obrátiť na triedu java.security.SecureRandom. Tá dedí od Randomu a rozhodne negeneruje predvídateľný tok čísiel.

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *