javamultithreadingtimespock

Update Spock mock from different threads


I am testing code that runs in its own thread in a while loop. The code under test does some stuff based on the current time, then updates an AtomicReference<Integer>

The application logic is an algorithm that runs for days. I need to simulate the behavior by controlling the time readings, and checking the computed results.

I can't figure out how to mock the time. When clock time gets updated from Spock test, the ClassUnderTest.run() thread does not see the new value.

I provided simplified code examples below.

Code under test

class ClassUnderTest {
    private ExecutorService executorService = ...
    private MyClock clock;
    private AtomicReference<Integer> result = ...

    public ClassUnderTest(MyClock clock) {
        this.clock = clock;

        // start the thread
        this.executorService.submit(this::run);
    }

    public void run() {
        while (true) {
            if (hasThreeHoursPassed(clock.getTime()) { // <-- PROBLEM, can't mock getTime() from Spock thread
                // ... core application logic that requires testing ...
                result.set(...);
            }
        }
   }
}

Spock test

def 'test thread' () {
    given:
    MyClock clockMock = Mock(MyClock)
    ClassUnderTest classUnderTest = ClassUnderTest(clockMock)
    long mockedTime = 1L;
    clockMock.getTime() >> mockedTime

    expect:
    for (int i = 0; i < 100; i++) {
        mockedTime += 1000 * 60 * 60 * 3 // 3 hours

        clockMock.getTime() >> mockedTime // <-- PROBLEM, this does not work. The thread does not see this value

        classUnderTest.getResult() == ... expected value ...
    }


}

It is important to note that the logic being tested is itself the behavior of the code being executed over time. I tried to keep the code sample simple for readability. The real application logic is an algorithm that maintains a data structure that changes over time (it is a time based decaying algorithm to rank popular content based on views).


Solution

  • If at all possible I'd advise you to refactor your ClassUnderTest so that you can just invoke the logic directly.

    class ClassUnderTest {
        private ExecutorService executorService = ...
        private MyClock clock;
        private AtomicReference<Integer> result = ...
    
        public ClassUnderTest(MyClock clock) {
            this.clock = clock;
    
            // start the thread
            this.executorService.submit(this::run);
        }
    
        public void run() {
            while (true) {
                if (hasThreeHoursPassed(clock.getTime()) { // <-- PROBLEM, can't mock getTime() from Spock thread
                    performApplicationLogic()
                }
            }
       }
    
       @VisibleForTesting
       void performApplicationLogic() {
           // ... core application logic that requires testing ...
           result.set(...);
       }
    }
    

    Then you can just call it a 100 times if you need. I would also decouple the executor submit from the constructor, or add another constructor that doesn't start the thread as it will not be necessary for the testing.

    def 'test decoupled' () {
        given:
        MyClock clockMock = Mock(MyClock)
        ClassUnderTest classUnderTest = ClassUnderTest(clockMock, false) // new constructor with false indicating not starting the test.
        long mockedTime = 1L;
        clockMock.getTime() >> mockedTime
    
        when:
        def results = []
        for (int i = 0; i < 100; i++) {
            classUnderTest.performApplicationLogic()
            results << classUnderTest.getResult()
        }
    
        then:
        // assert that the results match the expected values
    }
    

    If you still need to modify the time returned by the mock then use an AtomicLong and a computed return value.

        def mockedTime = new AtomicLong(1L);
        clockMock.getTime() >> { mockedTime.get() }
    
        when:
        def results = []
        for (int i = 0; i < 100; i++) {
            mockedTime.addAndGet(1000 * 60 * 60 * 3)  // 3 hours
            classUnderTest.performApplicationLogic()
            results << classUnderTest.getResult()
        }
    

    As an aside, use a ScheduledExecutorService instead of creating a busy loop with while(true).