From: Nat P. <np...@us...> - 2002-08-14 13:55:03
|
Update of /cvsroot/mockobjects/no-stone-unturned/doc/xdocs In directory usw-pr-cvs1:/tmp/cvs-serv8105/doc/xdocs Modified Files: random.xml Log Message: Incorporated changes suggested by TimM. Wrote introduction. Index: random.xml =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/doc/xdocs/random.xml,v retrieving revision 1.5 retrieving revision 1.6 diff -u -r1.5 -r1.6 --- random.xml 10 Aug 2002 23:43:41 -0000 1.5 +++ random.xml 14 Aug 2002 13:54:58 -0000 1.6 @@ -1,26 +1,35 @@ <chapter status="draft"> - <title>Random Acts</title> +<title>Random Acts</title> - <chapterinfo> - <author>Nat Pryce</author> - <copyright><year>2002</year> <holder>Nat Pryce</holder></copyright> - </chapterinfo> +<chapterinfo> + <author>Nat Pryce</author> + <copyright><year>2002</year> <holder>Nat Pryce</holder></copyright> +</chapterinfo> - <section> - <title>Introduction</title> - - <comment>Expand this</comment> - - <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> +<section> +<title>Introduction</title> - <comment>How do we test randomness?</comment> +<para> +Pseudo-random behaviour is used in many applications. +For example, in video games pseudo-randomness 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> - </section> +<para> +Testing random behaviour is not complex. You will not need to use +statistical analysis, unless you are actually writing a new random +number generator. Instead, as in many test driven designs, you +will end up separating the objects that direct activity from +those that effect activity so that you can mock one to test +the other. In the case of random behaviour, the object directing +the activity is a random number generator. You can use a mock +generator to feed deterministic values into the objects under test. +So far, soo good, but it turns out that testing random behaviour +highlights interesting subtleties with test driven design in general. +</para> +</section> <section> <title>Come Rain...</title> @@ -53,19 +62,19 @@ </para> <programlisting lang="java">public void testRandomRain() { - MockRandom rng = new MockRandom(); + MockRandom random = new MockRandom(); - Weather weather = new Weather( rng ); + Weather weather = new Weather( random ); - rng.setNextDouble( 0.0 ); + random.setNextDouble( 0.0 ); weather.randomize(); assertTrue( "is raining", weather.isRaining() ); - rng.setNextDouble( Weather.CHANCE_OF_RAIN ); + random.setNextDouble( Weather.CHANCE_OF_RAIN ); weather.randomize(); assertTrue( "is not raining", !weather.isRaining() ); - rng.setNextDouble( 1.0 ); + random.setNextDouble( 1.0 ); weather.randomize(); assertTrue( "is not raining", !weather.isRaining() ); }</programlisting> @@ -98,11 +107,11 @@ public static double CHANCE_OF_RAIN = 0.2; - private Random rng; + private Random random; private boolean isRaining = false; - public Weather( Random rng ) { - this.rng = rng; + public Weather( Random random ) { + this.random = random; } public boolean isRaining() { @@ -110,10 +119,9 @@ } public void randomize() { - isRaining = rng.nextDouble() < CHANCE_OF_RAIN; + isRaining = random.nextDouble() < CHANCE_OF_RAIN; } }</programlisting> - </section> @@ -171,23 +179,23 @@ <programlisting>public void testRandomTemperatureSunny() { - MockRandom rng = new MockRandom(); + MockRandom random = new MockRandom(); final double SUNNY = 1.0; - Weather weather = new Weather( rng ); + Weather weather = new Weather( random ); - rng.setNextDoubles( new double[] { SUNNY, 0.0 } ); + random.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 } ); + random.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 } ); + random.setNextDoubles( new double[] { SUNNY, 1.0 } ); weather.randomize(); assertEquals( "should be max temperature", Weather.MAX_TEMPERATURE, weather.getTemperature(), 0.0 ); @@ -203,12 +211,12 @@ <emphasis>public static double MIN_TEMPERATURE = 20; public static double MAX_TEMPERATURE = 30;</emphasis> - private Random rng; + private Random random; private boolean isRaining = false; <emphasis>private double temperature = MIN_TEMPERATURE;</emphasis> - public Weather( Random rng ) { - this.rng = rng; + public Weather( Random random ) { + this.random = random; } public boolean isRaining() { @@ -220,36 +228,36 @@ }</emphasis> public void randomize() { - isRaining = rng.nextDouble() < CHANCE_OF_RAIN; + isRaining = random.nextDouble() < CHANCE_OF_RAIN; <emphasis>temperature = MIN_TEMPERATURE + - rng.nextDouble() * (MAX_TEMPERATURE-MIN_TEMPERATURE);</emphasis> + random.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 +the the one I just wrote, except that it expects the temperatures to be half those when sunny. </para> <programlisting>public void testRandomTemperatureRaining() { - MockRandom rng = new MockRandom(); + MockRandom random = new MockRandom(); final double RAIN = 0.0; - Weather weather = new Weather( rng ); + Weather weather = new Weather( random ); - rng.setNextDoubles( new double[] { RAIN, 0.0 } ); + random.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 } ); + random.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 } ); + random.setNextDoubles( new double[] { RAIN, 1.0 } ); weather.randomize(); assertEquals( "should be max rainy temperature", Weather.MAX_TEMPERATURE/2, weather.getTemperature(), 0.0 ); @@ -262,9 +270,9 @@ <programlisting>public void randomize() { temperature = MIN_TEMPERATURE + - rng.nextDouble() * (MAX_TEMPERATURE-MIN_TEMPERATURE); + random.nextDouble() * (MAX_TEMPERATURE-MIN_TEMPERATURE); - isRaining = rng.nextDouble() < CHANCE_OF_RAIN; + isRaining = random.nextDouble() < CHANCE_OF_RAIN; if( isRaining ) temperature *= 0.5; }</programlisting> @@ -275,9 +283,9 @@ <methodname>randomize</methodname> method should not have had an effect when it was not raining. </para> - </section> + <section> <title>Test Smell: Order Shouldn't Matter</title> @@ -303,30 +311,37 @@ </para> <programlisting>public void testRandomTemperatureRaining() { - <emphasis>MockRandom rain_rng = new MockRandom(); - rain_rng.setNextDouble(0.0); + <emphasis>MockRandom rain_random = new MockRandom(); + rain_random.setNextDouble(0.0); - MockRandom temp_rng = new MockRandom(); + MockRandom temp_random = new MockRandom(); - Weather weather = new Weather( rain_rng, temp_rng ); + Weather weather = new Weather( rain_random, temp_random ); - temp_rng.setNextDouble( 0.0 );</emphasis> + temp_random.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> + <emphasis>temp_random.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> + <emphasis>temp_random.setNextDouble( 1.0 );</emphasis> weather.randomize(); assertEquals( "should be max rainy temperature", Weather.MAX_TEMPERATURE/2, weather.getTemperature(), 0.0 ); }</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> I can rewrite the <methodname>testRandomTemperatureSunny</methodname> test in a similar way and I also have to change the @@ -366,15 +381,8 @@ } }</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 +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. @@ -443,21 +451,72 @@ 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 +<classname>WeatherRandom</classname>. Instead we 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: +calculates a cooler temperature when it is raining. +However, we also have to test that our <classname>Weather</classname> +class does not make multiple calls to the methods of the +<classname>WeatherRandom</classname> object. If it does, my tests +are showing me that I need to define define more methods in the +<classname>WeatherRandom</classname> interface to avoid more problems like +we have just encountered. +I will create a <classname>MockWeatherRandom</classname> object that +uses the <classname>ExpectationCounter</classname> class from the +Mock Objects framework: +</para> + +<remark>Need to add cross reference to the chapter on Expectations.</remark> + +<programlisting>import com.mockobjects.ExpectationCounter; + +public class MockWeatherRandom + implements WeatherRandom +{ + private ExpectationCounter nextIsRainingCounter = + new ExpectationCounter("nextIsRaining"); + private boolean nextIsRainingResult = false; + + private ExpectationCounter nextTemperatureCounter = + new ExpectationCounter("nextTemperature"); + private double nextTemperatureResult = 0.0; + + + public void setupNextIsRaining( boolean result ) { + nextIsRainingCounter.setExpected(1); + nextIsRainingResult = result; + } + + public boolean nextIsRaining() { + nextIsRainingCounter.inc(); + return nextIsRainingResult; + } + + public void setupNextTemperature( double result ) { + nextTemperatureCounter.setExpected(1); + nextTemperatureResult = result; + } + + public double nextTemperature() { + nextTemperatureCounter.inc(); + return nextTemperatureResult; + } +}</programlisting> + +<para> +Here's the new version of the +<methodname>testRandomTemperatureRaining</methodname> test that uses +the <classname>MockWeatherRandom</classname> class. The other tests +look similar; again I'll skip the code in the interests of forestry. </para> <programlisting>public void testRandomTemperatureRaining() { final double TEMPERATURE = 20; - MockWeatherRandom rng = new MockWeatherRandom() { - public boolean nextIsRaining() { return true; } - public double nextTemperature() { return TEMPERATURE; } - }; + MockWeatherRandom random = new MockWeatherRandom(); + random.setupNextIsRaining( true ); + random.setupNextTemperature( TEMPERATURE ); - Weather weather = new Weather( rng ); + Weather weather = new Weather( random ); weather.randomize(); assertEquals( "temperature", @@ -500,16 +559,16 @@ </para> <programlisting>public void testNextIsRaining() { - MockRandom rng = new MockRandom(); - WeatherRandom weather_random = new DefaultWeatherRandom(rng); + MockRandom random = new MockRandom(); + WeatherRandom weather_random = new DefaultWeatherRandom(random); - rng.setNextDouble( 0.0 ); + random.setNextDouble( 0.0 ); assertTrue( "is raining", weather_random.nextIsRaining() ); - rng.setNextDouble( DefaultWeatherRandom.CHANCE_OF_RAIN ); + random.setNextDouble( DefaultWeatherRandom.CHANCE_OF_RAIN ); assertTrue( "is not raining", !weather_random.nextIsRaining() ); - rng.setNextDouble( 1.0 ); + random.setNextDouble( 1.0 ); assertTrue( "is not raining", !weather_random.nextIsRaining() ); }</programlisting> |