Démarrer Spring StateMachine

Entrée


Dans les projets, j'ai rencontré trois exemples, d'une manière ou d'une autre liés à la théorie des automates finis

  • Exemple 1. Un code govnokod divertissant . Il faut beaucoup de temps pour comprendre ce qui se passe. Une caractéristique du mode de réalisation de la théorie indiquée dans le code est un cliché assez féroce qui ressemble parfois à un code procédural. Le fait que cette version du code soit préférable de ne pas toucher au projet connaît tous les technologues, méthodologistes et spécialistes des produits. Ils vont dans ce code pour réparer quelque chose en cas d'urgence (quand il est complètement cassé), il n'est pas question de finaliser les fonctionnalités. Car ça fait peur de casser. La deuxième caractéristique frappante qui isole ce type est la présence de tels commutateurs puissants, en plein écran.
    Il y a même une blague sur ce score:
    Taille optimale
    Sur l'un des JPoint, l'un des haut-parleurs, peut-être Nikolai Alimenkov, a parlé du nombre de cas dans le commutateur sont normaux, a déclaré que la première réponse est "jusqu'à présent, s'inscrit dans l'écran." Par conséquent, s'il interfère et que votre commutateur n'est pas déjà normal, prenez et réduisez la taille de la police dans l'IDE
  • Exemple 2. État du modèle . L'idée principale (pour ceux qui n'aiment pas suivre les liens) est de décomposer une certaine tâche commerciale en un ensemble d'états finaux et de les décrire avec du code.
    Le principal inconvénient de Pattern State est que les États se connaissent, savent qu'il y a des frères et s'appellent. Un tel code est assez difficile à rendre universel. Par exemple, lorsque vous implémentez un système de paiement avec plusieurs types de paiements, vous risquez de creuser dans Generic-s à tel point que la déclaration de vos méthodes peut devenir quelque chose comme ceci:

    private < T extends BaseContextPayment, Q extends BaseDomainPaymentRequest, S, B extends AbstractPaymentDetailBuilder<T, Q, S, B>, F extends AbstractPaymentBuilder<T, Q, S, B> > PaymentContext<T, S> build(final Q request, final Class<F> factoryClass){ //""  } 

    État de synthèse: une implémentation peut entraîner un code assez compliqué.
  • Exemple 3 StateMachine L'idée principale du Pattern est que les états ne se connaissent pas, le contrôle de transition est effectué par le contexte, c'est mieux, moins de connectivité - le code est plus simple.

Après avoir expérimenté toute la «puissance» du premier type et la complexité du second, nous avons décidé d'utiliser Pattern StateMachine pour la nouvelle analyse de rentabilisation.
Afin de ne pas réinventer la roue, il a été décidé de prendre Statemachine Spring comme base (c'est Spring).

Après avoir lu les quais, je suis allé sur YouTube et Habr (pour comprendre comment les gens travaillent avec lui, comment ça se sent sur la prod, quel type de râteau, etc.) Il s'est avéré qu'il y a peu d'informations, sur YouTube il y a quelques vidéos, toutes sont assez superficielles. Sur Habré à ce sujet je n'ai trouvé qu'un seul article, ainsi que la vidéo, assez superficielle.
Dans un article, il est impossible de décrire toutes les subtilités du travail de Spring statemachine, de faire le tour du quai et de décrire tous les cas, mais j'essaierai de dire les plus importants et les plus demandés, et sur le râteau, spécialement pour moi, lorsque je me suis familiarisé avec le cadre, les informations ci-dessous étaient serait très utile.

Corps principal


Nous allons créer une application Spring Boot, ajouter un démarreur Web (nous ferons fonctionner l'application Web le plus rapidement possible). L'application sera une abstraction du processus d'achat. Le produit à l'achat passera par les étapes de nouveau, réservé, de refus réservé et d'achat terminé.
Un peu d'improvisation, il y aurait plus de statuts dans un vrai projet, mais bon, on a aussi un vrai projet.
Dans le pom.xml de l'application Web nouvellement créée, ajoutez la dépendance à la machine et aux tests pour celle-ci (Web Starter devrait déjà l'être, s'il est collecté via start.spring.io ):
 <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-core</artifactId> <version>2.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-test</artifactId> <version>2.1.3.RELEASE</version> <scope>test</scope> </dependency> <cut /> 

Créez la structure:

Je n'ai pas encore à entrer dans les détails de cette structure, je vais tout expliquer séquentiellement, et il y aura un lien vers la source à la fin de l'article.

Alors allons-y.
Nous avons un projet propre avec les dépendances nécessaires, pour commencer nous allons créer une énumération, avec des états et des événements, une abstraction assez simple, ces composants eux-mêmes ne portent aucune logique.
 public enum PurchaseEvent { RESERVE, BUY, RESERVE_DECLINE } 

 public enum PurchaseState { NEW, RESERVED, CANCEL_RESERVED, PURCHASE_COMPLETE } 

Bien que formellement, vous pouvez ajouter des champs à ces énumérations et y coder en dur quelque chose qui est caractéristique, par exemple, d'un état particulier, ce qui est assez logique (nous l'avons fait en résolvant notre cas, très facilement).
Nous allons configurer la machine via la configuration java, créer le fichier de configuration et, pour la classe étend EnumStateMachineConfigurerAdapter <PurchaseState, PurchaseEvent>. Étant donné que notre état et événement est enum, l'interface est appropriée, mais elle n'est pas nécessaire, tout type d'objet peut être utilisé comme générique (nous ne considérerons pas d'autres exemples dans l'article, car EnumStateMachineConfigurerAdapter est plus que suffisant à mon avis).

Le point important suivant est de savoir si une machine vivra dans le contexte de l'application: dans une seule instance de @EnableStateMachine, ou chaque fois qu'une nouvelle @EnableStateMachineFactory sera créée. S'il s'agit d'une application Web multi-utilisateurs avec un groupe d'utilisateurs, la première option ne vous convient pas, nous utiliserons donc la seconde comme la plus populaire. StateMachine peut également être créé via le générateur en tant que bean standard (voir la documentation), ce qui est pratique dans certains cas (par exemple, vous avez besoin que la machine soit explicitement déclarée en tant que bean), et s'il s'agit d'un bean distinct, alors nous pouvons lui indiquer notre portée par exemple session ou demande. Dans notre projet, le wrapper (caractéristiques de notre logique métier) a été implémenté sur le bean statemachine, le wrapper était singleton et la machine prototype elle-même
Râteau
Comment implémenter un prototype en singlton?
En fait, tout ce que vous devez faire est d'obtenir un nouveau bean à partir de l'applicationContext chaque fois que vous accédez à l'objet. Injecter un ApplicationContext dans la logique métier est un péché, par conséquent, un statemachine de bean doit implémenter une interface avec au moins une méthode ou une méthode abstraite (injection de méthode), lors de la création d'une configuration java, vous devrez implémenter la méthode abstraite indiquée, et dans l'implémentation, nous tirerons de applicationContext nouveau bean. Il est normal d'avoir un lien vers l'applicationContext dans la classe config, et à travers la méthode abstraite, nous appellerons .getBean ();

La classe EnumStateMachineConfigurerAdapter a plusieurs méthodes, remplaçant celle que nous configurons la machine.
Pour commencer, enregistrez les statuts:
  @Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .states(EnumSet.allOf(PurchaseState.class)); } 

initial (NEW) est l'état dans lequel la machine sera après la création du bean, end (PURCHASE_COMPLETE) est l'état en allant vers lequel la machine exécute statemachine.stop (), pour une machine non déterministe (dont la plupart) n'est pas pertinente, mais quelque chose doit être spécifié . .states (EnumSet.allOf (PurchaseState.class) liste de tous les statuts, vous pouvez pousser en masse.

Configurer les paramètres globaux de la machine
  @Override public void configure(final StateMachineConfigurationConfigurer<PurchaseState, PurchaseEvent> config) throws Exception { config .withConfiguration() .autoStartup(true) .listener(new PurchaseStateMachineApplicationListener()); } 

Ici, le démarrage automatique détermine si la machine sera démarrée immédiatement après sa création par défaut, en d'autres termes - si elle passera automatiquement au statut NOUVEAU (faux par défaut). Immédiatement, nous enregistrons un écouteur pour le contexte de la machine (à ce sujet un peu plus tard), dans la même configuration, vous pouvez définir un TaskExecutor distinct, ce qui est pratique lorsqu'une longue action est effectuée sur certaines de leurs transitions, et l'application devrait aller plus loin.
Eh bien, les transitions elles-mêmes:
  @Override public void configure(final StateMachineTransitionConfigurer<PurchaseState, PurchaseEvent> transitions) throws Exception { transitions .withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(CANCEL_RESERVED) .event(RESERVE_DECLINE) .action(cancelAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .action(buyAction(), errorAction()); } 

Toute la logique des transitions ou des transitions est définie ici, Guard peut être suspendu aux transitions, un composant qui retourne toujours booléen, que vérifierez-vous exactement sur la transition d'un état à un autre à votre discrétion, toute logique peut être parfaite dans Guard, c'est un composant tout à fait ordinaire mais il doit revenir booléen. Dans le cadre de notre projet, par exemple, HideGuard peut vérifier un certain paramètre que l'utilisateur pourrait définir (ne pas montrer ce produit) et, conformément à cela, ne pas laisser la machine dans l'état protégé par Guard. Je note que Guard, une seule peut être ajoutée à une transition dans la configuration, une telle conception ne fonctionnera pas:
  .withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .guard(veryHideGuard()) 

Plus précisément, cela fonctionnera, mais seulement le premier garde (hideGuard ())
Mais vous pouvez ajouter plusieurs actions (maintenant nous parlons d'action, que nous prescrivons dans la configuration des transitions), j'ai personnellement essayé d'ajouter trois actions à une transition.
  .withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction()) 

le deuxième argument est ErrorAction, le contrôle y arrivera si ReservedAction lève une exception (throw ).
Râteau
Gardez à l'esprit que si dans votre action vous gérez toujours l'erreur via try / catch, vous n'entrerez pas dans ErrorAction, si vous devez traiter et aller dans ErrorAction, vous devez alors lancer RuntimeException () à partir de catch, par exemple (vous avez dit vous-même que c'était très nécessaire).

En plus de "suspendre" l'action dans les transitions, vous pouvez également les "suspendre" dans la méthode configure pour l'état, sous la forme approximative suivante:
  @Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .stateEntry() .stateExit() .state() .states(EnumSet.allOf(PurchaseState.class)); } 

