Я настроил Spring State Machine со следующими состояниями и переходами:
IDLE (начальный) -> STARTED -> PRE_SNAPSHOT (действие предварительного снимка) -> SNAPSHOT (действие снимка) -> IDLE
Вот моя конфигурация:
@TestConfiguration
@EnableStateMachineFactory
public class StateMachineConfiguration extends StateMachineConfigurerAdapter<String, String> {
public static final String IDLE = "IDLE";
public static final String STARTED = "STARTED";
public static final String PRE_SNAPSHOT = "PRE_SNAPSHOT";
public static final String SNAPSHOT = "SNAPSHOT";
public static final String START_EVENT = "START_EVENT";
@Autowired
private PreSnapshotActionMock preSnapshotActionMock;
@Autowired
private SnapshotActionMock snapshotActionMock;
@Override
public void configure(StateMachineConfigurationConfigurer<String, String> config) throws Exception {
config.withConfiguration().autoStartup(true);
}
@Override
public void configure(StateMachineStateConfigurer<String, String> stateConfigurer) throws Exception {
stateConfigurer
.withStates()
.initial(IDLE)
.state(STARTED)
.stateDo(PRE_SNAPSHOT, preSnapshotActionMock)
.stateDo(SNAPSHOT, snapshotActionMock)
.end(IDLE);
}
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitionConfigurer) throws Exception {
transitionConfigurer
.withExternal().source(IDLE).target(STARTED).event(START_EVENT)
.and()
.withExternal().source(STARTED).target(PRE_SNAPSHOT)
.and()
.withExternal().source(PRE_SNAPSHOT).target(SNAPSHOT)
.and()
.withExternal().source(SNAPSHOT).target(IDLE);
}
@Bean
public StateMachine<String, String> testStateMachine(@Autowired StateMachineFactory<String, String> smFactory) throws Exception {
return smFactory.getStateMachine();
}
}
В моем случае я хочу издеваться над длительным выполнением действий, но в отдельных тестовых случаях для каждого. Для этого я использую Mockito AdditionalAnswers.answersWithDelay()
:
@SpringBootTest
@RunWith(SpringRunner.class)
@Import(StateMachineConfiguration.class)
@ActiveProfiles("local")
public class StateMachineTest {
@MockBean
private PreSnapshotActionMock preSnapshotActionMock;
@MockBean
private SnapshotActionMock snapshotActionMock;
@Autowired
private StateMachine<String, String> testStateMachine;
@Before
public void setUp() {
Answer preSnapshotAnswer = AdditionalAnswers.answersWithDelay(10, invocation -> {
System.out.println("default pre-snapshot action short execution");
return null;
});
doAnswer(preSnapshotAnswer).when(preSnapshotActionMock).execute(any());
Answer snapshotAnswer = AdditionalAnswers.answersWithDelay(10, invocation -> {
System.out.println("default snapshot action short execution");
return null;
});
doAnswer(snapshotAnswer).when(snapshotActionMock).execute(any());
testStateMachine.start();
}
@After
public void reset() throws InterruptedException {
testStateMachine.stop();
testStateMachine.getStateMachineAccessor().withRegion().resetStateMachine(
new DefaultStateMachineContext<>(IDLE, null, null, null));
Thread.sleep(250);
}
@Test
//here I mock long execution for pre-snapshot action only
public void test1() {
Answer answerWithDelay = AdditionalAnswers.answersWithDelay(10, invocation -> {
int i = 0;
while (i < 10) {
System.out.println("pre-snapshot action from test1 #" + i);
i++;
Thread.sleep(10_000);
}
return null;
});
doAnswer(answerWithDelay).when(preSnapshotActionMock).execute(any());
StateMachineListenerAdapter<String, String> stateChangeListenerSpy = Mockito.spy(StateMachineListenerAdapter.class);
testStateMachine.addStateListener(stateChangeListenerSpy);
Assertions.assertThat(testStateMachine.sendEvent(START_EVENT)).isTrue();
Mockito.verify(stateChangeListenerSpy, Mockito.timeout(10_000).times(2)).stateEntered(any());
Mockito.verify(stateChangeListenerSpy, Mockito.timeout(10_000).times(2)).stateExited(any());
testStateMachine.removeStateListener(stateChangeListenerSpy);
}
@Test
//here I mock long execution for snapshot action only
public void test2() {
Answer answerWithDelay = AdditionalAnswers.answersWithDelay(10, invocation -> {
int i = 0;
while (i < 10) {
System.out.println("snapshot action from test2 #" + i);
i++;
Thread.sleep(10_000);
}
return null;
});
doAnswer(answerWithDelay).when(snapshotActionMock).execute(any());
StateMachineListenerAdapter<String, String> stateChangeListenerSpy = Mockito.spy(StateMachineListenerAdapter.class);
testStateMachine.addStateListener(stateChangeListenerSpy);
System.out.println("Sending start event now");
Assertions.assertThat(testStateMachine.sendEvent(START_EVENT)).isTrue();
Mockito.verify(stateChangeListenerSpy, Mockito.timeout(10000).times(3)).stateEntered(any());
Mockito.verify(stateChangeListenerSpy, Mockito.timeout(10000).times(3)).stateExited(any());
testStateMachine.removeStateListener(stateChangeListenerSpy);
}
}
Первый тестовый пример проходит, потому что конечный автомат выполняет длительное выполнение действия предварительного снимка, что и ожидается. Второй тест, однако, не прошел, потому что снова выполняется предварительное создание снимка медленно. Что более интересно, если я изменю время ожидания в test1 с 10_000 на 100, вот журнал из оба теста:
test1:
pre-snapshot action from test1 #0
pre-snapshot action from test1 #1
pre-snapshot action from test1 #2
pre-snapshot action from test1 #3
test2:
Sending start event now
pre-snapshot action from test1 #4
pre-snapshot action from test1 #5
pre-snapshot action from test1 #6
pre-snapshot action from test1 #7
pre-snapshot action from test1 #8
pre-snapshot action from test1 #9
snapshot action from test2 #0
Кажется, что даже если я настроил поведение по умолчанию для действий в методе @Before , в test2 оно вообще не выполняется и вместо этого действие pre-snapshot продолжает свою работу с того места, где оно остановилось в test1 , только после этого конечный автомат переходит в состояние SNAPSHOT.
Если Я настраиваю в точности тот же конечный автомат и контрольные примеры, что и в модульных тестах, вот журнал из test2:
Sending start event now
default pre-snapshot action short execution
snapshot action from test2 #0
pre-snapshot action from test1 #4
pre-snapshot action from test1 #5
pre-snapshot action from test1 #6
, который показывает противоположное (и ожидаемое, на мой взгляд) поведение: сначала действие по умолчанию перед снимком выполняется.
Как вы думаете, это может быть ошибка в State Machine или я что-то здесь упускаю? Я согласен, что это может быть какой-то механизм восстановления, но если это так, то он не очень совместим с насмешками. Первоначально я думал, что причина может быть в Mockito's ResponersWithDelay, но, поскольку я тестировал его с другими объектами, похоже, это не так ...