<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.spiretrading.com/index.php?action=history&amp;feed=atom&amp;title=State_Machine_Unit_Testing</id>
	<title>State Machine Unit Testing - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.spiretrading.com/index.php?action=history&amp;feed=atom&amp;title=State_Machine_Unit_Testing"/>
	<link rel="alternate" type="text/html" href="https://wiki.spiretrading.com/index.php?title=State_Machine_Unit_Testing&amp;action=history"/>
	<updated>2026-07-28T17:52:33Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.34.1</generator>
	<entry>
		<id>https://wiki.spiretrading.com/index.php?title=State_Machine_Unit_Testing&amp;diff=217&amp;oldid=prev</id>
		<title>Kamal: Created page with &quot;Unit tests run a state machine against a simulated trading environment, exercising the paths of its specification. Tests feed the machine its...&quot;</title>
		<link rel="alternate" type="text/html" href="https://wiki.spiretrading.com/index.php?title=State_Machine_Unit_Testing&amp;diff=217&amp;oldid=prev"/>
		<updated>2026-07-13T19:00:55Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;Unit tests run a state machine against a simulated trading environment, exercising the paths of its &lt;a href=&quot;/index.php?title=State_Machine_Specifications&amp;amp;action=edit&amp;amp;redlink=1&quot; class=&quot;new&quot; title=&quot;State Machine Specifications (page does not exist)&quot;&gt;specification&lt;/a&gt;. Tests feed the machine its...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;Unit tests run a state machine against a simulated trading environment,&lt;br /&gt;
exercising the paths of its [[State Machine Specifications|specification]].&lt;br /&gt;
Tests feed the machine its events one at a time and assert the machine's&lt;br /&gt;
reactions: what it submits, what it cancels, and how it terminates, etc... This&lt;br /&gt;
article covers the test environment, how to drive it, and the synchronization&lt;br /&gt;
rules that keep tests deterministic. It continues the &amp;lt;code&amp;gt;PeggedOrder&amp;lt;/code&amp;gt;&lt;br /&gt;
example from [[State Machine Implementation]].&lt;br /&gt;
&lt;br /&gt;
A module's tests live beside it in &amp;lt;code&amp;gt;&amp;lt;module&amp;gt;_tester.py&amp;lt;/code&amp;gt;, written&lt;br /&gt;
with &amp;lt;code&amp;gt;unittest&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
import unittest&lt;br /&gt;
&lt;br /&gt;
import beam&lt;br /&gt;
import nexus&lt;br /&gt;
&lt;br /&gt;
import pegged_order&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Imports are grouped: standard library, then platform modules, then local&lt;br /&gt;
modules.&lt;br /&gt;
&lt;br /&gt;
== The test environment ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;nexus.TestEnvironment&amp;lt;/code&amp;gt; simulates the entire trading platform, market&lt;br /&gt;
data, order execution, and time. &amp;lt;code&amp;gt;nexus.TestClients&amp;lt;/code&amp;gt; gives&lt;br /&gt;
the machine under test the same &amp;lt;code&amp;gt;clients&amp;lt;/code&amp;gt; interface it uses in&lt;br /&gt;
production. The machine can't tell the difference as the test plays the role&lt;br /&gt;
of both the market and the venue.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
class TestPeggedOrder(unittest.TestCase):&lt;br /&gt;
  def setUp(self):&lt;br /&gt;
    self.environment = nexus.TestEnvironment()&lt;br /&gt;
    self.clients = nexus.TestClients(self.environment)&lt;br /&gt;
    self.ticker = nexus.parse_ticker('ABX.TSX')&lt;br /&gt;
    self.submissions = beam.Queue()&lt;br /&gt;
    self.environment.monitor_order_submissions(self.submissions)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;monitor_order_submissions&amp;lt;/code&amp;gt; publishes every order any machine&lt;br /&gt;
submits into a queue for review. It is backed by a snapshot publisher, so&lt;br /&gt;
monitoring before or after a submission both work.&lt;br /&gt;
&lt;br /&gt;
Machines that load reference data need it populated before they start. For&lt;br /&gt;
example, a TWAP order might need to know a ticker symbol's board lot, in that&lt;br /&gt;
case the &amp;lt;code&amp;gt;setUp&amp;lt;/code&amp;gt; method should populate the market data environment's&lt;br /&gt;
test data store with a &amp;lt;code&amp;gt;TickerInfo&amp;lt;/code&amp;gt; object that specifies the board&lt;br /&gt;
lot and any other information that is anticipated to be loaded:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    self.environment.get_market_data_environment().data_store.store(&lt;br /&gt;
      nexus.TickerInfo(self.ticker, 'Barrick Gold Corp', '', 100))&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Anatomy of a scenario ==&lt;br /&gt;
