Yes, it is in fact possible to test randomness within your application! Like with so many other facets of your code, it’s important too. Whether you’re making a complex game or a simple random featured item selector on a web page, verifying that your code behaves properly given random input is essential. Let’s see what we can do with some of what C# has to offer in terms of random features.
Random.Shared
A lot of C# developers will see the System.Random
class on a cursory search for “how do I get a random outcome” and get straight to using it. Out of the box it’ll get what you want easily! For a random number, one merely provides a range of values to myRandomInstance.Next()
and it’s yours. Many will then go on to ask “how do I manage my instances of the Random
class efficiently?” Should you maintain a single static instance? New one up every time you need it?
Well, as of .NET 6, the framework provides the answer for you. The Random
class has a static, thread-safe instance of itself, accessible via a property called Shared
on the Random
class. Anywhere in your code you can call System.Random.Shared.Next()
and be off to the races with whatever random numbers you need. No more managing instances of Random
on your own!
As of .NET 8, if you’re needing random elements out of an array or other ReadOnlySpan<T>
, GetItems()
has you covered. Prior to this, you’d have to write code akin to var randomItem = items[random.next(0, items.Length)];
to pull a random element out of an array. I strongly suspect this is why Random.Next(int minValue, int maxValue)
treats maxValue
as an exclusive upper bound, as otherwise you’d have to write items.Length - 1
.
So, we have our instance of Random
. How do we test it?
Static Cling
Accessing a global static instance of anything, Random
or otherwise, makes code harder to test. This is a code smell known as Static Cling. Consider the following.
using System;
namespace SuperCoolRPG;
public class BattleCalculator
{
public int CalculateDamage(int weaponStrength, int characterStrength)
{
return weaponStrength + characterStrength + Random.Shared.Next(5, 31);
}
}
Getting the amount of damage dealt by a character’s attack here is a simple formula: Add the strengths up alongside a random value between 5 and 30 (recall the 2nd parameter is an exclusive upper bound). This is a pretty simple calculation, but real games have far more complex math going on. For example, the damage calculation in Pokémon has far more inputs and operations happening. We’d like to unit test our calculations for various random results to make sure our game is properly balanced and doesn’t give off the wall results.
To accomplish this, we’re going to take our shared instance of Random
and Wrap It Up!
Controlling Chaos with an Abstraction for Random
Random
has a myriad of methods available (including the static Shared
property even). When one creates an interface to be used for dependency injection, one only needs to provide what’s being used. It can be daunting and exhaustive to try and wrap and mirror everything. For our class, we only use one overload of Next()
, so that’s all we’ll do for now. We can always add additional implementations as needed later.
public interface IRandom
{
/// <inheritdoc cref="System.Random.Next(int,int)" />
int Next(int minValue, int maxValue);
}
public class SystemRandom : IRandom
{
/// <inheritdoc />
public int Next(int minValue, int maxValue) => System.Random.Shared.Next(minValue, maxValue);
}
Using “inheritdoc” to Spill the Beans
Our wrapper class SystemRandom
just calls to the Shared
instance to do real randomization. Our interface exposes the documentation available to the real Next()
method using inheritdoc
, with cref
pointing to the original method. This will maintain the documentation when wanting to discover details about the method when you’re working in an IDE like Visual Studio or JetBrains Rider. No need to copy and paste! We do this because we know there’s just one implementation of IRandom
(a wrapping class of System.Random
). If there existed multiple IRandom
implementations it would be inaccurate to use System.Random.Next
, but because we know it always will be, we can allow our interface to “know about the implementation details” for the sake of making the documentation easier to read when using IRandom
.
Using the Abstraction
Let’s modify our BattleCalculator
with our new interface.
public class BattleCalculator(IRandom random)
{
public int CalculateDamage(int weaponStrength, int characterStrength)
{
return weaponStrength + characterStrength + random.Next(5, 31);
}
}
Nothing changes about the real behavior. In fact, you could write integration tests for various scenarios by simply providing a new instance of SystemRandom
to the BattleCalculator
constructor. But of course we’re here to control the chaos of randomization! Now that we have our IRandom
interface, it’s trivial to mock it using your mocking tool of choice, and test to your heart’s content.
Here’s an example using NSubstitute.
using FluentAssertions;
using NSubstitute;
using SuperCoolRPG;
namespace SuperCoolRPGTests.BattleCalculatorTests;
public class CalculateDamage
{
private readonly IRandom _mockRandom = Substitute.For<IRandom>();
private readonly BattleCalculator _calculator;
public CalculateDamage()
{
_calculator = new(_mockRandom);
}
[Fact]
public void AddsRandomValueToDamage()
{
const int randomValue = 50;
const int weaponStrength = 95;
const int characterStrength = 5;
_mockRandom.Next(Arg.Any<int>(), Arg.Any<int>()).Returns(randomValue);
int damage = _calculator.CalculateDamage(95, 5);
damage.Should().Be(weaponStrength + characterStrength + randomValue);
}
}
Had we been stuck with real randomization, there’s no way we could have written the above test. Well, to be precise, we could have written it, but it would have failed most of the time! But since we controlled randomization in our application via interface, we now have the capability of controlling randomization to validate any assertions we want to make about our code’s behavior.
Happy Holidays!
That’ll do it for my blog posts this year. Happy holidays, everyone!