17

What is wrong with the UUID.randomUUID() - it just can't be mocked

Is it possible to mock? Or i have an error in my source?

Look at example:

1) Class that is tested

package com.grayen;

import java.util.UUID;

public class TestedClass {
    public UUID getUuid() {
        return UUID.randomUUID();
    }
    public UUID getUuidFromWrapper() {
        return UuidWrapper.randomUUID();
    }
}

One method uses a wrapper for UUID and i can mock that wrapper!

2) Wrapper for real UUID (all modifiers the same)

package com.grayen;

import java.util.UUID;

public final class UuidWrapper {
    public static UUID randomUUID() {
        return UUID.randomUUID();
    }
}

3) Testing (last commented line throws an exception)

package com.grayen;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.util.UUID;
import static org.junit.Assert.assertEquals;

@PrepareForTest({UUID.class, UuidWrapper.class})
@RunWith(PowerMockRunner.class)
public class TestedClassTest {

    @Test
    public void testMethod() {
        UUID uuid = UUID.randomUUID();

        PowerMockito.mockStatic(UUID.class);
        PowerMockito.mockStatic(UuidWrapper.class);

        PowerMockito.when(UUID.randomUUID()).thenReturn(uuid);
        PowerMockito.when(UuidWrapper.randomUUID()).thenReturn(uuid);

        TestedClass testedClass = new TestedClass();

        assertEquals(uuid, testedClass.getUuidFromWrapper());
        //assertEquals(uuid, testedClass.getUuid());
    }
}
Michael Lihs
  • 7,460
  • 17
  • 52
  • 85
Maksim Petrov
  • 313
  • 1
  • 2
  • 7
  • 3
    You should add `TestedClass` to `@PrepareForTest` – k5_ Sep 06 '18 at 17:34
  • 1
    The intent of the test is suspect. Consider removing the mocking and simply `assertFalse(testClass.getUuid().equals(testClass.getUuid())`. – Andrew S Sep 06 '18 at 17:54
  • It seems like either your test is wrong, or your test is telling you that the tested class' implementation is wrong. Practically speaking, `randomUUID()` is guaranteed to never turn the same result twice. Maybe `TestedClass` should have a UUID member? I don't understand the purpose of this test; it seems like you are testing PowerMock more than your own code. – erickson Sep 06 '18 at 18:11

2 Answers2

16

Mocking a static method is always a fragile approach. If possible, prefer to use a non-static UUID source, which then can be easily mocked.

For example:

/**
 * A source of new {@link UUID} instances.
 */
public interface UuidSource {
    /**
     * Returns a new {@link UuidSource} that generates UUIDs using {@link UUID#randomUUID}.
     */
    public static UuidSource random() {
        return UUID::randomUUID;
    }

    /**
     * Returns a new {@link UUID} instance.
     *
     * <p>The returned value is guaranteed to be unique.
     */
    UUID newUuid();
}

Then you can inject it into TestedClass, or have TestedClass have a private member:

public class TestedClass {
    private UuidSource uuidSource = UuidSource.random();

    public UUID getUUID() {
        return uuidSource.newUuid();
    }
    // etc.
}

Then to test it, you can either have a test-only constructor, to allow injecting in a mocked UuidSource, or you could replace the value of the uuidSource field directly (either by widening its visibility or by using reflection or something).

As a bonus: this decouples your actual production code from UUID.randomUUID(). If later you decide that you need to use version 2 UUIDs (datetime-based) or some other version rather than random UUIDs, you can easily change that in your production code as well. This is what people mean when they say that making your code more testable usually leads to a better overall design.

Daniel Pryden
  • 59,486
  • 16
  • 97
  • 135
  • 4
    I usually have this construct for UUID or Instant public class TestedClass { private Supplier uuidSupplier = UUID::randomUUID; public void foo() { UUID uuid = uuidSupplier.get(); } } – Nils Rommelfanger Sep 12 '18 at 13:39
5

So I took your code and got it to work. All you need to do is add TestedClass.class to your @PrepareForTest.

package com.grayen;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.util.UUID;
import static org.junit.Assert.assertEquals;

@PrepareForTest({
    UUID.class, 
    UuidWrapper.class, 
    TestedClass.class
})
@RunWith(PowerMockRunner.class)
public class TestedClassTest {

    @Test
    public void testMethod() {
        UUID uuid = UUID.randomUUID();

        PowerMockito.mockStatic(UUID.class);
        PowerMockito.mockStatic(UuidWrapper.class);

        PowerMockito.when(UUID.randomUUID()).thenReturn(uuid);
        PowerMockito.when(UuidWrapper.randomUUID()).thenReturn(uuid);

        TestedClass testedClass = new TestedClass();

        assertEquals(uuid, testedClass.getUuidFromWrapper());
        assertEquals(uuid, testedClass.getUuid());
    }
}

Edit

I did not see the comment, but k5_ pointed it out.

CodeMonkey
  • 1,136
  • 16
  • 31