&lt;br /&gt;
Each test is one scenario: a comment block stating the steps imperatively, then&lt;br /&gt;
a body that performs exactly those steps. The comments are the scenario's&lt;br /&gt;
specification, a reviewer should be able to check the body against them line by&lt;br /&gt;
line.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  # Start a PeggedOrder on the bid for 1000 shares with no limit price and a&lt;br /&gt;
  # peg difference of 0.01.&lt;br /&gt;
  # Set the BBO to 1.00 / 1.01.&lt;br /&gt;
  # Expect a limit bid order submission for 0.99 for 1000 shares, published&lt;br /&gt;
  # on the orders publisher.&lt;br /&gt;
  # Accept and fill the order submission.&lt;br /&gt;
  # Expect the PeggedOrder to terminate and close its orders publisher.&lt;br /&gt;
  def test_fill(self):&lt;br /&gt;
    order_fields = nexus.make_market_order_fields(&lt;br /&gt;
      self.ticker, nexus.Side.BID, 1000)&lt;br /&gt;
    order = pegged_order.PeggedOrder(&lt;br /&gt;
      self.clients, order_fields, nexus.Money.CENT)&lt;br /&gt;
    orders = beam.Queue()&lt;br /&gt;
    order.orders.monitor(orders)&lt;br /&gt;
    self.environment.update_bbo_price(&lt;br /&gt;
      self.ticker, nexus.Money.parse('1.00'), nexus.Money.parse('1.01'))&lt;br /&gt;
    expected_order = self.submissions.pop()&lt;br /&gt;
    self.assertEqual(expected_order.info.fields.type, nexus.OrderType.LIMIT)&lt;br /&gt;
    self.assertEqual(&lt;br /&gt;
      expected_order.info.fields.price, nexus.Money.parse('0.99'))&lt;br /&gt;
    self.assertEqual(expected_order.info.fields.quantity, 1000)&lt;br /&gt;
    self.assertEqual(expected_order.info.fields.side, nexus.Side.BID)&lt;br /&gt;
    published_order = orders.pop()&lt;br /&gt;
    self.assertEqual(&lt;br /&gt;
      published_order.info.id, expected_order.info.id)&lt;br /&gt;
    self.environment.accept(expected_order)&lt;br /&gt;
    self.environment.fill(expected_order, 1000)&lt;br /&gt;
    order.wait()&lt;br /&gt;
    self.assertRaises(beam.PipeBrokenException, orders.pop)&lt;br /&gt;
    self.assertIsNone(self.submissions.try_pop())&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The structure is drive, observe, assert. Calls to the environment play the&lt;br /&gt;
market data, a blocking &amp;lt;code&amp;gt;pop&amp;lt;/code&amp;gt; waits for the machine's reaction,&lt;br /&gt;
and assertions check the reaction's fields. Rinse and repeat.&lt;br /&gt;
&lt;br /&gt;
== Driving the machine ==&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;update_bbo_price(ticker, bid, ask)&amp;lt;/code&amp;gt; publishes a quote. The machine's subscription uses a current query, so a quote set before the machine subscribed is still delivered from the snapshot.&lt;br /&gt;
* &amp;lt;code&amp;gt;accept(order)&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;reject(order)&amp;lt;/code&amp;gt; resolve a pending submission.&lt;br /&gt;
* &amp;lt;code&amp;gt;fill(order, quantity)&amp;lt;/code&amp;gt; executes shares, a quantity below the order's fills partially and leaves it live, so multi-fill scenarios are just repeated calls.&lt;br /&gt;
* &amp;lt;code&amp;gt;cancel(order)&amp;lt;/code&amp;gt; confirms a cancel the machine requested — drive it only once the request is observed (below).&lt;br /&gt;
* &amp;lt;code&amp;gt;advance_time(duration)&amp;lt;/code&amp;gt; moves the simulated clock and triggers any timers whose expiry falls within it, for schedule-driven machines.&lt;br /&gt;
&lt;br /&gt;
== Synchronization ==&lt;br /&gt;
&lt;br /&gt;
Everything the machine does happens asynchronously in its own routine, so&lt;br /&gt;
tests synchronize by '''blocking on observations''', never by sleeping.&lt;br /&gt;
&lt;br /&gt;
'''A blocking &amp;lt;code&amp;gt;pop&amp;lt;/code&amp;gt; is the primary mechanism.''' It waits until&lt;br /&gt;
the machine acts, and a popped value proves everything causally before it&lt;br /&gt;
already happened. If the machine never acts, the pop blocks forever — which&lt;br /&gt;
is why suites run under a timeout as the backstop&lt;br /&gt;
(&amp;lt;code&amp;gt;timeout 60 python3 pegged_order_tester.py&amp;lt;/code&amp;gt;): a hang is a failure.&lt;br /&gt;
&lt;br /&gt;
'''Await order state through its reports.''' Asserting a status&lt;br /&gt;
point-in-time races the machine's handler. To wait for a cancel request,&lt;br /&gt;
monitor the order's reports and pop until the status arrives:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    reports = beam.Queue()&lt;br /&gt;
    expected_order.publisher.monitor(reports)&lt;br /&gt;
    order.cancel()&lt;br /&gt;
    while reports.pop().status != nexus.OrderStatus.PENDING_CANCEL:&lt;br /&gt;
      pass&lt;br /&gt;
    self.environment.cancel(expected_order)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
