Tuesday, March 11, 2025

google test for a code that has random number generator

 Writing tests for code that uses a random number generator (RNG) can be tricky, as random behavior introduces non-determinism into your tests. However, you can make testing easier by controlling or "mocking" the randomness. There are several strategies you can use to test code that involves random numbers, particularly when using a testing framework like Google Test.

Here are the steps to test code that uses a random number generator:

1. Refactor to Allow Injecting the RNG

If possible, refactor your code to allow the random number generator to be injected (dependency injection). This allows you to substitute the random number generator with a mock or predictable RNG during testing.

Example Code to Refactor:

cpp
#include <iostream> #include <random> class RandomGenerator { public: virtual int generate() = 0; }; class RealRandomGenerator : public RandomGenerator { public: int generate() override { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(1, 100); return dis(gen); } }; class MyClass { private: RandomGenerator* rng; public: MyClass(RandomGenerator* rng) : rng(rng) {} int getRandomValue() { return rng->generate(); } };

In this refactored code, RandomGenerator is an abstract class that is implemented by RealRandomGenerator. The MyClass class takes a pointer to a RandomGenerator in its constructor, allowing you to inject different implementations for testing purposes.

2. Write Google Test for Injected RNG

Now, you can write a test that uses a mock RNG. In Google Test, you can use Google Mock to create a mock class for RandomGenerator that returns predictable values.

Step-by-Step Google Test Example:

cpp
#include <gtest/gtest.h> #include <gmock/gmock.h> // Mock class for RandomGenerator class MockRandomGenerator : public RandomGenerator { public: MOCK_METHOD(int, generate, (), (override)); }; // Test for MyClass TEST(MyClassTest, GetRandomValue) { MockRandomGenerator mockRng; // Set up the mock to return predictable values EXPECT_CALL(mockRng, generate()) .WillOnce(testing::Return(42)) // First call will return 42 .WillOnce(testing::Return(99)); // Second call will return 99 MyClass obj(&mockRng); // Test that the first call returns 42 EXPECT_EQ(obj.getRandomValue(), 42); // Test that the second call returns 99 EXPECT_EQ(obj.getRandomValue(), 99); }

Key Elements of the Test:

  1. MockRandomGenerator: The mock class simulates the behavior of the RandomGenerator. We use Google Mock's MOCK_METHOD macro to create the mock method generate().

  2. EXPECT_CALL: We use EXPECT_CALL to specify that the generate() method should be called, and we use WillOnce(testing::Return(value)) to define what value it should return when called.

  3. Testing: After injecting the mock into the class, we test the behavior of the MyClass methods. We verify that the correct value is returned when the random number is generated, allowing us to validate the logic without involving true randomness.

3. Test with Real RNG (Optional)

If you want to test the real random number generator as well, you can do so by controlling the seed or by using a known seed value in the random generator.

cpp
TEST(MyClassTest, RealRandomValue) { RealRandomGenerator realRng; MyClass obj(&realRng); // Since random numbers are unpredictable, we can't check the exact number, // but we can test if the value falls within a certain range. int value = obj.getRandomValue(); EXPECT_GE(value, 1); EXPECT_LE(value, 100); }

In this test, we’re not testing for a specific random number (since it's truly random), but we can verify that the generated value falls within the expected range (1 to 100 in this case).

4. Use a Fixed Seed for Predictability

If you want to make the tests more deterministic but still use a real RNG, you can fix the random number generator's seed to a known value. This way, the sequence of random numbers generated will always be the same across runs.

cpp
class FixedSeedRandomGenerator : public RandomGenerator { public: int generate() override { static std::mt19937 gen(12345); // Fixed seed for predictability std::uniform_int_distribution<> dis(1, 100); return dis(gen); } }; TEST(MyClassTest, FixedSeedRandomValue) { FixedSeedRandomGenerator fixedRng; MyClass obj(&fixedRng); // Now the sequence of numbers generated is fixed, so we can test it deterministically. EXPECT_EQ(obj.getRandomValue(), 25); // Fixed result from fixed seed }

5. Testing Edge Cases

You can test edge cases like generating the minimum or maximum possible random values:

cpp
TEST(MyClassTest, EdgeCaseRandomValue) { MockRandomGenerator mockRng; EXPECT_CALL(mockRng, generate()) .WillOnce(testing::Return(1)); // Min value EXPECT_CALL(mockRng, generate()) .WillOnce(testing::Return(100)); // Max value MyClass obj(&mockRng); // Test the min value EXPECT_EQ(obj.getRandomValue(), 1); // Test the max value EXPECT_EQ(obj.getRandomValue(), 100); }

Summary:

To effectively test code that involves randomness:

  • Refactor your code to allow dependency injection of the RNG.
  • Use Google Mock to mock or control the behavior of the RNG.
  • You can use a fixed seed for reproducible tests.
  • Validate that your code behaves correctly even with predictable/random values within expected ranges.

By following these strategies, you can create reliable and deterministic tests for code that involves random number generation.

No comments:

Post a Comment