This is an automated email from the ASF dual-hosted git repository.

hepin pushed a commit to branch 1.3.x-askBehaviorKit
in repository https://gitbox.apache.org/repos/asf/pekko.git

commit bbbabadeb35dae2912e8b2685c28655e659abfb2
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Sun Nov 9 13:54:12 2025 +0800

    feat: Add asking support to BehaviorTestKit (#2453)
    
    (cherry picked from commit 82692c86467b212d3d8525a3bad4aa608c952446)
---
 .../add-ask-behavior-methods.excludes              |  22 ++++
 .../typed/internal/BehaviorTestKitImpl.scala       |  25 ++++-
 .../testkit/typed/internal/TestInboxImpl.scala     | 121 ++++++++++++++++++++-
 .../testkit/typed/javadsl/BehaviorTestKit.scala    |  59 +++++++++-
 .../actor/testkit/typed/javadsl/TestInbox.scala    |  84 +++++++++++++-
 .../testkit/typed/scaladsl/BehaviorTestKit.scala   |  19 ++++
 .../actor/testkit/typed/scaladsl/TestInbox.scala   |  94 ++++++++++++++++
 .../testkit/typed/javadsl/BehaviorTestKitTest.java |  14 +--
 .../typed/scaladsl/BehaviorTestKitSpec.scala       |  26 ++---
 9 files changed, 438 insertions(+), 26 deletions(-)

diff --git 
a/actor-testkit-typed/src/main/mima-filters/2.0.x.backwards.excludes/add-ask-behavior-methods.excludes
 
b/actor-testkit-typed/src/main/mima-filters/2.0.x.backwards.excludes/add-ask-behavior-methods.excludes
new file mode 100644
index 0000000000..7dcbf1fa06
--- /dev/null
+++ 
b/actor-testkit-typed/src/main/mima-filters/2.0.x.backwards.excludes/add-ask-behavior-methods.excludes
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Add ask behavior methods
+ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.actor.testkit.typed.javadsl.BehaviorTestKit.runAsk")
+ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.actor.testkit.typed.javadsl.BehaviorTestKit.runAskWithStatus")
+ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.actor.testkit.typed.scaladsl.BehaviorTestKit.runAsk")
+ProblemFilters.exclude[ReversedMissingMethodProblem]("org.apache.pekko.actor.testkit.typed.scaladsl.BehaviorTestKit.runAskWithStatus")
diff --git 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/internal/BehaviorTestKitImpl.scala
 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/internal/BehaviorTestKitImpl.scala
index 534b06fd11..0ab4376b13 100644
--- 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/internal/BehaviorTestKitImpl.scala
+++ 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/internal/BehaviorTestKitImpl.scala
@@ -29,8 +29,10 @@ import pekko.actor.typed.internal.{ AdaptMessage, 
AdaptWithRegisteredMessageAdap
 import pekko.actor.typed.receptionist.Receptionist
 import pekko.actor.typed.scaladsl.Behaviors
 import pekko.annotation.InternalApi
+import pekko.japi.function.{ Function => JFunction }
+import pekko.pattern.StatusReply
+import pekko.util.OptionVal
 import pekko.util.ccompat.JavaConverters._
-
 /**
  * INTERNAL API
  */
@@ -61,6 +63,27 @@ private[pekko] final class BehaviorTestKitImpl[T](
   // execute any future tasks scheduled in Actor's constructor
   runAllTasks()
 
+  override def runAsk[Res](f: ActorRef[Res] => T): ReplyInboxImpl[Res] = {
+    val replyToInbox = TestInboxImpl[Res]("replyTo")
+
+    run(f(replyToInbox.ref))
+    new ReplyInboxImpl(OptionVal(replyToInbox))
+  }
+
+  override def runAsk[Res](messageFactory: JFunction[ActorRef[Res], T]): 
ReplyInboxImpl[Res] =
+    runAsk(messageFactory.apply _)
+
+  override def runAskWithStatus[Res](f: ActorRef[StatusReply[Res]] => T): 
StatusReplyInboxImpl[Res] = {
+    val replyToInbox = TestInboxImpl[StatusReply[Res]]("replyTo")
+
+    run(f(replyToInbox.ref))
+    new StatusReplyInboxImpl(OptionVal(replyToInbox))
+  }
+
+  override def runAskWithStatus[Res](
+      messageFactory: JFunction[ActorRef[StatusReply[Res]], T]): 
StatusReplyInboxImpl[Res] =
+    runAskWithStatus(messageFactory.apply _)
+
   override def retrieveEffect(): Effect = context.effectQueue.poll() match {
     case null => NoEffects
     case x    => x
diff --git 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/internal/TestInboxImpl.scala
 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/internal/TestInboxImpl.scala
index ae0f1f26a8..9dc91cc678 100644
--- 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/internal/TestInboxImpl.scala
+++ 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/internal/TestInboxImpl.scala
@@ -19,9 +19,11 @@ import scala.annotation.tailrec
 import scala.collection.immutable
 
 import org.apache.pekko
-import pekko.actor.ActorPath
+import pekko.actor.{ ActorPath, Address, RootActorPath }
 import pekko.actor.typed.ActorRef
 import pekko.annotation.InternalApi
+import pekko.pattern.StatusReply
+import pekko.util.OptionVal
 
 /**
  * INTERNAL API
@@ -63,3 +65,120 @@ private[pekko] final class TestInboxImpl[T](path: ActorPath)
   @InternalApi private[pekko] def as[U]: TestInboxImpl[U] = 
this.asInstanceOf[TestInboxImpl[U]]
 
 }
+
+/**
+ * INTERNAL API
+ */
+@InternalApi
+object TestInboxImpl {
+  def apply[T](name: String): TestInboxImpl[T] = {
+    new TestInboxImpl(address / name)
+  }
+
+  private[pekko] val address = 
RootActorPath(Address("pekko.actor.typed.inbox", "anonymous"))
+}
+
+/**
+ * INTERNAL API
+ */
+@InternalApi
+private[pekko] final class ReplyInboxImpl[T](private var underlying: 
OptionVal[TestInboxImpl[T]])
+    extends pekko.actor.testkit.typed.javadsl.ReplyInbox[T]
+    with pekko.actor.testkit.typed.scaladsl.ReplyInbox[T] {
+
+  def receiveReply(): T =
+    underlying match {
+      case OptionVal.Some(testInbox) =>
+        underlying = OptionVal.None
+        testInbox.receiveMessage()
+
+      case _ => throw new AssertionError("Reply was already received")
+    }
+
+  def expectReply(expectedReply: T): Unit =
+    receiveReply() match {
+      case matches if matches == expectedReply => ()
+      case doesntMatch                         =>
+        throw new AssertionError(s"Expected $expectedReply but received 
$doesntMatch")
+    }
+
+  def expectNoReply(): ReplyInboxImpl[T] =
+    underlying match {
+      case OptionVal.Some(testInbox) if testInbox.hasMessages =>
+        throw new AssertionError(s"Expected no reply, but ${receiveReply()} 
was received")
+
+      case OptionVal.Some(_) => this
+
+      case _ =>
+        // already received the reply, so this expectation shouldn't even be 
made
+        throw new AssertionError("Improper expectation of no reply: reply was 
already received")
+    }
+
+  def hasReply: Boolean =
+    underlying match {
+      case OptionVal.Some(testInbox) => testInbox.hasMessages
+      case _                         => false
+    }
+}
+
+/**
+ * INTERNAL API
+ */
+@InternalApi
+private[pekko] final class StatusReplyInboxImpl[T](private var underlying: 
OptionVal[TestInboxImpl[StatusReply[T]]])
+    extends pekko.actor.testkit.typed.javadsl.StatusReplyInbox[T]
+    with pekko.actor.testkit.typed.scaladsl.StatusReplyInbox[T] {
+
+  def receiveStatusReply(): StatusReply[T] =
+    underlying match {
+      case OptionVal.Some(testInbox) =>
+        underlying = OptionVal.None
+        testInbox.receiveMessage()
+
+      case _ => throw new AssertionError("Reply was already received")
+    }
+
+  def receiveValue(): T =
+    receiveStatusReply() match {
+      case StatusReply.Success(v) => v.asInstanceOf[T]
+      case err                    => throw new AssertionError(s"Expected a 
successful reply but received $err")
+    }
+
+  def receiveError(): Throwable =
+    receiveStatusReply() match {
+      case StatusReply.Error(t) => t
+      case success              => throw new AssertionError(s"Expected an 
error reply but received $success")
+    }
+
+  def expectValue(expectedValue: T): Unit =
+    receiveValue() match {
+      case matches if matches == expectedValue => ()
+      case doesntMatch                         =>
+        throw new AssertionError(s"Expected $expectedValue but received 
$doesntMatch")
+    }
+
+  def expectErrorMessage(errorMessage: String): Unit =
+    receiveError() match {
+      case matches if matches.getMessage == errorMessage => ()
+      case doesntMatch                                   =>
+        throw new AssertionError(s"Expected a throwable with message 
$errorMessage, but got ${doesntMatch.getMessage}")
+    }
+
+  def expectNoReply(): StatusReplyInboxImpl[T] =
+    underlying match {
+      case OptionVal.Some(testInbox) if testInbox.hasMessages =>
+        throw new AssertionError(s"Expected no reply, but 
${receiveStatusReply()} was received")
+
+      case OptionVal.Some(_) => this
+
+      case _ =>
+        // already received the reply, so this expectation shouldn't even be 
made
+        throw new AssertionError("Improper expectation of no reply: reply was 
already received")
+    }
+
+  def hasReply: Boolean =
+    underlying match {
+      case OptionVal.Some(testInbox) => testInbox.hasMessages
+      case _                         => false
+    }
+}
diff --git 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/javadsl/BehaviorTestKit.scala
 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/javadsl/BehaviorTestKit.scala
index 385b6549e2..71992c42d9 100644
--- 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/javadsl/BehaviorTestKit.scala
+++ 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/javadsl/BehaviorTestKit.scala
@@ -13,15 +13,20 @@
 
 package org.apache.pekko.actor.testkit.typed.javadsl
 
+import java.util.concurrent.ThreadLocalRandom
+
+import scala.annotation.nowarn
+
 import org.apache.pekko
 import pekko.actor.testkit.typed.internal.{ ActorSystemStub, 
BehaviorTestKitImpl }
 import pekko.actor.testkit.typed.{ CapturedLogEvent, Effect }
 import pekko.actor.typed.receptionist.Receptionist
 import pekko.actor.typed.{ ActorRef, Behavior, Signal }
 import pekko.annotation.{ ApiMayChange, DoNotInherit }
-import com.typesafe.config.Config
+import pekko.japi.function.{ Function => JFunction }
+import pekko.pattern.StatusReply
 
-import java.util.concurrent.ThreadLocalRandom
+import com.typesafe.config.Config
 
 object BehaviorTestKit {
 
@@ -70,6 +75,56 @@ object BehaviorTestKit {
 @ApiMayChange
 abstract class BehaviorTestKit[T] {
 
+  /**
+   * Constructs a message using the provided 'messageFactory' to inject a 
single-use "reply to"
+   * [[akka.actor.typed.ActorRef]], and sends the constructed message to the 
behavior, recording any [[Effect]]s.
+   *
+   * The returned [[ReplyInbox]] allows the message sent to the "reply to" 
`ActorRef` to be asserted on.
+   *
+   * @since 1.3.0
+   */
+  def runAsk[Res](messageFactory: JFunction[ActorRef[Res], T]): ReplyInbox[Res]
+
+  /**
+   * The same as [[runAsk]], but with the response class specified.  This 
improves type inference in Java
+   * when asserting on the reply in the same statement as the `runAsk` as in:
+   *
+   * ```
+   * testkit.runAsk(Done.class, 
DoSomethingCommand::new).expectReply(Done.getInstance());
+   * ```
+   *
+   * If explicitly saving the [[ReplyInbox]] in a variable, the version 
without the class may be preferred.
+   *
+   * @since 1.3.0
+   */
+  @nowarn("msg=never used") // responseClass is a pretend param to guide 
inference
+  def runAsk[Res](responseClass: Class[Res], messageFactory: 
JFunction[ActorRef[Res], T]): ReplyInbox[Res] =
+    runAsk(messageFactory)
+
+  /**
+   * The same as [[runAsk]] but only for requests that result in a response of 
type [[akka.pattern.StatusReply]].
+   *
+   * @since 1.3.0
+   */
+  def runAskWithStatus[Res](messageFactory: 
JFunction[ActorRef[StatusReply[Res]], T]): StatusReplyInbox[Res]
+
+  /**
+   * The same as [[runAskWithStatus]], but with the response class specified.  
This improves type inference in
+   * Java when asserting on the reply in the same statement as the 
`runAskWithStatus` as in:
+   *
+   * ```
+   * testkit.runAskWithStatus(Done.class, 
DoSomethingWithStatusCommand::new).expectValue(Done.getInstance());
+   * ```
+   *
+   * If explicitly saving the [[StatusReplyInbox]] in a variable, the version 
without the class may be preferred.
+   *
+   * @since 1.3.0
+   */
+  @nowarn("msg=never used") // responseClass is a pretend param to guide 
inference
+  def runAskWithStatus[Res](responseClass: Class[Res],
+      messageFactory: JFunction[ActorRef[StatusReply[Res]], T]): 
StatusReplyInbox[Res] =
+    runAskWithStatus(messageFactory)
+
   /**
    * Requests the oldest [[Effect]] or 
[[pekko.actor.testkit.typed.javadsl.Effects.noEffects]] if no effects
    * have taken place. The effect is consumed, subsequent calls won't
diff --git 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/javadsl/TestInbox.scala
 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/javadsl/TestInbox.scala
index 6bd0a30f5a..1b549eda98 100644
--- 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/javadsl/TestInbox.scala
+++ 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/javadsl/TestInbox.scala
@@ -20,7 +20,8 @@ import scala.collection.immutable
 import org.apache.pekko
 import pekko.actor.testkit.typed.internal.TestInboxImpl
 import pekko.actor.typed.ActorRef
-import pekko.annotation.DoNotInherit
+import pekko.annotation.{ ApiMayChange, DoNotInherit }
+import pekko.pattern.StatusReply
 import pekko.util.ccompat.JavaConverters._
 
 object TestInbox {
@@ -76,3 +77,84 @@ abstract class TestInbox[T] {
 
   // TODO expectNoMsg etc
 }
+
+/**
+ * Similar to an [[akka.actor.testkit.typed.javadsl.TestInbox]], but can only 
ever give access to a single message (a reply).
+ *
+ * Not intended for user creation: the 
[[akka.actor.testkit.typed.javadsl.BehaviorTestKit]] will provide these to
+ * denote that at most a single reply is expected.
+ *
+ * @since 1.3.0
+ */
+@DoNotInherit
+@ApiMayChange
+trait ReplyInbox[T] {
+
+  /**
+   * Get and remove the reply.  Subsequent calls to `receiveReply`, 
`expectReply`, and `expectNoReply` will fail and `hasReplies`
+   * will be false after calling this method
+   */
+  def receiveReply(): T
+
+  /**
+   * Assert and remove the message.  Subsequent calls to `receiveReply`, 
`expectReply`, and `expectNoReply` will fail and `hasReplies`
+   * will be false after calling this method
+   */
+  def expectReply(expectedReply: T): Unit
+
+  def expectNoReply(): ReplyInbox[T]
+  def hasReply: Boolean
+}
+
+/**
+ * A [[akka.actor.testkit.typed.javadsl.ReplyInbox]] which specially handles 
[[akka.pattern.StatusReply]].
+ *
+ * Note that there is no provided ability to expect a specific `Throwable`, as 
it's recommended to prefer
+ * a string error message or to enumerate failures with specific types.
+ *
+ * Not intended for user creation: the 
[[akka.actor.testkit.typed.javadsl.BehaviorTestKit]] will provide these to
+ * denote that at most a single reply is expected.
+ */
+@DoNotInherit
+@ApiMayChange
+trait StatusReplyInbox[T] {
+
+  /**
+   * Get and remove the status reply.  Subsequent calls to any `receive` or 
`expect` method will fail and `hasReply`
+   * will be false after calling this method.
+   */
+  def receiveStatusReply(): StatusReply[T]
+
+  /**
+   * Get and remove the successful value of the status reply.  This will fail 
if the status reply is an error.
+   * Subsequent calls to any `receive` or `expect` method will fail and 
`hasReply` will be false after calling this
+   * method.
+   */
+  def receiveValue(): T
+
+  /**
+   * Get and remove the error value of the status reply.  This will fail if 
the status reply is a success.
+   * Subsequent calls to any `receive` or `expect` method will fail and 
`hasReply` will be false after calling this
+   * method.
+   */
+  def receiveError(): Throwable
+
+  /**
+   * Assert that the status reply is a success with this value and remove the 
status reply.  Subsequent calls to any
+   * `receive` or `expect` method will fail and `hasReply` will be false after 
calling this method.
+   */
+  def expectValue(expectedValue: T): Unit
+
+  /**
+   * Assert that the status reply is a failure with this error message and 
remove the status reply.  Subsequent
+   * calls to any `receive` or `expect` method will fail and `hasReply` will 
be false after calling this method.
+   */
+  def expectErrorMessage(errorMessage: String): Unit
+
+  /**
+   * Assert that this inbox has *never* received a reply.
+   */
+  def expectNoReply(): StatusReplyInbox[T]
+
+  def hasReply: Boolean
+}
diff --git 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/scaladsl/BehaviorTestKit.scala
 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/scaladsl/BehaviorTestKit.scala
index a69b05778b..446cdb0c7c 100644
--- 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/scaladsl/BehaviorTestKit.scala
+++ 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/scaladsl/BehaviorTestKit.scala
@@ -19,6 +19,8 @@ import pekko.actor.testkit.typed.{ CapturedLogEvent, Effect }
 import pekko.actor.typed.receptionist.Receptionist
 import pekko.actor.typed.{ ActorRef, Behavior, Signal, TypedActorContext }
 import pekko.annotation.{ ApiMayChange, DoNotInherit }
+import pekko.pattern.StatusReply
+
 import com.typesafe.config.Config
 
 import java.util.concurrent.ThreadLocalRandom
@@ -55,6 +57,23 @@ object BehaviorTestKit {
 @ApiMayChange
 trait BehaviorTestKit[T] {
 
+  /**
+   * Constructs a message using the provided function to inject a single-use 
"reply to" [[akka.actor.typed.ActorRef]],
+   * and sends the constructed message to the behavior, recording any 
[[Effect]]s.
+   *
+   * The returned [[ReplyInbox]] allows the message sent to the "reply to" 
`ActorRef` to be asserted on.
+   *
+   * @since 1.3.0
+   */
+  def runAsk[Res](f: ActorRef[Res] => T): ReplyInbox[Res]
+
+  /**
+   * The same as [[runAsk]] but only for requests that result in a response of 
type [[akka.pattern.StatusReply]].
+   *
+   * @since 1.3.0
+   */
+  def runAskWithStatus[Res](f: ActorRef[StatusReply[Res]] => T): 
StatusReplyInbox[Res]
+
   // FIXME it is weird that this is public but it is used in BehaviorSpec, 
could we avoid that?
   private[pekko] def context: TypedActorContext[T]
 
diff --git 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/scaladsl/TestInbox.scala
 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/scaladsl/TestInbox.scala
index 9f66b97a43..6c1a1935aa 100644
--- 
a/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/scaladsl/TestInbox.scala
+++ 
b/actor-testkit-typed/src/main/scala/org/apache/pekko/actor/testkit/typed/scaladsl/TestInbox.scala
@@ -18,10 +18,12 @@ import java.util.concurrent.ThreadLocalRandom
 import scala.collection.immutable
 
 import org.apache.pekko
+import pekko.Done
 import pekko.actor.{ Address, RootActorPath }
 import pekko.actor.testkit.typed.internal.TestInboxImpl
 import pekko.actor.typed.ActorRef
 import pekko.annotation.{ ApiMayChange, DoNotInherit }
+import pekko.pattern.StatusReply
 
 @ApiMayChange
 object TestInbox {
@@ -74,3 +76,95 @@ trait TestInbox[T] {
 
   // TODO expectNoMsg etc
 }
+
+/**
+ * Similar to an [[akka.actor.testkit.typed.scaladsl.TestInbox]], but can only 
ever give access to a single message (a reply).
+ *
+ * Not intended for user creation: the 
[[akka.actor.testkit.typed.scaladsl.BehaviorTestKit]] will provide these
+ * to denote that at most a single reply is expected.
+ *
+ * @since 1.3.0
+ */
+@DoNotInherit
+@ApiMayChange
+trait ReplyInbox[T] {
+
+  /**
+   * Get and remove the reply.  Subsequent calls to `receiveReply`, 
`expectReply`, and `expectNoReply` will fail and `hasReply`
+   * will be false after calling this method
+   */
+  def receiveReply(): T
+
+  /**
+   * Assert and remove the reply.  Subsequent calls to `receiveReply`, 
`expectReply`, and `expectNoReply` will fail and `hasReply`
+   * will be false after calling this method
+   */
+  def expectReply(expectedReply: T): Unit
+
+  /**
+   * Assert that this inbox has *never* received a reply.
+   */
+  def expectNoReply(): ReplyInbox[T]
+
+  def hasReply: Boolean
+}
+
+/**
+ * A [[akka.actor.testkit.typed.scaladsl.ReplyInbox]] which specially handles 
[[akka.pattern.StatusReply]].
+ *
+ * Note that there is no provided ability to expect a specific `Throwable`, as 
it's recommended to prefer
+ * a string error message or to enumerate failures with specific types.
+ *
+ * Not intended for user creation: the 
[[akka.actor.testkit.typed.scaladsl.BehaviorTestKit]] will provide these
+ * to denote that at most a single reply is expected.
+ */
+@DoNotInherit
+@ApiMayChange
+trait StatusReplyInbox[T] {
+
+  /**
+   * Get and remove the status reply.  Subsequent calls to any `receive` or 
`expect` method will fail and `hasReply`
+   * will be false after calling this method.
+   */
+  def receiveStatusReply(): StatusReply[T]
+
+  /**
+   * Get and remove the successful value of the status reply.  This will fail 
if the status reply is an error.
+   * Subsequent calls to any `receive` or `expect` method will fail and 
`hasReply` will be false after calling this
+   * method.
+   */
+  def receiveValue(): T
+
+  /**
+   * Get and remove the error value of the status reply.  This will fail if 
the status reply is a success.
+   * Subsequent calls to any `receive` or `expect` method will fail and 
`hasReply` will be false after calling this
+   * method.
+   */
+  def receiveError(): Throwable
+
+  /**
+   * Assert that the status reply is a success with this value and remove the 
status reply.  Subsequent calls to any
+   * `receive` or `expect` method will fail and `hasReply` will be false after 
calling this method.
+   */
+  def expectValue(expectedValue: T): Unit
+
+  /**
+   * Assert that the status reply is a failure with this error message and 
remove the status reply.  Subsequent
+   * calls to any `receive` or `expect` method will fail and `hasReply` will 
be false after calling this method.
+   */
+  def expectErrorMessage(errorMessage: String): Unit
+
+  /**
+   * Assert that the successful value of the status reply is [[akka.Done]].  
Subsequent calls to any `receive` or
+   * `expect` method will fail and `hasReply` will be false after calling this 
method.
+   */
+  @annotation.nowarn("msg=never used")
+  def expectDone()(implicit ev: T =:= Done): Unit = 
expectValue(Done.asInstanceOf[T])
+
+  /**
+   * Assert that this inbox has *never* received a reply.
+   */
+  def expectNoReply(): StatusReplyInbox[T]
+
+  def hasReply: Boolean
+}
diff --git 
a/actor-testkit-typed/src/test/java/org/apache/pekko/actor/testkit/typed/javadsl/BehaviorTestKitTest.java
 
b/actor-testkit-typed/src/test/java/org/apache/pekko/actor/testkit/typed/javadsl/BehaviorTestKitTest.java
index ea45123057..f19f2ba1ae 100644
--- 
a/actor-testkit-typed/src/test/java/org/apache/pekko/actor/testkit/typed/javadsl/BehaviorTestKitTest.java
+++ 
b/actor-testkit-typed/src/test/java/org/apache/pekko/actor/testkit/typed/javadsl/BehaviorTestKitTest.java
@@ -403,12 +403,13 @@ public class BehaviorTestKitTest extends JUnitSuite {
   @Test
   public void allowRetrievingAndKilling() {
     BehaviorTestKit<Command> test = BehaviorTestKit.create(behavior);
-    TestInbox<ActorRef<String>> i = TestInbox.create();
     TestInbox<String> h = TestInbox.create();
-    test.run(new SpawnSession(i.getRef(), h.getRef()));
 
-    ActorRef<String> sessionRef = i.receiveMessage();
-    assertFalse(i.hasMessages());
+    ReplyInbox<ActorRef<String>> sessionReply =
+        test.runAsk(replyTo -> new SpawnSession(replyTo, h.getRef()));
+
+    ActorRef<String> sessionRef = sessionReply.receiveReply();
+
     Effect.SpawnedAnonymous s = 
test.expectEffectClass(Effect.SpawnedAnonymous.class);
     assertEquals(sessionRef, s.ref());
 
@@ -416,10 +417,9 @@ public class BehaviorTestKitTest extends JUnitSuite {
     session.run("hello");
     assertEquals(Collections.singletonList("hello"), h.getAllReceived());
 
-    TestInbox<Done> d = TestInbox.create();
-    test.run(new KillSession(sessionRef, d.getRef()));
+    ReplyInbox<Done> doneReply = test.runAsk(replyTo -> new 
KillSession(sessionRef, replyTo));
+    doneReply.expectReply(Done.getInstance());
 
-    assertEquals(Collections.singletonList(Done.getInstance()), 
d.getAllReceived());
     test.expectEffectClass(Effect.Stopped.class);
   }
 
diff --git 
a/actor-testkit-typed/src/test/scala/org/apache/pekko/actor/testkit/typed/scaladsl/BehaviorTestKitSpec.scala
 
b/actor-testkit-typed/src/test/scala/org/apache/pekko/actor/testkit/typed/scaladsl/BehaviorTestKitSpec.scala
index 70ec5212c9..e4c45b0240 100644
--- 
a/actor-testkit-typed/src/test/scala/org/apache/pekko/actor/testkit/typed/scaladsl/BehaviorTestKitSpec.scala
+++ 
b/actor-testkit-typed/src/test/scala/org/apache/pekko/actor/testkit/typed/scaladsl/BehaviorTestKitSpec.scala
@@ -395,12 +395,11 @@ class BehaviorTestKitSpec extends AnyWordSpec with 
Matchers with LogCapturing {
   "BehaviorTestKit’s child actor support" must {
     "allow retrieving and killing" in {
       val testkit = BehaviorTestKit(Parent.init)
-      val i = TestInbox[ActorRef[String]]()
       val h = TestInbox[String]()
-      testkit.run(SpawnSession(i.ref, h.ref))
 
-      val sessionRef = i.receiveMessage()
-      i.hasMessages shouldBe false
+      val sessionRef =
+        testkit.runAsk[ActorRef[String]](SpawnSession(_, h.ref)).receiveReply()
+
       val s = testkit.expectEffectType[SpawnedAnonymous[_]]
       // must be able to get the created ref, even without explicit reply
       s.ref shouldBe sessionRef
@@ -409,10 +408,8 @@ class BehaviorTestKitSpec extends AnyWordSpec with 
Matchers with LogCapturing {
       session.run("hello")
       h.receiveAll() shouldBe Seq("hello")
 
-      val d = TestInbox[Done]()
-      testkit.run(KillSession(sessionRef, d.ref))
+      testkit.runAsk(KillSession(sessionRef, _)).expectReply(Done)
 
-      d.receiveAll() shouldBe Seq(Done)
       testkit.expectEffectType[Stopped]
     }
 
@@ -447,9 +444,9 @@ class BehaviorTestKitSpec extends AnyWordSpec with Matchers 
with LogCapturing {
   "timer support" must {
     "schedule and cancel timers" in {
       val testkit = BehaviorTestKit[Parent.Command](Parent.init)
-      val t = TestInbox[Boolean]()
-      testkit.run(IsTimerActive("abc", t.ref))
-      t.receiveMessage() shouldBe false
+
+      testkit.runAsk(IsTimerActive("abc", _)).expectReply(false)
+
       testkit.run(ScheduleCommand("abc", 42.seconds, 
Effect.TimerScheduled.SingleMode, SpawnChild))
       testkit.expectEffectPF {
         case Effect.TimerScheduled(
@@ -460,15 +457,16 @@ class BehaviorTestKitSpec extends AnyWordSpec with 
Matchers with LogCapturing {
               false /*not overriding*/ ) =>
           finiteDuration should equal(42.seconds)
       }
-      testkit.run(IsTimerActive("abc", t.ref))
-      t.receiveMessage() shouldBe true
+
+      testkit.runAsk(IsTimerActive("abc", _)).expectReply(true)
+
       testkit.run(CancelScheduleCommand("abc"))
       testkit.expectEffectPF {
         case Effect.TimerCancelled(key) =>
           key should equal("abc")
       }
-      testkit.run(IsTimerActive("abc", t.ref))
-      t.receiveMessage() shouldBe false
+
+      testkit.runAsk(IsTimerActive("abc", _)).expectReply(false)
     }
 
     "schedule and fire timers" in {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to