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