Skip to content

Commit 85d3bca

Browse files
authored
feat: Add unpersistent versions of persistent behaviors (#2456) (#2465)
(cherry picked from commit 523c05f)
1 parent 399d6b0 commit 85d3bca

File tree

9 files changed

+1696
-3
lines changed

9 files changed

+1696
-3
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* license agreements; and to You under the Apache License, version 2.0:
4+
*
5+
* https://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* This file is part of the Apache Pekko project, which was derived from Akka.
8+
*/
9+
10+
/*
11+
* Copyright (C) 2022 Lightbend Inc. <https://www.lightbend.com>
12+
*/
13+
14+
package jdocs.org.apache.pekko.cluster.sharding.typed;
15+
16+
import org.apache.pekko.Done;
17+
import org.scalatestplus.junit.JUnitSuite;
18+
19+
import static jdocs.org.apache.pekko.cluster.sharding.typed.AccountExampleWithEventHandlersInState.AccountEntity;
20+
import static org.junit.Assert.*;
21+
22+
// #test
23+
import java.math.BigDecimal;
24+
25+
import org.apache.pekko.actor.testkit.typed.javadsl.BehaviorTestKit;
26+
import org.apache.pekko.actor.testkit.typed.javadsl.ReplyInbox;
27+
import org.apache.pekko.actor.testkit.typed.javadsl.StatusReplyInbox;
28+
import org.apache.pekko.persistence.testkit.javadsl.UnpersistentBehavior;
29+
import org.apache.pekko.persistence.testkit.javadsl.PersistenceEffect;
30+
import org.apache.pekko.persistence.typed.PersistenceId;
31+
import org.junit.Test;
32+
33+
public class AccountExampleUnpersistentDocTest
34+
// #test
35+
extends JUnitSuite
36+
// #test
37+
{
38+
@Test
39+
public void createWithEmptyBalance() {
40+
UnpersistentBehavior<AccountEntity.Command, AccountEntity.Event, AccountEntity.Account>
41+
unpersistent = emptyAccount();
42+
43+
BehaviorTestKit<AccountEntity.Command> testkit = unpersistent.getBehaviorTestKit();
44+
45+
StatusReplyInbox<Done> ackInbox = testkit.runAskWithStatus(AccountEntity.CreateAccount::new);
46+
47+
ackInbox.expectValue(Done.getInstance());
48+
unpersistent.getEventProbe().expectPersisted(AccountEntity.AccountCreated.INSTANCE);
49+
50+
// internal state is only exposed by the behavior via responses to messages or if it happens
51+
// to snapshot. This particular behavior never snapshots, so we query within the actor's
52+
// protocol
53+
assertFalse(unpersistent.getSnapshotProbe().hasEffects());
54+
55+
ReplyInbox<AccountEntity.CurrentBalance> currentBalanceInbox =
56+
testkit.runAsk(AccountEntity.GetBalance::new);
57+
58+
assertEquals(BigDecimal.ZERO, currentBalanceInbox.receiveReply().balance);
59+
}
60+
61+
@Test
62+
public void handleDepositAndWithdraw() {
63+
UnpersistentBehavior<AccountEntity.Command, AccountEntity.Event, AccountEntity.Account>
64+
unpersistent = openedAccount();
65+
66+
BehaviorTestKit<AccountEntity.Command> testkit = unpersistent.getBehaviorTestKit();
67+
BigDecimal currentBalance;
68+
69+
testkit
70+
.runAskWithStatus(
71+
Done.class, replyTo -> new AccountEntity.Deposit(BigDecimal.valueOf(100), replyTo))
72+
.expectValue(Done.getInstance());
73+
74+
assertEquals(
75+
BigDecimal.valueOf(100),
76+
unpersistent
77+
.getEventProbe()
78+
.expectPersistedClass(AccountEntity.Deposited.class)
79+
.persistedObject()
80+
.amount);
81+
82+
currentBalance =
83+
testkit
84+
.runAsk(AccountEntity.CurrentBalance.class, AccountEntity.GetBalance::new)
85+
.receiveReply()
86+
.balance;
87+
88+
assertEquals(BigDecimal.valueOf(100), currentBalance);
89+
90+
testkit
91+
.runAskWithStatus(
92+
Done.class, replyTo -> new AccountEntity.Withdraw(BigDecimal.valueOf(10), replyTo))
93+
.expectValue(Done.getInstance());
94+
95+
// can save the persistence effect for in-depth inspection
96+
PersistenceEffect<AccountEntity.Withdrawn> withdrawEffect =
97+
unpersistent.getEventProbe().expectPersistedClass(AccountEntity.Withdrawn.class);
98+
assertEquals(BigDecimal.valueOf(10), withdrawEffect.persistedObject().amount);
99+
assertEquals(3L, withdrawEffect.sequenceNr());
100+
assertTrue(withdrawEffect.tags().isEmpty());
101+
102+
currentBalance =
103+
testkit
104+
.runAsk(AccountEntity.CurrentBalance.class, AccountEntity.GetBalance::new)
105+
.receiveReply()
106+
.balance;
107+
108+
assertEquals(BigDecimal.valueOf(90), currentBalance);
109+
}
110+
111+
@Test
112+
public void rejectWithdrawOverdraft() {
113+
UnpersistentBehavior<AccountEntity.Command, AccountEntity.Event, AccountEntity.Account>
114+
unpersistent = accountWithBalance(BigDecimal.valueOf(100));
115+
116+
BehaviorTestKit<AccountEntity.Command> testkit = unpersistent.getBehaviorTestKit();
117+
118+
testkit
119+
.runAskWithStatus(
120+
Done.class, replyTo -> new AccountEntity.Withdraw(BigDecimal.valueOf(110), replyTo))
121+
.expectErrorMessage("not enough funds to withdraw 110");
122+
123+
assertFalse(unpersistent.getEventProbe().hasEffects());
124+
}
125+
126+
// #test
127+
private UnpersistentBehavior<AccountEntity.Command, AccountEntity.Event, AccountEntity.Account>
128+
emptyAccount() {
129+
return
130+
// #unpersistent-behavior
131+
UnpersistentBehavior.fromEventSourced(
132+
AccountEntity.create("1", PersistenceId.of("Account", "1")),
133+
null, // use the initial state
134+
0 // initial sequence number
135+
);
136+
// #unpersistent-behavior
137+
}
138+
139+
private UnpersistentBehavior<AccountEntity.Command, AccountEntity.Event, AccountEntity.Account>
140+
openedAccount() {
141+
return
142+
// #unpersistent-behavior-provided-state
143+
UnpersistentBehavior.fromEventSourced(
144+
AccountEntity.create("1", PersistenceId.of("Account", "1")),
145+
new AccountEntity.EmptyAccount()
146+
.openedAccount(), // duplicate the event handler for AccountCreated on an EmptyAccount
147+
1 // assume that CreateAccount was the first command
148+
);
149+
// #unpersistent-behavior-provided-state
150+
}
151+
152+
private UnpersistentBehavior<AccountEntity.Command, AccountEntity.Event, AccountEntity.Account>
153+
accountWithBalance(BigDecimal balance) {
154+
return UnpersistentBehavior.fromEventSourced(
155+
AccountEntity.create("1", PersistenceId.of("Account", "1")),
156+
new AccountEntity.OpenedAccount(balance),
157+
2);
158+
}
159+
// #test
160+
}
161+
// #test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* license agreements; and to You under the Apache License, version 2.0:
4+
*
5+
* https://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* This file is part of the Apache Pekko project, which was derived from Akka.
8+
*/
9+
10+
/*
11+
* Copyright (C) 2022 Lightbend Inc. <https://www.lightbend.com>
12+
*/
13+
14+
package docs.org.apache.pekko.cluster.sharding.typed
15+
16+
import org.apache.pekko
17+
import pekko.Done
18+
import org.scalatest.matchers.should.Matchers
19+
import org.scalatest.wordspec.AnyWordSpecLike
20+
21+
// #test
22+
import pekko.persistence.testkit.scaladsl.UnpersistentBehavior
23+
import pekko.persistence.typed.PersistenceId
24+
25+
class AccountExampleUnpersistentDocSpec
26+
extends AnyWordSpecLike
27+
// #test
28+
with Matchers
29+
// #test
30+
{
31+
// #test
32+
import AccountExampleWithEventHandlersInState.AccountEntity
33+
// #test
34+
"Account" must {
35+
"be created with zero balance" in {
36+
onAnEmptyAccount { (testkit, eventProbe, snapshotProbe) =>
37+
testkit.runAskWithStatus[Done](AccountEntity.CreateAccount(_)).expectDone()
38+
39+
eventProbe.expectPersisted(AccountEntity.AccountCreated)
40+
41+
// internal state is only exposed by the behavior via responses to messages or if it happens
42+
// to snapshot. This particular behavior never snapshots, so we query within the actor's
43+
// protocol
44+
snapshotProbe.hasEffects shouldBe false
45+
46+
testkit.runAsk[AccountEntity.CurrentBalance](AccountEntity.GetBalance(_)).receiveReply().balance shouldBe 0
47+
}
48+
}
49+
50+
"handle Deposit and Withdraw" in {
51+
onAnOpenedAccount { (testkit, eventProbe, _) =>
52+
testkit.runAskWithStatus[Done](AccountEntity.Deposit(100, _)).expectDone()
53+
54+
eventProbe.expectPersisted(AccountEntity.Deposited(100))
55+
56+
testkit.runAskWithStatus[Done](AccountEntity.Withdraw(10, _)).expectDone()
57+
58+
eventProbe.expectPersisted(AccountEntity.Withdrawn(10))
59+
60+
testkit.runAsk[AccountEntity.CurrentBalance](AccountEntity.GetBalance(_)).receiveReply().balance shouldBe 90
61+
}
62+
}
63+
64+
"reject Withdraw overdraft" in {
65+
onAnAccountWithBalance(100) { (testkit, eventProbe, _) =>
66+
testkit.runAskWithStatus(AccountEntity.Withdraw(110, _)).receiveStatusReply().isError shouldBe true
67+
68+
eventProbe.hasEffects shouldBe false
69+
}
70+
}
71+
}
72+
// #test
73+
74+
// #unpersistent-behavior
75+
private def onAnEmptyAccount
76+
: UnpersistentBehavior.EventSourced[AccountEntity.Command, AccountEntity.Event, AccountEntity.Account] =
77+
UnpersistentBehavior.fromEventSourced(AccountEntity("1", PersistenceId("Account", "1")))
78+
// #unpersistent-behavior
79+
80+
// #unpersistent-behavior-provided-state
81+
private def onAnOpenedAccount
82+
: UnpersistentBehavior.EventSourced[AccountEntity.Command, AccountEntity.Event, AccountEntity.Account] =
83+
UnpersistentBehavior.fromEventSourced(
84+
AccountEntity("1", PersistenceId("Account", "1")),
85+
Some(
86+
AccountEntity.EmptyAccount.applyEvent(AccountEntity.AccountCreated) -> // reuse the event handler
87+
1L // assume that CreateAccount was the first command
88+
))
89+
// #unpersistent-behavior-provided-state
90+
91+
private def onAnAccountWithBalance(balance: BigDecimal) =
92+
UnpersistentBehavior.fromEventSourced(
93+
AccountEntity("1", PersistenceId("Account", "1")),
94+
Some(AccountEntity.OpenedAccount(balance) -> 2L))
95+
// #test
96+
}
97+
// #test

docs/src/main/paradox/typed/persistence-testing.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,49 @@ To use Pekko Persistence TestKit, add the module to your project:
1919

2020
@@project-info{ projectId="persistence-testkit" }
2121

22-
## Unit testing
22+
## Unit testing with the BehaviorTestKit
2323

24-
**Note!** The `EventSourcedBehaviorTestKit` is a new feature, api may have changes breaking source compatibility in future versions.
24+
**Note!** The `UnpersistentBehavior` is a new feature: the API may have changes breaking source compatibility in future versions.
25+
26+
Unit testing of `EventSourcedBehavior` can be performed by converting it into an @apidoc[UnpersistentBehavior]. Instead of
27+
persisting events and snapshots, the `UnpersistentBehavior` exposes @apidoc[PersistenceProbe]s for events and snapshots which
28+
can be asserted on.
29+
30+
Scala
31+
: @@snip [AccountExampleUnpersistentDocSpec.scala](/cluster-sharding-typed/src/test/scala/docs/org/apache/pekko/cluster/sharding/typed/AccountExampleUnpersistentDocSpec.scala) { #unpersistent-behavior }
32+
33+
Java
34+
: @@snip [AccountExampleUnpersistentDocTest.java](/cluster-sharding-typed/src/test/java/jdocs/org/apache/pekko/cluster/sharding/typed/AccountExampleUnpersistentDocTest.java) { #unpersistent-behavior }
35+
36+
The `UnpersistentBehavior` can be initialized with arbitrary states:
37+
38+
Scala
39+
: @@snip [AccountExampleUnpersistentDocSpec.scala](/cluster-sharding-typed/src/test/scala/docs/org/apache/pekko/cluster/sharding/typed/AccountExampleUnpersistentDocSpec.scala) { #unpersistent-behavior-provided-state }
40+
41+
Java
42+
: @@snip [AccountExampleUnpersistentDocTest.java](/cluster-sharding-typed/src/test/java/jdocs/org/apache/pekko/cluster/sharding/typed/AccountExampleUnpersistentDocTest.java) { #unpersistent-behavior-provided-state }
43+
44+
The `UnpersistentBehavior` is especially well-suited to the synchronous @ref:[`BehaviorTestKit`](testing-sync.md#synchronous-behavior-testing):
45+
the `UnpersistentBehavior` can directly construct a `BehaviorTestKit` wrapping the behavior. When commands are run by `BehaviorTestKit`,
46+
they are processed in the calling thread (viz. the test suite), so when the run returns, the suite can be sure that the message has been
47+
fully processed. The internal state of the `EventSourcedBehavior` is not exposed to the suite except to the extent that it affects how
48+
the behavior responds to commands or the events it persists (in addition, any snapshots made by the behavior are available through a
49+
`PersistenceProbe`).
50+
51+
A full test for the `AccountEntity`, which is shown in the @ref:[Persistence Style Guide](persistence-style.md) might look like:
52+
53+
Scala
54+
: @@snip [AccountExampleUnpersistentDocSpec.scala](/cluster-sharding-typed/src/test/scala/docs/org/apache/pekko/cluster/sharding/typed/AccountExampleUnpersistentDocSpec.scala) { #test }
55+
56+
Java
57+
: @@snip [AccountExampleUnpersistentDocTest.java](/cluster-sharding-typed/src/test/java/jdocs/org/apache/pekko/cluster/sharding/typed/AccountExampleUnpersistentDocTest.java) { #test }
58+
59+
`UnpersistentBehavior` does not require any configuration. It therefore does not verify the serialization of commands, events, or state.
60+
If using this style, it is advised to independently test serialization for those classes.
61+
62+
## Unit testing with the the ActorTestKit and EventSourcedBehaviorTestKit
63+
64+
**Note!** The `EventSourcedBehaviorTestKit` is a new feature: the API may have changes breaking source compatibility in future versions.
2565

2666
Unit testing of `EventSourcedBehavior` can be done with the @apidoc[EventSourcedBehaviorTestKit]. It supports running
2767
one command at a time and you can assert that the synchronously returned result is as expected. The result contains the

docs/src/main/paradox/typed/testing-sync.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ limitations:
1212
* Spawning of @scala[`Future`]@java[`CompletionStage`] or other asynchronous task and you rely on a callback to
1313
complete before observing the effect you want to test.
1414
* Usage of scheduler is not supported.
15-
* `EventSourcedBehavior` can't be tested.
15+
* `EventSourcedBehavior` can't be fully tested, but it is possible to @ref:[test the core functionality](persistence-testing.md#unit-testing-with-the-behaviortestkit)
1616
* Interactions with other actors must be stubbed.
1717
* Blackbox testing style.
1818
* Supervision is not supported.

0 commit comments

Comments
 (0)