How to create a StateMachine from a StateMachineConfigurer
Asked Answered
E

2

6

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.

Ensample answered 22/2, 2019 at 13:45 Comment(3)
I've updated my answer: I do have such tests and they do not create full application context, but please give more details in your question on what is it that you want to achieve.Hyphenate
Also instead of using the SMConfigurer for your tests (if you don't want to) you can use directly a StateMachineBuilder that returns a SM with the same config as described in the SMConfigurer, but just for your tests. This is demonstrated in the original answer - there you have 0 application context.Hyphenate
Can you please elaborate on the expected answer for this question? SM supports annotation based configuration for instantiation or a builder - there's no other options. I've demoed both in my answer and none of the approaches brings up the full spring application context. What exactly do you want to know about the other components? I guess you're not simply looking for someone to copy-paste you the official reference doc on factories, adapters, configurations and configurators or source code references from git...Hyphenate
H
3

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);
            }
        };
    }
Hyphenate answered 23/2, 2019 at 10:52 Comment(4)
Thanks, I am aware of StateMachineTestPlan. I also know how to create dynamic state machine with StateMachineBuilder. But my question is how I get from a EnumStateMachineConfigurerAdapter instance to a StateMachine instance without an application context (and hence without the default StateMachineFactory).Ensample
I see - but why would you require the full application context to test an instance of the EnumSM? You can use SpringBootTest(classes = EnumSM.class) and mock everything else (like SM guards/ SM actions/ services etc)Hyphenate
Right, good point. I have the feeling that SpringBootTest does create a full application context. That's probably a wrong assumption. What I basically mean is "without an implicit call to getBean("machine1"). That would then translate to "without an(y) application context"? Implicitly the question is also about understanding about all the factories, adapters, configurations and configurators of spring state machine. I just "feel" there must be a way to get from A to C, and I can just not find the B...Ensample
I'm trying a similar approach but am getting a "No qualifying bean of type 'org.springframework.statemachine.data.jpa.JpaStateMachineRepository" after adding my enum configurer to the classes={}. @RunWith(SpringRunner.class) @SpringBootTest(classes = { EnumConfig.class, MonitorConfig.class, PersistenceConfig.class}) class Test{Caldera
C
-1

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.

Christoforo answered 4/3, 2019 at 0:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.