Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a StateMachine from a StateMachineConfigurer

I have an annotation based state machine configuration:

@Component @Scope(BeanDefinition.SCOPE_PROTOTYPE)
@EnableStateMachine(name = "machine1")
public class Machine1 extends
   EnumStateMachineConfigurerAdapter<SimStates, SimEvents> {

   @Override
   public void configure(StateMachineStateConfigurer<SimStates, SimEvents> states) throws Exception {
      states.withStates()
        .initial(INIT)
        .state(INIT)
        .state(S1)
        .state(FINISH)
        .end(FINISH)
      ;
    }
  ...

Now I want to create Tests for it. I would prefer not have an implicit call to getBean("machine1")" via StateMachineFactory.getStateMachine("machine1"), which would require an application context.

I'd rather instantiate Machine1 and feed it to some Builder, Configurator or Adapter to get a StateMachine<> instance.

public class Machine1Test {

  @Test
  public void testMachine1() throws Exception {

    final StateMachineConfigurer<SimStates, SimEvents> smc = 
      new Machine1();


    final StateMachineBuilder.Builder<SimStates, SimEvents> builder = 
        StateMachineBuilder.builder();

    // can I use the builder together with smc? Or something else?

    StateMachine<SimStates,SimEvents> sm = ... // how?
  }
}

Update: I updated "without full application context" to "without an implicit call to getBean("machine1")". The question is also about understanding about all the factories, adapters, configurations and configurators of spring state machine.

like image 857
towi Avatar asked Feb 22 '19 13:02

towi


1 Answers

I'd rather instantiate Machine1 and feed it to some Builder, Configurator or Adapter to get a StateMachine<> instance.

Spring State Mahcine supports annotation based configuration for instantiation (e.g. via Adapter) or a Builder - there's no other options.

SM via Adapter

Using @SpringBootTest(clasess = <YourEnumSMConfig> definitely does not create a full application context:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Machine1.class})
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class StateMachineTests {

    @Autowired
    private StateMachine<String, String> machine1;

    @Test
    public void testInitialState() throws Exception {
        StatMachineTestPlan<SimState, SimEvent> plan = StateMachineTestPlanBuilder.<SimState, SimEvent>builder()
          .defaultAwaitTime(2)
          .stateMachine(machine1)
          .step()
            .expectStateChange(1)
            .expectStateEntered(SimState.INITIAL)
            .expectState(SimState.INITIAL)
          .and()
          .build()

      plan.test();
    }

}

Now I want to create Tests for it.

Testing with TestPlanBuilder:

There's a Testing support out of the box to test a spring state machine. It's called StateMachineTestPlan. You can build StateMachineTestPlan using StateMachineTestPlanBuilder.

Access to these classes you can get from declaring the following dependency:

<dependency>
    <groupId>org.springframework.statemachine</groupId>
    <artifactId>spring-statemachine-test</artifactId>
    <version>2.0.3.RELEASE</version>  // change version to match yours
    <scope>test</scope>
</dependency>

The detailed official documentation regarding testing is available here.

SM via Builder

I would prefer not have an implicit call to getBean("machine1")" via StateMachineFactory.getStateMachine("machine1"), which would require an application context.

Creating a SM via Builder does not require any Spring context.

public class TestEventNotAccepted {

    @Test
    public void testEventNotAccepted() throws Exception {
        StateMachine<String, String> machine = buildMachine();
        StateMachineTestPlan<String, String> plan =
                StateMachineTestPlanBuilder.<String, String>builder()
                        .defaultAwaitTime(2)
                        .stateMachine(machine)
                        .step()
                        .expectStates("SI")
                        .and()
                        .step()
                        .sendEvent("E2")
                        .and()
                        .build();
        plan.test();
    }

    private StateMachine<String, String> buildMachine() throws Exception {
        StateMachineBuilder.Builder<String, String> builder = StateMachineBuilder.builder();

        builder.configureConfiguration()
                .withConfiguration()
                .taskExecutor(new SyncTaskExecutor())
                .listener(customListener())
                .autoStartup(true);

        builder.configureStates()
                .withStates()
                .initial("SI")
                .state("S1")
                .state("S2");

        builder.configureTransitions()
                .withExternal()
                .source("SI").target("S1")
                .event("E1")
                .action(c -> c.getExtendedState().getVariables().put("key1", "value1"))
                .and()
                .withExternal()
                .source("S1").target("S2").event("E2");

        return builder.build();
    }

    private StateMachineListener<String, String> customListener() {
        return new StateMachineListenerAdapter<String, String>() {
            @Override
            public void eventNotAccepted(Message event) {
                System.out.println("EVENT NOT ACCEPTED: " + event);
            }
        };
    }
like image 114
hovanessyan Avatar answered Oct 02 '22 11:10

hovanessyan