Tout dépend de la façon dont vous souhaitez exécuter l'action.
Râteau
Notez que si vous spécifiez une action lors de la configuration de state (), comme par exemple
  states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .state(randomAction()) 

il sera exécuté de manière asynchrone, on suppose que si vous dites .stateEntry () par exemple, alors l'action doit être exécutée directement à l'entrée, mais si vous dites .state () alors l'action doit être exécutée dans l'état cible, mais ce n'est pas si important quand.
Dans notre projet, nous avons configuré toutes les actions sur la configuration de transition, car vous pouvez les suspendre plusieurs à la fois.

La version finale de la configuration ressemblera à ceci:
 @Configuration @EnableStateMachineFactory public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<PurchaseState, PurchaseEvent> { @Override public void configure(final StateMachineConfigurationConfigurer<PurchaseState, PurchaseEvent> config) throws Exception { config .withConfiguration() .autoStartup(true) .listener(new PurchaseStateMachineApplicationListener()); } @Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .stateEntry() .stateExit() .state() .states(EnumSet.allOf(PurchaseState.class)); } @Override public void configure(final StateMachineTransitionConfigurer<PurchaseState, PurchaseEvent> transitions) throws Exception { transitions .withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(CANCEL_RESERVED) .event(RESERVE_DECLINE) .action(cancelAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .action(buyAction(), errorAction()); } @Bean public Action<PurchaseState, PurchaseEvent> reservedAction() { return new ReservedAction(); } @Bean public Action<PurchaseState, PurchaseEvent> cancelAction() { return new CancelAction(); } @Bean public Action<PurchaseState, PurchaseEvent> buyAction() { return new BuyAction(); } @Bean public Action<PurchaseState, PurchaseEvent> errorAction() { return new ErrorAction(); } @Bean public Guard<PurchaseState, PurchaseEvent> hideGuard() { return new HideGuard(); } @Bean public StateMachinePersister<PurchaseState, PurchaseEvent, String> persister() { return new DefaultStateMachinePersister<>(new PurchaseStateMachinePersister()); } 

Faites attention au schéma de la machine, il est très clairement visible sur ce que nous avons encodé exactement (quelles transitions sur quels événements sont valides, quel Guard protège l'état et ce qui sera effectué lorsque l'état est changé, quelle action).

Faisons le contrôleur:
 @RestController @SuppressWarnings("unused") public class PurchaseController { private final PurchaseService purchaseService; public PurchaseController(PurchaseService purchaseService) { this.purchaseService = purchaseService; } @RequestMapping(path = "/reserve") public boolean reserve(final String userId, final String productId) { return purchaseService.reserved(userId, productId); } @RequestMapping(path = "/cancel") public boolean cancelReserve(final String userId) { return purchaseService.cancelReserve(userId); } @RequestMapping(path = "/buy") public boolean buyReserve(final String userId) { return purchaseService.buy(userId); } } 


interface de service
 public interface PurchaseService { /** *    ,          * * @param userId id ,    ,      id  *    http- * @param productId id ,     * @return /  ,             *      . */ boolean reserved(String userId, String productId); /** *   /    * * @param userId id ,    ,      id  *    http- * @return /  ,             *      . */ boolean cancelReserve(String userId); /** *     * * @param userId id ,    ,      id  *    http- * @return /  ,             *      . */ boolean buy(String userId); } 

Râteau
Savez-vous pourquoi il est important de créer un bean via l'interface lorsque vous travaillez avec Spring? Face à ce problème (enfin, oui, oui, et Zhenya Borisov l'a dit dans le ripper), lorsqu'une fois dans le contrôleur, ils ont essayé de mettre en œuvre une interface improvisée non vide. Spring crée un proxy pour les composants, et si le composant n'implémente aucune interface, il le fera via CGLIB, mais dès que vous implémenterez une interface - Spring essaiera de créer un proxy via un proxy dynamique, par conséquent vous obtiendrez un type d'objet incompréhensible et NoSuchBeanDefinitionException .