'''The one legitimate flush.''' A timer-driven machine registers its first&lt;br /&gt;
timer asynchronously; &amp;lt;code&amp;gt;advance_time&amp;lt;/code&amp;gt; immediately after&lt;br /&gt;
construction can win that race, and a test timer counts from its&lt;br /&gt;
&amp;lt;code&amp;gt;start()&amp;lt;/code&amp;gt; so the missed window is unrecoverable. Settle the&lt;br /&gt;
routines once, after construction, before the first advance:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    order = market_twap_order.MarketTwapOrder(&lt;br /&gt;
      self.clients, nexus.make_market_order_fields(&lt;br /&gt;
        self.ticker, nexus.Side.ASK, 100), datetime.timedelta(minutes=1))&lt;br /&gt;
    beam.flush_pending_routines()&lt;br /&gt;
    self.environment.advance_time(datetime.timedelta(seconds=30))&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Avoid using &amp;lt;code&amp;gt;flush_pending_routines&amp;lt;/code&amp;gt; as much as possible in favor of&lt;br /&gt;
other synchronization options.&lt;br /&gt;
&lt;br /&gt;
== Testing composition ==&lt;br /&gt;
&lt;br /&gt;
Tests should focus on the side effects of a machine, for example by monitoring&lt;br /&gt;
the &amp;lt;code&amp;gt;orders&amp;lt;/code&amp;gt; publisher and matching published orders against the&lt;br /&gt;
environment's submissions by &amp;lt;code&amp;gt;info.id&amp;lt;/code&amp;gt;, and asserting the close. A&lt;br /&gt;
normal termination breaks the monitor's queue with&lt;br /&gt;
&amp;lt;code&amp;gt;beam.PipeBrokenException&amp;lt;/code&amp;gt; while a failure delivers the original&lt;br /&gt;
exception:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    self.environment.reject(expected_order)&lt;br /&gt;
    order.wait()&lt;br /&gt;
    orders.pop()&lt;br /&gt;
    self.assertRaises(RuntimeError, orders.pop)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== What to cover ==&lt;br /&gt;
&lt;br /&gt;
Cover every edge of the specification's graph, and beyond the graph, the&lt;br /&gt;
policies:&lt;br /&gt;
&lt;br /&gt;
* The happy path, including the surface (publication and normal close).&lt;br /&gt;
* Rejection, including the failure close.&lt;br /&gt;
* Every condition in both directions ie. the peg's away-move repricing ''and'' the toward-move hold.&lt;br /&gt;
* Partial fills, and terminal states that leave shares remaining to be filled.&lt;br /&gt;
* Every cancellation lane: while resting, while pending acceptance (resolved by acceptance ''and'' by rejection), while idle, and cancelling an already-completed order as a no-op.&lt;br /&gt;
* For schedule-driven machines: pacing between intervals, catch-up after a slow fill, and the overdue remainder when the schedule is exhausted.&lt;br /&gt;
&lt;br /&gt;
A scenario that only exists to make a policy visible is worth having, the&lt;br /&gt;
toward-move test asserts that ''nothing'' happens, which is the policy.&lt;br /&gt;
&lt;br /&gt;
== Debugging ==&lt;br /&gt;
&lt;br /&gt;
* '''A silent hang — the suite terminates with no verbose output — usually is not a deadlock.''' It means an assertion raised mid-scenario and teardown wedged on the still-live machine. Diagnose by running tests individually (&amp;lt;code&amp;gt;python3 pegged_order_tester.py TestPeggedOrder.test_fill&amp;lt;/code&amp;gt;) before suspecting the machine.&lt;br /&gt;
* A probe script that constructs the machine, drives one step, sleeps briefly, and prints &amp;lt;code&amp;gt;order._state&amp;lt;/code&amp;gt; localizes where a machine actually stops — cheap and effective when a scenario disagrees with your reading of the specification.&lt;/div&gt;</summary>
		<author><name>Kamal</name></author>
		
	</entry>
</feed>