bootstarted / effects

Almost-extensible effects for Elixir.
27 stars 0 forks source link

CQRS/ES testing #10

Closed henry-hz closed 7 years ago

henry-hz commented 7 years ago

Dear @izaakschroeder , amazing lib! please, how should we use Effects to test Aggregate Roots data structures (that contains an apply function to change it's state) ?
see: https://github.com/slashdotdash/commanded/blob/master/test/example_domain/bank_account/bank_account.ex

We would like to make a complete pure test framework for Aggregates and Process Managers.

I have isolated and organized them in a Pure CQRS/ES project, https://github.com/work-capital/ev_sim , pls, see account aggregate

izaakschroeder commented 7 years ago

Roughly you would look at turning your Commands into effects. The following structures can be converts into effects with defeffect:

defmodule Commands do
    defmodule OpenAccount,    do: defstruct [:account_number, :initial_balance]
    defmodule DepositMoney,   do: defstruct [:account_number, :transfer_uuid, :amount]
    defmodule WithdrawMoney,  do: defstruct [:account_number, :transfer_uuid, :amount]
    defmodule CloseAccount,   do: defstruct [:account_number]
  end

defeffect open_account (account_number) do
  %OpenAccount{account_number: account_number, initial_balance: 0}
end

# ...

You would do similar things with DepositMoney, CloseAccount and so forth. Once you have the effects created you can start composing them together. Here is an example that opens an account, deposits money into it, and then, if the account balance is less than 15, closes it.

# pure, ~> and ~>> are from the effects library

action = open_account(1234) ~> deposit_money(1234, 10.22) ~>> fn account ->
  if account.balance < 15 do
    close_account(account.id)
  else
    pure(account)
  end
end

The action you have is just an effect that represents a sequence of things. No "dangerous" code has actually taken place. You then need to build an interpreter for your effects (i.e. what does DepositMoney mean).

# queue_apply is from the effects library

def interpret(%Effect{effect: %OpenAccount{account_id: account_id}, next: next}) do
  new_account = %Account{id: account_id, balance: 0}
  interpret(queue_apply(next, new_account))
end

def interpret(%Effect{effect: %DepositMoney{account_id: id, amount: a}, next: next}) do
  # TODO: Get previous account from DB or test env or whatever
  previous_account = get_account(id)
  updated_account = {previous_account | balance: previous_account.balance + a}
  interpret(queue_apply(next, updated_account))
end

# ...

You need to define what action is taken on each effect. There are lots of fancier ways to define your interpret function, examples of which can found in the test folder in the project. 😄

Once you have an interpreter you can send your effects/actions through it.

final_account = interpret(action)

You can then define interpreters for any kind of behaviour on your effects that you wish, whether it be effects that are no-ops for testing or calls to databases or whatever.

NOTE: This library while fairly complete isn't necessarily super polished. You may find some odd things. e.g. queue_apply isn't the most appropriately named. Best source of knowledge is the tests folder.

izaakschroeder commented 7 years ago

As an additional resource you may find the following repository useful: https://github.com/metalabdesign/effects-workshop and its corresponding slides: https://docs.google.com/presentation/d/1JwS5ZrjnFucpMSRyjIt5Sd_rHmUJoyIKN3vGJa8YrzA/edit?usp=sharing

henry-hz commented 7 years ago

@izaakschroeder thanks for the complete and fast feedback. the example you sent is very good for multi-scenario modeling with gherkin,

action = open_account(1234) ~> deposit_money(1234, 10.22) ~>> fn account ->
  if account.balance < 15 do
    close_account(account.id)
  else
    pure(account)
  end
end

but, from the CRQR/ES implementation, we must have the pattern "command" -> create a new "event" -> apply(event) into one defmodule scope i.e. we will use this structure into gen_servers to receive commands from command handlers. See how commanded run the domain model. For complex business applications, a semantic boundary protects the conceptual model from ambiguities, and a 'lego' application is built, using eventual consistency to update different contexts or services around. the point is that all the business logic should be into the aggregate scope. @slashdotdash made another lib, eventsourced that solves it partially, but it's not used in commanded, once the 'update' function is too verbose, he decided to move this into the gen_server, and use this

defp apply_events(aggregate_module, aggregate_state, events)do                                                                                                                                                                     Enum.reduce(events, aggregate_state, &aggregate_module.apply(&2, &1))                                                                                                                                                                     end

to replay events and rebuild the state. if we could 'point' to aggregate_module.apply and let that scope apply the events to change the aggregate state, using effects, would be the ideal solution, pls, if you can help us with that :100:

henry-hz commented 7 years ago

this is the Monadex suggestion: https://github.com/rob-brown/MonadEx/issues/6 in CQRS/ES, Events are the cause of state mutation, and commands are causing events to be created

izaakschroeder commented 7 years ago

The event handlers just map 1:1 to interpreters in this kind of scenario. Except instead of using gen_server, an interpreter keeps track of its own state. You can observe this pattern in the following example:

def interpret(
  previous_state,
  %Effect{effect: %OpenAccount{account_id: account_id}, next: next}
) do
  new_account = %Account{id: account_id, balance: 0}
  next_state = %{previous_state | accounts: [new_account]}
  interpret(next_state, queue_apply(next, new_account))
end

if we could 'point' to aggregate_module.apply and let that scope apply the events to change the aggregate state, using effects

I'm not sure what this means. What you're referring to seems to be analogous to a "process manager" here: https://github.com/slashdotdash/commanded#process-managers. As far as effects are concerned, this can be modelled as another interpreter exactly as above.

def apply(
  %TransferMoneyProcessManager{} = transfer, 
  %MoneyTransferRequested{
    transfer_uuid: transfer_uuid, 
    debit_account: debit_account, 
    credit_account: credit_account, 
    amount: amount
}) do
    %TransferMoneyProcessManager{transfer |
      transfer_uuid: transfer_uuid,
      debit_account: debit_account,
      credit_account: credit_account,
      amount: amount,
      status: :withdraw_money_from_debit_account
    }
  end

Becomes:

def interpret(
  local_state, 
  %Effect{next: next, effect: %MoneyTransferRequested{
    transfer_uuid: transfer_uuid, 
    debit_account: debit_account, 
    credit_account: credit_account, 
    amount: amount
  }}
) do
    next_state = %{local_state |
      transfer_uuid: transfer_uuid,
      debit_account: debit_account,
      credit_account: credit_account,
      amount: amount,
      status: :withdraw_money_from_debit_account
    }
    interpret(next_state, queue_apply(next, some_result_here))
  end
henry-hz commented 7 years ago

yes, this I got it, the problem is that we are implementing using the

def apply(
  %TransferMoneyProcessManager{} = transfer, 
  %MoneyTransferRequested{
    transfer_uuid: transfer_uuid, 
    debit_account: debit_account, 
    credit_account: credit_account, 
    amount: amount
}) do
    %TransferMoneyProcessManager{transfer |
      transfer_uuid: transfer_uuid,
      debit_account: debit_account,
      credit_account: credit_account,
      amount: amount,
      status: :withdraw_money_from_debit_account
    }
  end

because only once this code should be written, and this is how the aggregate_instance is handling them. we are looking for a way to be possible to write this aggregates in a way that they continue to be a pure data structure (without macros, monads, etc...), only defstruct + apply, as you see here . So we can have one team dealing with the business modeling + testing, and the same 'business modeling' will be pasted into the 'side effects' environment. In less words, we can't refactor commanded to support monads, we have to make a work around to inject aggregates and process_manager data structure with their respective apply's into 'effects' and have a 'pure' environment to test. in the future, a GUI could make diagrams and simulate this business models.

izaakschroeder commented 7 years ago

I guess a sensible thing to do in this case might be just to make another implementation of a gen_server for testing that responds to all the same messages? I'm not sure of other options given the desire not to change the interface commanded is using. You could then configure it to respond to messages deterministically using the instance's initial state which seems reasonable enough, bypassing the need for any kind of monad. It just means that your gen_server should only be responsible for executing actions and not doing business logic (analog to the interpreter here). You can configure which gen_server your app uses with @variables. e.g. @manager System.get_env(:myapp, :manager_type) and then set :manager_type to be SomeModule.TestServer or SomeModule.RealServer where appropriate.

As far as testing going you could create additional handlers on your mock gen_server instance to respond back to the caller with the server's current state and to programatically alter/inject the server's state.

You could make the state an effect itself but I don't think that would be very useful – just another layer of unnecessary abstraction since whatever the server returns to caller's would then also have to be an effect and I imagine the callee wants to do something useful with the data.

henry-hz commented 7 years ago

Yes, this is the same path suggested by @slatdodash, the commanded author, to build a black box for system testing and modeling. I will still looking for an opportunity to use effect in our projects :smiley:

izaakschroeder commented 7 years ago

No worries happy to help! 😄