towi
towi

Reputation: 22267

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.

Upvotes: 6

Views: 3539

Answers (2)

hovanessyan
hovanessyan

Reputation: 31443

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);
            }
        };
    }

Upvotes: 3

Vi Țurcanu
Vi Țurcanu

Reputation: 45

I didn't find an explicit way to use the EnumStateMachineConfigurerAdapter with StateMachineBuilder.Builder<>, but I have used this approach:

@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 {
        configureStates(states);
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<SimStates, SimEvents> transitions) throws Exception {
        configureTransitions(transitions);
    }

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

    public static void configureTransitions(StateMachineTransitionConfigurer<SimStates, SimEvents> states) throws Exception {
        states.withTransitions()
                // configure transitions
        ;
    }
}

and importing the static configuration methods in the Statemachine test:

import static com.example.statemachine.Machine1.configureStates;
import static com.example.statemachine.Machine1.configureTransitions;

public class TestEventNotAccepted {

    @Test
    public void testEventNotAccepted() throws Exception {
        StateMachine<SimStates, SimEvents> machine = buildMachine();
        StateMachineTestPlan<SimStates, SimEvents> plan =
                StateMachineTestPlanBuilder.<SimStates, SimEvents>builder()
                        .defaultAwaitTime(2)
                        .stateMachine(machine)

                        .step()
                        .expectStates(INIT)
                        .and()

                        // configure other test steps

                        .build();
        plan.test();
    }

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

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

        configureStates(builder.configureStates());

        configureTransitions(builder.configureTransitions());

        return builder.build();
    }
}

As a result, I was able to unit test my exact configuration without building the whole Spring context and without using @SpringBootTest.

Upvotes: -1

Related Questions