0

I'm trying to test the behaviour of a method to generate a URL with a few parameters. The method makes use of a helper method which in turn makes use of new Random().nextDouble().

private static String generateChecksum() {
    return "s" + new Double(Math.floor(Instant.now().getEpochSecond() / 10800000)).longValue() % 10 +
            new Double(Math.floor(10000000000000L * new Random().nextDouble())).longValue();
}

My goal for this particular test is to ensure the output of the method matches what I expect. My issue is I can't predict the result of Random.nextDouble().

I read you can mock the results and I thought that overriding every instance of that method. I found out I couldn't do that but also that I shouldn't, but instead I should re-write my code to fit my test.

How do I rewrite it? Do I pass the checksum into the method pre-generated along with the parameters so my flow goes from:

1. Main()
2. generateUrl(params)
2a. generateChecksum() // for use in generateUrl
3. return URL

to

1. Main()
2. generateChecksum()
3. generateUrl(params, checksum)
4. return URL

Or am I missing something? Thanks!

Nanor
  • 2,400
  • 6
  • 35
  • 66

4 Answers4

1

In order to make such code testable, you there are two things that matter:

  • you want to somehow fake the system clock. This article tells you how you can do that in Java 8 by using the Clock class
  • when random comes in, simply use a seed. Which allows you to "predict" the random numbers that will come up in your production code.

In other words: you can control the time and the random number that your production code will see!

Alternatively, simple delegate the whole computation into something that you can easily mock, such as:

public class CheckSumGenerator {

  public String generate(...) { ...

Now, you can easily mock that class, and have it return whatever you want it to return. Without thinking about clocks or random numbers.

But keep in mind: setting a seed might still be a good idea, and ideally, you might want to use a secure random number as seed.

GhostCat
  • 137,827
  • 25
  • 176
  • 248
  • I wouldn't fake the system clock. Ultimately the problem is that the static methods calls make the class untestable. You solve that by pulling out the static method calls. – Michael May 21 '19 at 09:24
  • @Michael Enhanced my answer around that idea. – GhostCat May 21 '19 at 09:31
  • The Clock usage (as alternative to change the system clock) requires some changes but is fine. And for `new Random(42)` the same holds. In fact unit-tests are the intended use-cases for those constructs. Good you added the change for mocking the use of Clock and Random. – Joop Eggen May 21 '19 at 09:35
1

The simplest solution is to add a new parameter holding that random number to your methods:

private static String generateChecksum(double r) {
    return "s" + new Double(Math.floor(Instant.now().getEpochSecond() / 10800000)).longValue() % 10 +
            new Double(Math.floor(10000000000000L * r)).longValue();
}

static String generateUrl(double r) {
    // call generateChecksum(r)
}

public static String generateUrl() {
    // trivial
    return generateUrl(new Random().nextDouble());
}

Now you can just test generateUrl(double r) method easily. You don't need to test generateUrl() method because it is trivial.

Nghia Bui
  • 3,694
  • 14
  • 21
0

You can't test something when the results are not known ahead of time.

You need to inject the source of randomness as a dependency so that you can supply constants in your tests.

public class MyClass {
    private final Supplier<Double> randomNumberGenerator;

    public MyClass(Supplier<Double> randomNumberGenerator) {
        this.randomNumberGenerator = randomNumberGenerator;
    }

    private String generateChecksum() {
        //... randomNumberGenerator.get();
    }
}

In your application code, create the object like this:

new MyClass(() -> new Random().nextDouble());

In your test, create it like this, for example:

new MyClass(() -> 10.0);

You can do the same thing with the timestamp. Supplier<Instant>, Instant::now, () -> Instant.ofEpochSecond(123456)

Michael
  • 41,989
  • 11
  • 82
  • 128
0

While it is true that you do not know the exact output of the method using a random component, I don't think it is necessary to test for any exact output, but rather for the correct format of the output, i.e., validate that the output is a valid URL instead of a specific instance where you eliminate the randomness.

Of course the output might pass this check by chance, but by picking a specific seed you essentially run into the same problem.

floxbr
  • 144
  • 1
  • 9