|
From: Nat P. <np...@us...> - 2002-08-07 17:21:56
|
Update of /cvsroot/mockobjects/no-stone-unturned/doc/xdocs
In directory usw-pr-cvs1:/tmp/cvs-serv26275
Added Files:
random.xml
Log Message:
Added draft of chapter/article about testing random behaviour.
--- NEW FILE: random.xml ---
<?xml version="1.0"?>
<chapter>
<title>Random Acts</title>
<section>
<title>Introduction</title>
<remark>Expand this</remark>
<para>
Pseudo-random behaviour is used in many applications.
In games it is used to portray natural behaviours that are too complex to
simulate accurately, or to add variety to behaviours that are too predictable
when simulated algorithmically.
</para>
<para>
How do we test randomness?
</para>
</section>
<section>
<title>Come Rain...</title>
<para>
I have been contracted to write a game that simulates the strategy of
Formula One motor racing.
My customer tells me that the weather plays has an important effect on
F1 strategy, so my game will have to simulate the weather somehow.
However, she wants the game to run in a web browser, and that means
writing it as a Java applet.
I'm going to rule out a realistic weather simulation — I'm going to
need all those CPU cycles to simulate the cars and drivers — and
instead randomly generate weather effects.
</para>
<para>
The first story I have is that the player must choose different tyres
depending on whether it is raining or not. Most of the time it will
be sunny but it should rain on every one out of every five races,
chosen at random.
Time to write a test, but how do I test a random event?
The weather object is obviously going to use a random number generator
to test the probability of rain. If that random number generator is
completely encapsulated within the weather object, I cannot override
the randomness to cause the behaviour I want to test.
Therefore, I need to mock that random number generator in my tests to
feed in fixed values, and pass the random number generator to the
weather object's constructor.
</para>
<programlisting lang="java">public void testRandomRain() {
MockRandom rng = new MockRandom();
Weather weather = new Weather( rng );
rng.setNextDouble( 0.0 );
weather.randomize();
assertTrue( "is raining", weather.isRaining() );
rng.setNextDouble( Weather.CHANCE_OF_RAIN );
weather.randomize();
assertTrue( "is not raining", !weather.isRaining() );
rng.setNextDouble( 1.0 );
weather.randomize();
assertTrue( "is not raining", !weather.isRaining() );
}</programlisting>
<para>
Here's the <classname>MockRandom</classname> class used by the test:
</para>
<programlisting>public class MockRandom
extends Random
{
private double nextDouble = 0.0;
public void setNextDouble( double d ) {
nextDouble = d;
}
public double nextDouble() {
return nextDouble;
}
}
</programlisting>
<para>
And now we can write a Weather class that passes the tests:
</para>
<programlisting lang="java">public class Weather
{
public static double CHANCE_OF_RAIN = 0.2;
private Random rng;
private boolean isRaining = false;
public Weather( Random rng ) {
this.rng = rng;
}
public boolean isRaining() {
return isRaining;
}
public void randomize() {
isRaining = rng.nextDouble() < CHANCE_OF_RAIN;
}
}</programlisting>
</section>
<section>
<title>...Or Shine</title>
<para>My customer now tells me that ground temperature is also
important to race strategy.
The player should choose different tyre compounds depending on the
temperature. Now my weather object must choose a temperature and whether
it is raining, both at random.
The random temperature will be chosen from a range of 20°C and 30°C.
But, the ground should be on average half the temperature when it is raining
compared to when it is sunny.
</para>
<para>
Again, let's write a test. Actually, I think we need
two tests, one to test that the temperature
is selected from within the forecast range when it is sunny,
and another to test that it is half the sunny temperature when raining.
Let's start with the former. Again we need to mock the random number
generator, but this time the <methodname>randomize</methodname> method
will get <emphasis>two</emphasis> random numbers. We need to change our
<classname>MockRandom</classname> class to mock a stream of random
numbers, rather than just one:
</para>
<programlisting>public class MockRandom
extends Random
{
private double[] nextDoubles = {0.0};
private int nextIndex = 0;
public void setNextDouble( double d ) {
setNextDoubles( new double[]{ d } );
}
public void setNextDoubles( double[] d ) {
nextDoubles = d;
nextIndex = 0;
}
public double nextDouble() {
double result = nextDoubles[nextIndex];
nextIndex = (nextIndex + 1) % nextDoubles.length;
return result;
}
}</programlisting>
<para>
Ok, the new <classname>MockRandom</classname> doesn't affect our existing
tests, so we can go on to write the test for random temperature when sunny:
</para>
<programlisting>public void testRandomTemperatureSunny() {
MockRandom rng = new MockRandom();
final double SUNNY = 1.0;
Weather weather = new Weather( rng );
rng.setNextDoubles( new double[] { SUNNY, 0.0 } );
weather.randomize();
assertEquals( "should be min temperature",
Weather.MIN_TEMPERATURE, weather.getTemperature(), 0.0 );
rng.setNextDoubles( new double[] { SUNNY, 0.5 } );
weather.randomize();
assertEquals( "should be average temperature",
(Weather.MIN_TEMPERATURE + Weather.MAX_TEMPERATURE)/2,
weather.getTemperature(), 0.0 );
rng.setNextDoubles( new double[] { SUNNY, 1.0 } );
weather.randomize();
assertEquals( "should be max temperature",
Weather.MAX_TEMPERATURE, weather.getTemperature(), 0.0 );
}</programlisting>
<para>
And write code to pass that test:
</para>
<programlisting>public class Weather
{
public static double CHANCE_OF_RAIN = 0.2;
<emphasis>public static double MIN_TEMPERATURE = 20;
public static double MAX_TEMPERATURE = 30;</emphasis>
private Random rng;
private boolean isRaining = false;
<emphasis>private double temperature = MIN_TEMPERATURE;</emphasis>
public Weather( Random rng ) {
this.rng = rng;
}
public boolean isRaining() {
return isRaining;
}
<emphasis>public double getTemperature() {
return temperature;
}</emphasis>
public void randomize() {
isRaining = rng.nextDouble() < CHANCE_OF_RAIN;
<emphasis>temperature = MIN_TEMPERATURE +
rng.nextDouble() * (MAX_TEMPERATURE-MIN_TEMPERATURE);</emphasis>
}
}</programlisting>
<para>
Now for the temperature when it is raining. The test will look very similar
the the one I just wrote, except that it expect the temperatures to be
half those when sunny.
</para>
<programlisting>public void testRandomTemperatureRaining() {
MockRandom rng = new MockRandom();
final double RAIN = 0.0;
Weather weather = new Weather( rng );
rng.setNextDoubles( new double[] { RAIN, 0.0 } );
weather.randomize();
assertEquals( "should be min rainy temperature",
Weather.MIN_TEMPERATURE/2, weather.getTemperature(), 0.0 );
rng.setNextDoubles( new double[] { RAIN, 0.5 } );
weather.randomize();
assertEquals( "should be average rainy temperature",
(Weather.MIN_TEMPERATURE + Weather.MAX_TEMPERATURE)/4,
weather.getTemperature(), 0.0 );
rng.setNextDoubles( new double[] { RAIN, 1.0 } );
weather.randomize();
assertEquals( "should be max rainy temperature",
Weather.MAX_TEMPERATURE/2, weather.getTemperature(), 0.0 );
}</programlisting>
<para>
Now I'll change the <classname>Weather</classname>'s
<methodname>randomize</methodname> to half the temperature when it is raining:
</para>
<programlisting>public void randomize() {
temperature = MIN_TEMPERATURE +
rng.nextDouble() * (MAX_TEMPERATURE-MIN_TEMPERATURE);
isRaining = rng.nextDouble() < CHANCE_OF_RAIN;
if( isRaining ) temperature *= 0.5;
}</programlisting>
<para>That was easy! I'll just run my tests and... whoops! The
<methodname>testRandomTemperatureRaining</methodname> test failed.
Not only that, my <methodname>testRandomTemperatureSunny</methodname> test
failed as well! Why did that happen? The behaviour I added to the
<methodname>randomize</methodname> method should not have had an affect
when it was not raining.
</para>
</section>
<section>
<title>Test Smell: Order Shouldn't Matter</title>
<para>Actually, looking at it again, I realise that I swapped the order
of that statements that randomized the rain and temperature.
The tests now initialise the stream of mock random numbers in the wrong
order. This is not good: tests should not be tied to the internal
implementation details of the class. They should specify only its externally
visible behaviour. How can I make my tests less brittle?
</para>
<para>
There should not be any externally visible dependency between randomising
the temperature and randomising the rain. A way to remove the dependency
is to pass <emphasis>two</emphasis> random number generators to the
<classname>Weather</classname> class, one for the temperature and one for
the rain. My tests can then mock each generator independently to force
a particular outcome, no matter what order the <classname>Weather</classname>
samples the generators. Here's the last test rewritten with two generators:
</para>
<programlisting>public void testRandomTemperatureRaining() {
<emphasis>MockRandom rain_rng = new MockRandom();
rain_rng.setNextDouble(0.0);
MockRandom temp_rng = new MockRandom();
Weather weather = new Weather( rain_rng, temp_rng );
temp_rng.setNextDouble( 0.0 );</emphasis>
weather.randomize();
assertEquals( "should be min rainy temperature",
Weather.MIN_TEMPERATURE/2, weather.getTemperature(), 0.0 );
<emphasis>temp_rng.setNextDouble( 0.5 );</emphasis>
weather.randomize();
assertEquals( "should be average rainy temperature",
(Weather.MIN_TEMPERATURE + Weather.MAX_TEMPERATURE)/4,
weather.getTemperature(), 0.0 );
<emphasis>temp_rng.setNextDouble( 1.0 );</emphasis>
weather.randomize();
assertEquals( "should be max rainy temperature",
Weather.MAX_TEMPERATURE/2, weather.getTemperature(), 0.0 );
}</programlisting>
<para>
I can rewrite the <methodname>testRandomTemperatureSunny</methodname>
test in a similar way and I also have to change the
<methodname>testRandomRain</methodname> test to instantiate the
<classname>Weather</classname> object with two random number generators;
we'll skip over the code to save trees.
</para>
<para>
Now I have failing tests for the behaviour I want to implement, so I
need to change my Weather class to use two random number generators:
</para>
<programlisting>public class Weather
{
public static double CHANCE_OF_RAIN = 0.2;
public static double MIN_TEMPERATURE = 20; // degrees C
public static double MAX_TEMPERATURE = 30; // degrees C
<emphasis>private Random tempRandom, rainRandom;</emphasis>
private boolean isRaining = false;
private double temperature = MIN_TEMPERATURE;
<emphasis>public Weather( Random rainRandom, Random tempRandom ) {
this.tempRandom = tempRandom;
this.rainRandom = rainRandom;
}</emphasis>
[...]
public void randomize() {
temperature = MIN_TEMPERATURE +
<emphasis>tempRandom</emphasis>.nextDouble() * (MAX_TEMPERATURE-MIN_TEMPERATURE);
isRaining = <emphasis>rainRandom</emphasis>.nextDouble() < CHANCE_OF_RAIN;
if( isRaining ) temperature *= 0.5;
}
}</programlisting>
<tip>
Only test the order in which an object calls methods of other objects
if that is an important aspect of your object's publically visible
behaviour. If it is unimportant, your tests will be brittle if your
they expect one particular order.
</tip>
<para>
Finally, my <classname>MockRandom</classname> class now contains behaviour
that I don't use, and that I've realised is a bad idea. I'll discard
that code by restoring the original version of the class from my source
code repository.
</para>
</section>
<section>
<title>Refactoring: Too Many Arguments</title>
<para>
My customer is happy. However, she tells me, in Formula One, teams
use tyres with different treads depending on how wet the track is, so
our weather class needs to simulate both if it is raining, and
how wet the track is. Another important element of Formula One strategy
is changing tyres when the weather changes, so the rain should start and
stop at random intervals, and the track should become wetter when it is
raining and dry off when it is sunny.
</para>
<para>
All this is straightforward to implement, but my nose is twitching:
my code, although functional, is smelly.
I can foresee that as I add functionality to the
<classname>Weather</classname> class, the implementation will become
increasingly awkward because there will be too many random number generators.
In particular each time I add more random behaviour I will have to change
all the tests because the signature of the constructor will have changed,
and the constructor will end up with far too many parameters:
</para>
<programlisting>public Weather( Random rainRandom, Random tempRandom, Random wetnessRandom,
Random rainDurationRandom, Random dryDurationRandom )
{
[...]
}</programlisting>
<para>
I recognise this "code smell". Every time I see a method that takes a lot
of arguments, I know that there is a new concept waiting to be extracted.
Just as the method
<methodname>drawRectangle( int x, int y, int width, int height )</methodname>
indicates that a Rectangle class should be factored out and the method
replaced by <methodname>draw( Rectangle r )</methodname>, so all those
<classname>Random</classname> arguments indicate that there is an
weather-specific source of randomness waiting to be extracted.
It's time to don my refactoring hat and clear up this mess now;
leaving it any later will just make more work. The first thing I need
to do is define an interface for a weather-specific source of randomness:
</para>
<programlisting>public interface WeatherRandom
{
boolean nextIsRaining();
double nextTemperature();
}</programlisting>
<para>
I don't have to implement this interface right now.
I can test the <classname>Weather</classname> class by mocking the interface,
and then test and write an implementation once the
<classname>Weather</classname> tests are passing.
Our new <classname>WeatherRandom</classname> class simplifies our tests
greatly. We no longer have to test that the <classname>Weather</classname>
class compares probabilities against random numbers correctly; that will
be the responsibility of the real implementation of
<classname>WeatherRandom</classname>. Instead we just have to test that
the <classname>Weather</classname> class stores the random weather and
calculates a cooler temperature when it is raining. Here's our
<methodname>testRandomTemperatureRaining</methodname> test:
</para>
<programlisting>public void testRandomTemperatureRaining() {
final double TEMPERATURE = 20;
MockWeatherRandom rng = new MockWeatherRandom() {
public boolean nextIsRaining() { return true; }
public double nextTemperature() { return TEMPERATURE; }
};
Weather weather = new Weather( rng );
weather.randomize();
assertEquals( "temperature",
TEMPERATURE/2.0, weather.getTemperature(), 0.0 );
}</programlisting>
<para>
Now I have to modify the <classname>Weather</classname> class to
use a <classname>WeatherRandom</classname> to pass the tests:
</para>
<programlisting>public class Weather
{
<emphasis>private WeatherRandom random;</emphasis>
private boolean isRaining = false;
private double temperature = 0.0;
<emphasis>public Weather( WeatherRandom random ) {
this.random= random;
}</emphasis>
[...]
public void randomize() {
<emphasis>temperature = random.nextTemperature();
isRaining = random.nextIsRaining();</emphasis>
if( isRaining ) temperature *= 0.5;
}
}</programlisting>
<para>
And finally I need to test and implement a real
<classname>WeatherRandom</classname> that generates random weather
using a random number generator.
Each of the <classname>WeatherRandom</classname> methods can be tested
individually using a MockRandom object, just as we did in our earlier
tests of the <classname>Weather</classname> class. Because each method
makes one call to the random number generator, no internal
implementation details leak out into our tests.
</para>
<programlisting>public void testNextIsRaining() {
MockRandom rng = new MockRandom();
WeatherRandom weather_random = new DefaultWeatherRandom(rng);
rng.setNextDouble( 0.0 );
assertTrue( "is raining", weather_random.nextIsRaining() );
rng.setNextDouble( DefaultWeatherRandom.CHANCE_OF_RAIN );
assertTrue( "is not raining", !weather_random.nextIsRaining() );
rng.setNextDouble( 1.0 );
assertTrue( "is not raining", !weather_random.nextIsRaining() );
}</programlisting>
<para>
I can now go on to implement the additional random behaviour requested
by my customer. I will define each additional random effect as a method
in the <classname>WeatherRandom</classname> interface. I will also define
a sensible default result in the <classname>MockWeatherRandom</classname>
so that tests that do not care about the effect do not have to be changed.
</para>
<programlisting>public interface WeatherRandom
{
boolean nextIsRaining();
double nextTemperature();
<emphasis>double nextWetness();
double nextWetDuration();
double nextDryDuration();</emphasis>
}</programlisting>
</section>
<section>
<title>What Have We Learned?</title>
<remark>Expand this</remark>
<para>
Test random behaviour by pulling the random number generator out of the
object being tested and mocking it.
</para>
<para>
Mocking random behaviour can expose implementation details by assuming
how individual elements of a random number sequence will be used by
the class under test. This breaks encapsulation and makes tests brittle.
Avoid brittle tests by "parallelizing" your random number streams.
Initialise your objects with multiple sources of randomness that can be
mocked independently to test specific behaviours.
</para>
<para>
Define application-specific random generators, rather than passing multiple
random number generators into your class, so that changes to the random
behaviour of your class do not cause changes to ripple through your class
and all of its tests.
</para>
</section>
</chapter>
|