Le point important suivant est de savoir comment vous allez restaurer l'état de votre machine, car pour chaque appel un nouveau bean sera créé qui ne sait rien de vos états précédents de la machine et de son contexte.
À ces fins, la statemachine à ressort a un mécanisme Persistens:
 public class PurchaseStateMachinePersister implements StateMachinePersist<PurchaseState, PurchaseEvent, String> { private final HashMap<String, StateMachineContext<PurchaseState, PurchaseEvent>> contexts = new HashMap<>(); @Override public void write(final StateMachineContext<PurchaseState, PurchaseEvent> context, String contextObj) { contexts.put(contextObj, context); } @Override public StateMachineContext<PurchaseState, PurchaseEvent> read(final String contextObj) { return contexts.get(contextObj); } } 

Pour notre implémentation naïve, nous utilisons la carte habituelle comme magasin d'état, dans une implémentation non naïve, ce sera une sorte de base de données, faites attention au troisième type générique Chaîne, c'est la clé par laquelle l'état de votre machine sera enregistré, avec tous les statuts, variables dans le contexte, id et ainsi de suite. Dans mon exemple, j'ai utilisé l'ID utilisateur pour la clé de sauvegarde, qui peut être absolument n'importe quelle clé (utilisateur session_id, connexion unique, etc.).
Râteau
Dans notre projet, le mécanisme de sauvegarde et de restauration des états de la boîte ne nous convenait pas, car nous stockions les statuts de la machine dans la base de données et pouvaient être modifiés par un travail ne connaissant rien de la machine.
J'ai dû attacher le statut reçu de la base de données, faire une InitAction qui, lorsque la machine démarre, a reçu le statut de la base de données, et le définir de force, et seulement ensuite a lancé l'événement, un exemple de code qui fait ce qui précède:
 stateMachine .getStateMachineAccessor() .doWithAllRegions(access -> { access.resetStateMachine(new DefaultStateMachineContext<>({ResetState}, null, null, null, null)); }); stateMachine.start(); stateMachine.sendEvent({NewEventFromResetState}); 


