Skip to content

Commit de80045

Browse files
authored
feat(scalasdk): Action testkit for Scala SDK (#583)
1 parent 2ccca1a commit de80045

18 files changed

Lines changed: 580 additions & 77 deletions

File tree

codegen/java-gen/src/main/scala/com/lightbend/akkasls/codegen/java/ActionTestKitGenerator.scala

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@ object ActionTestKitGenerator {
6767
"com.akkaserverless.javasdk.testkit.ActionResult",
6868
"com.akkaserverless.javasdk.testkit.impl.ActionResultImpl",
6969
"com.akkaserverless.javasdk.impl.action.ActionEffectImpl",
70-
"com.akkaserverless.javasdk.testkit.impl.StubActionCreationContext",
71-
"com.akkaserverless.javasdk.testkit.impl.StubActionContext")
70+
"com.akkaserverless.javasdk.testkit.impl.TestKitActionContext")
7271
++ commandStreamedTypes(service.commands))
7372

7473
val testKitClassName = s"${className}TestKit"
@@ -84,8 +83,8 @@ object ActionTestKitGenerator {
8483
| private Function<ActionCreationContext, $className> actionFactory;
8584
|
8685
| private $className createAction() {
87-
| $className action = actionFactory.apply(new StubActionCreationContext());
88-
| action._internalSetActionContext(Optional.of(new StubActionContext()));
86+
| $className action = actionFactory.apply(new TestKitActionContext());
87+
| action._internalSetActionContext(Optional.of(new TestKitActionContext()));
8988
| return action;
9089
| };
9190
|
@@ -151,11 +150,12 @@ object ActionTestKitGenerator {
151150
require(!service.commands.isEmpty, "empty `commands` not allowed")
152151

153152
service.commands
154-
.map { command =>
155-
s"""|public ${selectOutputResult(command)} ${lowerFirst(command.name)}(${selectInput(command)} ${lowerFirst(
156-
command.inputType.protoName)}) {
153+
.collect {
154+
case command if command.isUnary =>
155+
s"""|public ${selectOutputResult(command)} ${lowerFirst(command.name)}(${selectInput(command)} ${lowerFirst(
156+
command.inputType.protoName)}) {
157157
| ${selectOutputEffect(command)} effect = createAction().${lowerFirst(command.name)}(${lowerFirst(
158-
command.inputType.protoName)});
158+
command.inputType.protoName)});
159159
| return ${selectOutputReturn(command)}
160160
|}
161161
|""".stripMargin + "\n"
@@ -165,8 +165,9 @@ object ActionTestKitGenerator {
165165

166166
def generateTestingServices(service: ModelBuilder.ActionService): String = {
167167
service.commands
168-
.map { command =>
169-
s"""|@Test
168+
.collect {
169+
case command if command.isUnary =>
170+
s"""|@Test
170171
|public void ${lowerFirst(command.name)}Test() {
171172
| ${service.className}TestKit testKit = ${service.className}TestKit.of(${service.className}::new);
172173
| // ${selectOutputResult(command)} result = testKit.${lowerFirst(command.name)}(${selectInput(command)}.newBuilder()...build());

codegen/java-gen/src/test/scala/com/lightbend/akkasls/codegen/java/ActionTestKitGeneratorSuite.scala

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ class ActionTestKitGeneratorSuite extends munit.FunSuite {
5959
|import com.akkaserverless.javasdk.impl.action.ActionEffectImpl;
6060
|import com.akkaserverless.javasdk.testkit.ActionResult;
6161
|import com.akkaserverless.javasdk.testkit.impl.ActionResultImpl;
62-
|import com.akkaserverless.javasdk.testkit.impl.StubActionContext;
63-
|import com.akkaserverless.javasdk.testkit.impl.StubActionCreationContext;
62+
|import com.akkaserverless.javasdk.testkit.impl.TestKitActionContext;
6463
|import com.example.actions.CounterJournalToTopicAction;
6564
|import com.example.actions.CounterTopicApi;
6665
|import com.example.domain.CounterDomain;
@@ -80,8 +79,8 @@ class ActionTestKitGeneratorSuite extends munit.FunSuite {
8079
| private Function<ActionCreationContext, CounterJournalToTopicAction> actionFactory;
8180
|
8281
| private CounterJournalToTopicAction createAction() {
83-
| CounterJournalToTopicAction action = actionFactory.apply(new StubActionCreationContext());
84-
| action._internalSetActionContext(Optional.of(new StubActionContext()));
82+
| CounterJournalToTopicAction action = actionFactory.apply(new TestKitActionContext());
83+
| action._internalSetActionContext(Optional.of(new TestKitActionContext()));
8584
| return action;
8685
| };
8786
|
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2021 Lightbend Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.akkaserverless.codegen.scalasdk.impl
18+
19+
import com.akkaserverless.codegen.scalasdk.File
20+
import com.lightbend.akkasls.codegen.{ Format, ModelBuilder }
21+
22+
object ActionTestKitGenerator {
23+
24+
import com.lightbend.akkasls.codegen.SourceGeneratorUtils._
25+
26+
def generateUnmanagedTest(service: ModelBuilder.ActionService): Seq[File] =
27+
Seq(test(service))
28+
29+
def generateManagedTest(service: ModelBuilder.ActionService): Seq[File] =
30+
Seq(testkit(service))
31+
32+
private[codegen] def testkit(service: ModelBuilder.ActionService): File = {
33+
implicit val imports: Imports =
34+
generateImports(
35+
service.commands.map(_.inputType) ++
36+
service.commands.map(_.outputType),
37+
service.fqn.parent.scalaPackage,
38+
otherImports = Seq(
39+
"com.akkaserverless.scalasdk.testkit.ActionResult",
40+
"com.akkaserverless.scalasdk.testkit.impl.ActionResultImpl",
41+
"com.akkaserverless.scalasdk.action.ActionCreationContext",
42+
"com.akkaserverless.scalasdk.testkit.impl.TestKitActionContext"),
43+
semi = false)
44+
45+
val actionClassName = service.className
46+
47+
val methods = service.commands.collect {
48+
case cmd if cmd.isUnary =>
49+
s"""|def ${lowerFirst(cmd.name)}(command: ${typeName(cmd.inputType)}): ActionResult[${typeName(cmd.outputType)}] =
50+
| new ActionResultImpl(newActionInstance().${lowerFirst(cmd.name)}(command))
51+
|""".stripMargin
52+
}
53+
54+
File(
55+
service.fqn.parent.scalaPackage,
56+
s"${actionClassName}TestKit",
57+
s"""|package ${service.fqn.parent.scalaPackage}
58+
|
59+
|$imports
60+
|
61+
|$managedComment
62+
|
63+
|/**
64+
| * TestKit for unit testing $actionClassName
65+
| */
66+
|object ${actionClassName}TestKit {
67+
| /**
68+
| * Create a testkit instance of $actionClassName
69+
| * @param entityFactory A function that creates a $actionClassName based on the given ActionCreationContext
70+
| */
71+
| def apply(actionFactory: ActionCreationContext => $actionClassName): ${actionClassName}TestKit =
72+
| new ${actionClassName}TestKit(actionFactory)
73+
|
74+
|}
75+
|
76+
|/**
77+
| * TestKit for unit testing $actionClassName
78+
| */
79+
|final class ${actionClassName}TestKit private(actionFactory: ActionCreationContext => $actionClassName) {
80+
|
81+
| private def newActionInstance() = actionFactory(new TestKitActionContext)
82+
|
83+
| ${Format.indent(methods, 2)}
84+
|}
85+
|""".stripMargin)
86+
}
87+
88+
def test(service: ModelBuilder.ActionService): File = {
89+
90+
implicit val imports: Imports =
91+
generateImports(
92+
service.commands.map(_.inputType) ++ service.commands.map(_.outputType),
93+
service.fqn.parent.scalaPackage,
94+
otherImports = Seq(
95+
"com.akkaserverless.scalasdk.action.Action",
96+
"com.akkaserverless.scalasdk.testkit.ActionResult",
97+
"org.scalatest.matchers.should.Matchers",
98+
"org.scalatest.wordspec.AnyWordSpec"),
99+
semi = false)
100+
101+
val actionClassName = service.className
102+
103+
val testCases = service.commands.collect {
104+
case cmd if cmd.isUnary =>
105+
s"""|"handle command ${cmd.name}" in {
106+
| val testKit = ${actionClassName}TestKit(new $actionClassName(_))
107+
| // val result = testKit.${lowerFirst(cmd.name)}(${typeName(cmd.inputType)}(...))
108+
|}
109+
|""".stripMargin
110+
}
111+
112+
File(
113+
service.fqn.parent.scalaPackage,
114+
actionClassName + "Spec",
115+
s"""|package ${service.fqn.parent.scalaPackage}
116+
|
117+
|$imports
118+
|
119+
|$unmanagedComment
120+
|
121+
|class ${actionClassName}Spec
122+
| extends AnyWordSpec
123+
| with Matchers {
124+
|
125+
| "${actionClassName}" must {
126+
|
127+
| "have example test that can be removed" in {
128+
| val testKit = ${actionClassName}TestKit(new $actionClassName(_))
129+
| // use the testkit to execute a command
130+
| // and verify final updated state:
131+
| // val result = testKit.someOperation(SomeRequest)
132+
| // verify the response
133+
| // result.reply shouldBe expectedReply
134+
| }
135+
|
136+
| ${Format.indent(testCases, 4)}
137+
|
138+
| }
139+
|}
140+
|""".stripMargin)
141+
}
142+
143+
}

codegen/scala-gen/src/main/scala/com/akkaserverless/codegen/scalasdk/impl/SourceGenerator.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ object SourceGenerator {
6161
}
6262
case _: ModelBuilder.ViewService =>
6363
Nil
64-
case _: ModelBuilder.ActionService =>
65-
Nil
64+
case service: ModelBuilder.ActionService =>
65+
ActionTestKitGenerator.generateManagedTest(service)
6666
}.toList
6767
}
6868

@@ -106,7 +106,7 @@ object SourceGenerator {
106106
}
107107
case _: ModelBuilder.ViewService =>
108108
Nil
109-
case _: ModelBuilder.ActionService =>
110-
Nil
109+
case service: ModelBuilder.ActionService =>
110+
ActionTestKitGenerator.generateUnmanagedTest(service)
111111
}.toList
112112
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2021 Lightbend Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.akkaserverless.codegen.scalasdk
18+
19+
import com.akkaserverless.codegen.scalasdk.impl.ActionServiceSourceGenerator
20+
import com.akkaserverless.codegen.scalasdk.impl.ActionTestKitGenerator
21+
import com.lightbend.akkasls.codegen.TestData
22+
23+
class ActionTestKitGeneratorSuite extends munit.FunSuite {
24+
private val testData = TestData.scalaStyle
25+
test("it can generate a TestKit for an action") {
26+
val service = testData.simpleActionService()
27+
val generatedSrc =
28+
ActionTestKitGenerator.generateManagedTest(service).head.content
29+
30+
assertNoDiff(
31+
generatedSrc,
32+
"""package com.example.service
33+
|
34+
|import com.akkaserverless.scalasdk.action.ActionCreationContext
35+
|import com.akkaserverless.scalasdk.testkit.ActionResult
36+
|import com.akkaserverless.scalasdk.testkit.impl.ActionResultImpl
37+
|import com.akkaserverless.scalasdk.testkit.impl.TestKitActionContext
38+
|import com.external.Empty
39+
|
40+
|// This code is managed by Akka Serverless tooling.
41+
|// It will be re-generated to reflect any changes to your protobuf definitions.
42+
|// DO NOT EDIT
43+
|
44+
|/**
45+
| * TestKit for unit testing MyServiceAction
46+
| */
47+
|object MyServiceActionTestKit {
48+
| /**
49+
| * Create a testkit instance of MyServiceAction
50+
| * @param entityFactory A function that creates a MyServiceAction based on the given ActionCreationContext
51+
| */
52+
| def apply(actionFactory: ActionCreationContext => MyServiceAction): MyServiceActionTestKit =
53+
| new MyServiceActionTestKit(actionFactory)
54+
|
55+
|}
56+
|
57+
|/**
58+
| * TestKit for unit testing MyServiceAction
59+
| */
60+
|final class MyServiceActionTestKit private(actionFactory: ActionCreationContext => MyServiceAction) {
61+
|
62+
| private def newActionInstance() = actionFactory(new TestKitActionContext)
63+
|
64+
| def simpleMethod(command: MyRequest): ActionResult[Empty] =
65+
| new ActionResultImpl(newActionInstance().simpleMethod(command))
66+
|}""".stripMargin)
67+
}
68+
69+
test("it can generate a test stub for an action") {
70+
val service = testData.simpleActionService()
71+
val generatedSrc =
72+
ActionTestKitGenerator.generateUnmanagedTest(service).head.content
73+
74+
println(generatedSrc)
75+
assertNoDiff(
76+
generatedSrc,
77+
"""package com.example.service
78+
|
79+
|import com.akkaserverless.scalasdk.action.Action
80+
|import com.akkaserverless.scalasdk.testkit.ActionResult
81+
|import com.external.Empty
82+
|import org.scalatest.matchers.should.Matchers
83+
|import org.scalatest.wordspec.AnyWordSpec
84+
|
85+
|// This class was initially generated based on the .proto definition by Akka Serverless tooling.
86+
|//
87+
|// As long as this file exists it will not be overwritten: you can maintain it yourself,
88+
|// or delete it so it is regenerated as needed.
89+
|
90+
|class MyServiceActionSpec
91+
| extends AnyWordSpec
92+
| with Matchers {
93+
|
94+
| "MyServiceAction" must {
95+
|
96+
| "have example test that can be removed" in {
97+
| val testKit = MyServiceActionTestKit(new MyServiceAction(_))
98+
| // use the testkit to execute a command
99+
| // and verify final updated state:
100+
| // val result = testKit.someOperation(SomeRequest)
101+
| // verify the response
102+
| // result.reply shouldBe expectedReply
103+
| }
104+
|
105+
| "handle command SimpleMethod" in {
106+
| val testKit = MyServiceActionTestKit(new MyServiceAction(_))
107+
| // val result = testKit.simpleMethod(MyRequest(...))
108+
| }
109+
|
110+
| }
111+
|}""".stripMargin)
112+
}
113+
}

0 commit comments

Comments
 (0)