端到端测试Route
Akka HTTP
这里我们将使用 Akka HTTP来开发一个很简单的业务应用:组织管理。它只有一个数据模型,**Org**。组织支持树型结构,所有每个 org 里面都可以有一个可选 parent
属性来指向父组织,若没有则代表此 org 是个一级组织。Org 的数据模型如下:
case class Org(
id Int, // PK
code Option[String], // 组织编码,可选值。Unique index
name String,
contact: ObjectNode, // Json类型,使用Jackson
parent Option[String], // 父组织
parents List[String], // 父组织全路径
status: Int,
createdAt OffsetDateTime,
updatedAt Option[OffsetDateTime]
)
业务流程上,对一个 Org 模型的操作我们设计如下的简单流程:
OrgRoute -> OrgService -> OrgRepo
OrgRoute
是一个使用 Akka HTTP Routing DSL 来定义实现的路由(从MVC架构术语来说,就是控制器(Controller))。从这个简单的示例来说,它拥有如下接口:
- createRoute:创建 Org
- getRoute:根据id或code获取 Org
- pageRoute:分页查询
- updateRoute:更新 Org
- removeRoute:根据id删除 Org
现在,我们已经设计好了我们需要的5个接口(名字),接下来需要定义具体的接口和实现。这里,我们先从测试开始。
akka-http-testkit
Akka HTTP 提供了一个测试套件来简化对 Akka HTTP 和 Akka HTTP Routing DSL的测试,我们需要在 sbt 配置里加上对应的库依赖:
libraryDependencies += "com.typesafe.akka" %% "akka-http-testkit" % "2.5.14" % Test
定义一个 Akka HTTP Routing DSL 的测试类,需要混入 ScalatestRouteTest
特质,它提供了对 Route DSL 的一系列测试辅助函数来支持Scalatest。
OrgRouteTest
现在,我们从 OrgRouteTest 开始,通过 红-绿-红-绿这样的测试循环来验证并一步一步实现对 Org 的各项接口功能。
首先,让我们来看看这个 OrgRouteTest 类:
class OrgRouteTest
extends WordSpec
with BeforeAndAfterAll
with ScalatestRouteTest
with Matchers
with OptionValues
with ScalaFutures {
private val schema = new Schema()
private var orgIds: Set[Int] = Set()
private val orgService = new OrgService(schema)
private val route: Route = new OrgRoute(orgService).route
"OrgRoute" should {
import helloscala.http.JacksonSupport._
var org: Org = null
"create" in {
val req = OrgCreateReq(Some("000001"), "测试组织", None, None)
Post("/org/item", req) ~> route ~> check {
status shouldBe StatusCodes.Created
org = responseAs[Org]
orgIds += org.id
org.id should be > 0
org.parent shouldBe None
org.updatedAt shouldBe None
}
}
"get" in {
pending
}
"pageRoute" in {
pending
}
"updateRoute" in {
pending
}
"remoteRoute" in {
pending
}
}
private def cleanup(): Unit = try {
orgService.removeByIds(orgIds).futureValue
} catch {
case NonFatal(e) => e.printStackTrace()
}
override def afterAll() {
cleanup()
schema.db.close()
super.afterAll()
}
}
这里的**OrgRouteTest**测试类继续了多个接口:
- WordSpec:使用了**Word**风格的测试,必需要混入这样的一个测试规范接口。类似的有:**FutureSpec**、**FlatSpec**等
- BeforeAndAfterAll:提供了所有测试用例执行前或完成后的钩子函数
- ScalatestRouteTest:Akka HTTP Routing 测试的辅助函数
- Matchers:用户友好的断言DSL
- OptionValues:Option类型的辅助函数
- ScalaFutures:Future类型的辅助函数
这里可以看到定义了5个测试样例,其中 create 测试样例已经实现,其它4个暂未实现,使用 pending
函数占位。
在测试开始,定义了3个变量:
schema
:数据库连接管理orgIds
:测试生成的组织ID列表,待测试完成后可用来进行数据清除route
:要测试的路由
routing test dsl
在 create 测试用例中,使用 ~>
函数连接了 Post
(请求)、route
(路由)、check
(检测函数)三个部分。Post
定义了我们要发起的测试请求,通过 ~>
符号(函数)连接(发送)到route
(路由),然后再用 ~>
将响应连接到 check
函数来做检测(断言)。Post
实际是调用了 RequestBuilder,RequestBuilder
有多个重载函数,这里使用了需要一个ToEntityMarshaller[T]
隐式转换的函数,它可以将我们提交的OrgCreateReq
请求(case class)转换成合适的HTTP数据并设置匹配的Content-Type
请求头:
def apply[T](uri: String, content: T)(implicit m: ToEntityMarshaller[T], ec: ExecutionContext): HttpRequest =
apply(uri, Some(content))
我们使用import helloscala.http.JacksonSupport._
来导入Akka HTTP的JSON支持来将请求转换成application/json
类型的HTTP请求数据发送到route
。*Akka HTTP JSON支持见:JSON*
check
route
响应的结果将通过 check 函数来进行测试断言,通过ScalatestRouteTest
提供了多个辅助函数来完成测试,而对Akka HTTP routing的各类处理细节被隐藏在了check
函数的调用内部。常用的辅助函数有:
status
:获取响应的HTTP状态码response
:获取响应数据responseAs[T: FromResponseUnmarshaller]
:将响应数据(body)转换成T类型entityAs[T: FromEntityUnmarshaller]
:类似responseAs,但是将HttpEntity
转换成T类型contentType
:获取响应的Conent-Type- `mediaType:获取响应的MediaType
headers
:获取响应的所有header头header[T >: Null <: HttpHeader: ClassTag]
:查找指定类型的响应header头,返回结果为Option[T]
run test
在sbt console中执行命令运行测试:test/testOnly scalaweb.test.route.OrgRouteTest
,结果如下:
01:04:39.075 INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
01:04:39.238 INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
01:04:40.439 DEBUG slick.jdbc.JdbcBackend.statement - Preparing insert statement (returning: id,code,name,contact,parent,parents,status,created_at,updated_at): insert into "t_org" ("code","name","contact","parent","parents","status","created_at","updated_at") values (?,?,?,?,?,?,?,?)
01:04:40.468 DEBUG slick.jdbc.JdbcBackend.benchmark - Execution of prepared update took 4ms
01:04:41.410 DEBUG slick.jdbc.JdbcBackend.statement - Preparing statement: delete from "t_org" where "t_org"."id" in (5)
01:04:41.411 DEBUG slick.jdbc.JdbcBackend.benchmark - Execution of prepared update took 499µs
01:04:41.443 INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
01:04:41.448 INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
[info] OrgRouteTest:
[info] OrgRoute
[info] - should create
[info] - should get (pending)
[info] - should pageRoute (pending)
[info] - should updateRoute (pending)
[info] - should remoteRoute (pending)
[info] ScalaTest
[info] Run completed in 3 seconds, 382 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 4
[info] All tests passed.
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1, Pending 4
[success] Total time: 5 s, completed 2018-8-22 1:04:41
可以看到,这里执行通过了一个测试用例should create
,有4个测试用例为**pending**(代表还未实现)。
总结
使用 akka-http-testkit 可以在不启动Server的情况下对定义的route进行测试,可以显著的提高测试效率。