Nous considérerons l'implémentation du service dans chaque méthode:
  @Override public boolean reserved(final String userId, final String productId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); stateMachine.getExtendedState().getVariables().put("PRODUCT_ID", productId); stateMachine.sendEvent(RESERVE); try { persister.persist(stateMachine, userId); } catch (final Exception e) { e.printStackTrace(); return false; } return true; } 

Nous récupérons la voiture de l'usine, mettons un paramètre dans le contexte de la machine, dans notre cas, c'est un productId, le contexte est une sorte de boîte dans laquelle vous pouvez mettre tout ce dont vous avez besoin, partout où il y a accès au bean statemachine ou à son contexte, car la machine démarre automatiquement au démarrage du contexte , puis après le départ, notre voiture sera dans le nouveau statut, jetez l'événement sur la réservation des marchandises.

Les deux méthodes restantes sont similaires:
  @Override public boolean cancelReserve(final String userId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); try { persister.restore(stateMachine, userId); stateMachine.sendEvent(RESERVE_DECLINE); } catch (Exception e) { e.printStackTrace(); return false; } return true; } @Override public boolean buy(final String userId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); try { persister.restore(stateMachine, userId); stateMachine.sendEvent(BUY); } catch (Exception e) { e.printStackTrace(); return false; } return true; } 

