次の方法で共有


Testing Against Randomness

This is the first in a small series of posts about testing against non-determinism. In this installation, I'm going to cover randomness.

When code paths are selected at random, the outcome of a unit test may not be deterministic. Consider this example:

 public bool DoRandomStuff()
 {
     Random r = new Random();
  
     if (r.NextDouble() < .5)
     {
         return false;
     }
     return true;
 }

This method has a fifty percent chance of returning either true or false, but you if you write a unit test, you will not be able to tell what the result will be in advance. In this very simplified case it may not be of much consequence, but if you have more complex logic, you will want to be able to test any possible code path and their return values, and you can't do that if a part of the code path is determined by uncontrolled randomness.

The key is obviously to inject controlled randomness. With the Random class, this is actually fairly easy, since it provides an overloaded constructor that takes a seed, and as you probably know, the sequence of random numbers generated is always the same for the same seed, and hence, deterministic.

This doesn't help a lot unless you feed either the seed or the entire Random instance into your class from the outside. I personally prefer to inject the entire Random instance, since that seems more natural: It clearly signifies to the caller that randomness may happen in this class; at least more clearly than some integer seed. You can choose to pass the Random instance to the class as a whole, or just pass it as a parameter to a single method. In this example, I'm using Construtor Injection to pass it to the class in its entirety, since that doesn't break the signature of the DoRandomStuff method:

 public class RandomConsumer
 {
     private Random random_;
  
     public RandomConsumer(Random r)
     {
         this.random_ = r;
     }
  
     public bool DoRandomStuff()
     {
         if (this.random_.NextDouble() < .5)
         {
             return false;
         }
         return true;
     }
 }

As you can tell, I'm just populating the class with an already created instance of Random, and I use that instance to get my random numbers. From a unit test, I can then test the method like this:

 [TestMethod]
 public void GetTrue()
 {
     RandomConsumer rc = new RandomConsumer(new Random(0));
  
     Assert.IsTrue(rc.DoRandomStuff());
 }

A seed value of 0 causes NextDouble to return approximately 0.7, which causes the DoRandomStuff method to return true. A seed of 1 will cause NextDouble to return approximately 0.2, which will result in false, so you can write a test similar to the one above with a seed of 1 and test that the return value is false.

While you would inject a seeded Random into the class from a unit test, you would obviously use it with an unseeded Random in production code to introduce true(r) randomness:

 RandomConsumer rc = new RandomConsumer(new Random());

Note that in this example, I've used the Constructor Injection pattern to keep things simple. However, as I wrote in my post about Provider Injection, this has the disadvantage of requiring early initialization of the Random instance, even if it's not going to be used right away. In real production code, I'd typically used Provider Injection and pass a provider that can create Random instances when I need them; thus allowing for lazy initialization.

Next: Testing Against Guids

Comments