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).
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)
.