Ici, nous restaurons d'abord l'état de la machine pour l'ID utilisateur d'un utilisateur particulier, puis lançons un événement qui correspond à la méthode api.
Notez que productId n'apparaît plus dans la méthode, nous l'avons ajouté au contexte de la machine et l'obtiendrons après la restauration de la machine à partir de sa sauvegarde.
Dans l'implémentation de l'action, nous obtiendrons l'ID du produit dans le contexte de la machine et afficherons un message correspondant à la transition dans le journal, par exemple, je donnerai le code ReservedAction:
 public class ReservedAction implements Action<PurchaseState, PurchaseEvent> { @Override public void execute(StateContext<PurchaseState, PurchaseEvent> context) { final String productId = context.getExtendedState().get("PRODUCT_ID", String.class); System.out.println("   " + productId + " ."); } } 

Nous ne pouvons que mentionner l'auditeur, qui, dès la sortie de la boîte, propose de nombreux scripts auxquels vous pouvez vous accrocher, voyez par vous-même:
 public class PurchaseStateMachineApplicationListener implements StateMachineListener<PurchaseState, PurchaseEvent> { @Override public void stateChanged(State<PurchaseState, PurchaseEvent> from, State<PurchaseState, PurchaseEvent> to) { if (from.getId() != null) { System.out.println("   " + from.getId() + "   " + to.getId()); } } @Override public void stateEntered(State<PurchaseState, PurchaseEvent> state) { } @Override public void stateExited(State<PurchaseState, PurchaseEvent> state) { } @Override public void eventNotAccepted(Message<PurchaseEvent> event) { System.out.println("   " + event); } @Override public void transition(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void transitionStarted(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void transitionEnded(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void stateMachineStarted(StateMachine<PurchaseState, PurchaseEvent> stateMachine) { System.out.println("Machine started"); } @Override public void stateMachineStopped(StateMachine<PurchaseState, PurchaseEvent> stateMachine) { } @Override public void stateMachineError(StateMachine<PurchaseState, PurchaseEvent> stateMachine, Exception exception) { } @Override public void extendedStateChanged(Object key, Object value) { } @Override public void stateContext(StateContext<PurchaseState, PurchaseEvent> stateContext) { } } 

Le seul problème est qu'il s'agit d'une interface, ce qui signifie que vous devez implémenter toutes ces méthodes, mais comme il est peu probable que vous en ayez toutes besoin, certaines d'entre elles resteront vides, ce qui signifie que les méthodes ne sont pas couvertes par les tests.
Ici, en écouteur, nous pouvons accrocher absolument toutes les métriques sur des événements complètement différents de la machine (par exemple, les paiements ne passent pas, la machine passe souvent dans une sorte d'état PAYMENT_FAIL, nous écoutons les transitions et si la machine est entrée dans un état erroné - nous écrivons, dans un journal impair, ou base ou appeler la police, peu importe).
Râteau
Il y a un événement stateMachineError dans lisener-e, mais avec une nuance, lorsque vous avez une exception et que vous la gérez dans catch, la machine ne considère pas qu'il y a eu une erreur, vous devez parler explicitement dans catch
stateMachine.setStateMachineError (exception) et passez une erreur.

Pour vérifier ce que nous avons fait, nous exécuterons deux cas:
  • 1. Réservation et refus ultérieur de l'achat. Nous enverrons à l'application une demande pour l'URI "/ reserve", avec les paramètres userId = 007, productId = 10001, puis la demande "/ cancel" avec le paramètre userId = 007, la sortie de la console sera la suivante:
    Machine started
    10001 .
    NEW RESERVED
    Machine started
    10001
    RESERVED CANCEL_RESERVED
  • 2. Réservation et achat réussi:
    Machine started
    10001 .
    NEW RESERVED
    Machine started
    10001
    RESERVED PURCHASE_COMPLETE

Conclusion


En conclusion, je vais donner un exemple de test du framework, je pense que tout deviendra clair à partir du code, vous avez juste besoin d'une dépendance sur la machine de test, et vous pouvez vérifier la configuration de manière déclarative.
  @Test public void testWhenReservedCancel() throws Exception { StateMachine<PurchaseState, PurchaseEvent> machine = factory.getStateMachine(); StateMachineTestPlan<PurchaseState, PurchaseEvent> plan = StateMachineTestPlanBuilder.<PurchaseState, PurchaseEvent>builder() .defaultAwaitTime(2) .stateMachine(machine) .step() .expectStates(NEW) .expectStateChanged(0) .and() .step() .sendEvent(RESERVE) .expectState(RESERVED) .expectStateChanged(1) .and() .step() .sendEvent(RESERVE_DECLINE) .expectState(CANCEL_RESERVED) .expectStateChanged(1) .and() .build(); plan.test(); } @Test public void testWhenPurchaseComplete() throws Exception { StateMachine<PurchaseState, PurchaseEvent> machine = factory.getStateMachine(); StateMachineTestPlan<PurchaseState, PurchaseEvent> plan = StateMachineTestPlanBuilder.<PurchaseState, PurchaseEvent>builder() .defaultAwaitTime(2) .stateMachine(machine) .step() .expectStates(NEW) .expectStateChanged(0) .and() .step() .sendEvent(RESERVE) .expectState(RESERVED) .expectStateChanged(1) .and() .step() .sendEvent(BUY) .expectState(PURCHASE_COMPLETE) .expectStateChanged(1) .and() .build(); plan.test(); } 

Râteau
Si vous voulez soudainement tester votre machine sans augmenter le contexte avec les tests unitaires habituels, vous pouvez créer une machine via le constructeur (partiellement discuté ci-dessus), créer une instance de la classe avec une configuration et obtenir une action et une protection à partir de là, cela fonctionnera sans contexte, vous pouvez écrire un petit test Le cadre est sur maquette, ce sera un plus pour vérifier quelles actions ont été appelées, lesquelles ne le sont pas, combien de fois, etc., dans différents cas.

PS


, , , , (Guard- Action- )


, choice, , switch, Guards, , choice Guard, Events, , , .

Les références



Source: https://habr.com/ru/post/fr462371/


All Articles