Closed amomchilov closed 4 years ago
This can be done by creating the transition records since they are just normal DB entries.
E.g a model foo
that can transition A -> B -> C -> D
FactoryBot.define do
factory :foo_transition do
association :foo
sequence(:sort_key) { |n| (n * 10) }
end
end
# inside spec/models/foo_spec.rb
let(:foo) { FactoryBot.create(:foo) }
before do
FactoryBot.create(:foo_transition, to_state: "A", most_recent: false, foo: foo)
FactoryBot.create(:foo_transition, to_state: "B", most_recent: false, foo: foo)
FactoryBot.create(:foo_transition, to_state: "C", most_recent: true, foo: foo)
end
it { expect(foo.current_state).to eq("C") }
it { expect(foo.history.map(&:to_state).to eq(["A", "B", "C"]) }
it { expect { foo.transition_to!("D") }.to_not raise_error }
If you only test this way you would have to be quite strict in the contents of the transition callbacks.
It probably doesnt matter if your transition graph is simple like above but if it allows cycles e.g A -> B -> C -> B -> C -> D
or alternate paths e.g A -> X -> C -> D
there is a chance of having untested code paths if any callback interacts with the model.
Minor complications: setting the most_recent
flag to false
breaks the uniqueness constraint imposed by the parent_model_id+most_recent
index. Had to use nil
instead. This is using MySQL, btw.
I'm curious: under what cases will the most recent record be anything other than the one with the maximal sort key? And why is the sort key necessary? Wouldn't a created_at
timestamp be sufficient?
The most_recent
is my mistake. We work with PostgreSQL and use true
& false
.
The most_recent
should always be the maximal sort key (if not it's a bug). The reason we have both is that we often have select a lot of records in the same state e.g Foo.in_state(:A)
Since we know this ahead of time we can add an index on to_state = "A" AND most_recent
which greatly helps us out.
Dropping sort_key
for created_at
works in theory. However we have found that using sort key allows us to 'backfill' states as we state machines evolve over time.
For example.
Foo.transition_to!(:B)
as a side effect sends an email requesting a customer send back some information. The customer responds via email with the info. This is entered into the system via an admin interface and the record is transitioned to C
In the future you decide to automate this and you add another state between B
and C
called waiting_for_info
.
For all of the customers where you entered the details by hand you could insert this state with a sort_key of 15
to track the fact that it happened in the history of the object.
If you did this sorting by created_at
wouldn't work.
Hey guys, I've tried reading all the docs, reverse engineering the source code, etc., but I can't figure this out. I would really appreciate it if you could point me in the right direction.
A have a model (subclass of
ActiveRecord::Base
) that encapsulates a Statesman state machine. I would like to test this model class, which requires me to be able to configure the state of the model, including its internal state machine.How can I create a new model object, in a particular initial state?
I know I can create a new model (with its state implicitly set to the initial state) and manually push it through a series of state transitions, but that triggers call backs, wastes time, and ruins the isolation of the test.