Play Framework Scala 测试指南用 GuiceApplicationBuilder 与 GuiceInjectorBuilder 配置测试中的依赖注入【免费下载链接】playframeworkThe Community Maintained High Velocity Web Framework For Java and Scala.项目地址: https://gitcode.com/gh_mirrors/pl/playframework本文以 Play Framework 官方文档 ScalaTestingWithGuice.md 为主体结合仓库中core/play-guice的源码实现与documentation/manual/working/scalaGuide/main/tests/code/tests/guice下的可运行测试样例系统讲解如何在 Scala 测试中直接配置依赖注入包括追加/覆盖绑定、禁用模块、替换 Environment 与 Configuration以及用 Mock 组件进行函数式测试的完整实战流程。读完本文你将掌握GuiceApplicationBuilder与GuiceInjectorBuilder的全部核心 API 及底层原理并能在自己的测试中灵活替换组件。如果你的应用使用 Guice 进行依赖注入那么你可以直接为测试配置组件的创建方式与应用的组装方式包括添加额外的绑定binding或覆盖已有的绑定。Play 为此提供了两个构建器 API面向完整Application的GuiceApplicationBuilder以及面向更一般化注入器的GuiceInjectorBuilder。本文的所有示例代码均来自仓库中的实测样例 ScalaGuiceApplicationBuilderSpec.scala它们是被持续运行的真实测试可直接对照验证。1. 前置Guice 模块与测试入口使用本文的 API 前需确认项目已引入 Play 的 Guice 模块Play 的 sbt 插件默认不捆绑任何 DI 框架libraryDependencies guice随后在测试代码中引入两个核心类对应文档中的builder-imports与bind-imports片段import play.api.inject.guice.GuiceApplicationBuilder import play.api.inject.bindbind是 Play 提供的轻量级绑定 DSL用于构造Binding[T]它与play.api.inject.Module一起构成Play Modules and Bindings 中介绍的框架无关绑定体系而GuiceApplicationBuilder则负责把这些绑定翻译成 Guice 模块并创建应用。从源码看翻译工作发生在 GuiceBuilder.createModule()val enabledModules modules.map(_.disable(disabled)) val bindingModules GuiceableModule.guiced(environment, configuration, binderOptions)(enabledModules) : injectorModule val overrideModules GuiceableModule.guiced(environment, configuration, binderOptions)(overrides) GuiceModules.override(bindingModules.asJava).with(overrideModules.asJava)可见最终结构是「基础模块 追加模块」作为主绑定overrides以 Guice 原生Modules.override(...).with(...)的方式叠加——这正是后文「覆盖绑定」生效的底层机制。2. GuiceApplicationBuilder为测试组装 ApplicationGuiceApplicationBuilder 提供了一套流畅的 builder API用于配置依赖注入并创建 Application。它的核心形态源码 GuiceApplicationBuilder.scala是一个不可变 case class持有environment、configuration、modules、overrides、disabled、binderOptions、eagerly、loadConfiguration、loadModules等字段每个xxx()方法都返回经过copy的新实例。最终build()的实现非常简洁GuiceApplicationBuilder.scaladef build(): Application injector().instanceOf[Application]即先由注入器解析出Application的绑定这依赖 Play 的BuiltinModule等内置模块声明的绑定其中包含由路由编译器生成、构造器注入了各控制器依赖的 Router再从注入器取出实例。2.1 配置 Environment环境Environment或其组成部分root path、mode、class loader都可以被指定。配置好的环境将用于加载应用配置、在加载模块时被传入并在从 Play 模块推导绑定以及向其他组件注入时被使用。一次性传入完整Environment对应文档set-environment片段val application new GuiceApplicationBuilder() .in(Environment(new File(path/to/app), classLoader, Mode.Test)) .build()也可以分步只设置其中某几项对应文档set-environment-values片段val application new GuiceApplicationBuilder() .in(new File(path/to/app)) .in(Mode.Test) .in(classLoader) .build()对应的底层实现是 GuiceBuilder 中三个重载的in(...)方法它们分别替换整个Environment或只修改rootPath、mode、classLoader字段final def in(env: Environment): Self copyBuilder(environment env) final def in(path: File): Self copyBuilder(environment environment.copy(rootPath path)) final def in(mode: Mode): Self copyBuilder(environment environment.copy(mode mode)) final def in(classLoader: ClassLoader): Self copyBuilder(environment environment.copy(classLoader classLoader))实测用例ScalaGuiceApplicationBuilderSpec.scala随后断言application.path、application.mode与application.classloader均与设置值一致验证了环境确实被完整传递。2.2 追加 Configuration配置可以为应用追加额外的配置。这些配置总是叠加在自动加载的配置之上当出现相同 key 时追加的配置优先对应文档add-configuration片段val application new GuiceApplicationBuilder() .configure(Configuration(a - 1)) .configure(Map(b - 2, c - three)) .configure(d - 4, e - five) .build()configure有三个重载GuiceInjectorBuilder.scala分别接受Configuration、Map[String, Any]和可变参数键值对其内部统一为final def configure(conf: Configuration): Self copyBuilder(configuration conf.withFallback(configuration))关键点在于conf.withFallback(configuration)在 HOCON 语义下this优先因此本次调用传入的配置优先级更高。而在 applicationModule() 中最终配置又经过一层configuration.withFallback(initialConfiguration)即 builder 中追加的配置始终压过从环境自动加载的配置val initialConfiguration loadConfiguration(environment) val appConfiguration configuration.withFallback(initialConfiguration)这一机制非常适合测试场景例如用inMemoryDatabase(test)替换真实数据库、关闭过滤器等见函数式测试指南中的 appWithMemoryDatabase 示例。2.3 完全替换配置加载方式除了追加还可以整体替换「从环境自动加载配置」的行为这将完全取代应用配置对应文档override-configuration片段val application new GuiceApplicationBuilder() .loadConfig(env Configuration.load(env)) .build()loadConfig同样有两个重载GuiceApplicationBuilder.scaladef loadConfig(loader: Environment Configuration): GuiceApplicationBuilder copy(loadConfiguration loader) def loadConfig(conf: Configuration): GuiceApplicationBuilder loadConfig(env conf)默认的loadConfiguration就是Configuration.load它会按 mode 从application.conf、reference.conf等位置加载配置测试中你可以传入自定义函数甚至直接给一个固定的Configuration实例。注意此时 builder 中通过.configure(...)追加的配置仍然会叠加生效。3. 绑定与模块Bindings and Modules用于依赖注入的绑定是完全可配置的。builder 方法同时支持 Play Modules and Bindings 与原生 Guice Module。3.1 追加绑定Additional bindings可以通过 Play 模块、Play 绑定或 Guice 模块追加绑定对应文档add-bindings片段val injector new GuiceApplicationBuilder() .bindings(new ComponentModule) // 追加一个 Play 模块 .bindings(bind[Component].to[DefaultComponent]) // 追加一条 Play 绑定 .injector()bindings(...)接受可变参数的GuiceableModuleGuiceInjectorBuilder.scalafinal def bindings(bindModules: GuiceableModule*): Self copyBuilder(modules modules bindModules)GuiceableModule是一个磁吸magnet类型GuiceableModule通过隐式转换统一接受三种输入play.api.inject.ModulePlay 模块、com.google.inject.ModuleGuice 模块、Binding[T]Play 绑定。从 GuiceableModuleConversions 可以看到Play 绑定最终被翻译成com.google.inject.AbstractModule支持 Provider 目标、实现类目标、作用域scope与 eager 声明。3.2 覆盖绑定Override bindings可以使用 Play 绑定或能提供绑定的模块来覆盖已有绑定对应文档override-bindings片段val application new GuiceApplicationBuilder() .bindings(new ComponentModule) // 基础绑定Component - DefaultComponent .overrides(bind[Component].to[MockComponent]) // 覆盖为 Mock 实现 .build()overrides(...)的实现GuiceInjectorBuilder.scala与bindings类似只是进入独立的overrides列表最终通过前文提到的GuiceModules.override(...).with(...)生效。实测用例ScalaGuiceApplicationBuilderSpec.scala验证了覆盖后的行为running(application) { val Some(result) route(application, FakeRequest(GET, /)) contentAsString(result) must_ mock }3.3 禁用模块Disable modules任何已加载的模块都可以按类名禁用对应文档disable-modules片段val injector new GuiceApplicationBuilder() .bindings(new ComponentModule) .disable[ComponentModule] // 按类型禁用 .injector()disable提供两个重载GuiceInjectorBuilder.scala第二个是类型安全的泛型版本final def disable(moduleClasses: Class[?]*): Self copyBuilder(disabled disabled moduleClasses) final def disableT: Self disable(tag.runtimeClass)实测用例验证禁用ComponentModule后再从注入器获取Component会抛出com.google.inject.ConfigurationException因为没有其他绑定能提供该组件。从 GuiceableModuleConversions.filterOut 看禁用是运行时通过isAssignableFrom匹配实例类完成的。3.4 自定义模块加载Loaded modules默认情况下模块会根据play.modules.enabled配置从 classpath 自动加载。这种默认加载行为可以整体覆盖对应文档load-modules片段val injector new GuiceApplicationBuilder() .load( new play.api.inject.BuiltinModule, new play.api.i18n.I18nModule, new play.api.mvc.CookiesModule, bind[Component].to[DefaultComponent] ) .injector()load同样有两个重载GuiceApplicationBuilder.scaladef load(loader: (Environment, Configuration) Seq[GuiceableModule]): GuiceApplicationBuilder copy(loadModules loader) def load(modules: GuiceableModule*): GuiceApplicationBuilder load((env, conf) modules)默认的loadModules是GuiceableModule.loadModulesGuiceInjectorBuilder.scala它调用Modules.locate(environment, configuration)按play.modules.enabled定位并实例化模块。若使用load(...)显式传入模块列表则不再自动定位——这也解释了为什么上述示例必须把BuiltinModule、I18nModule、CookiesModule等基础设施模块一并列出否则应用将缺少核心绑定。若只想在自动加载之外调整个别模块更常见的选择是结合 3.1 的bindings(...)与 3.3 的disable(...)。3.5 路由相关便捷方法源码补充除文档主线的绑定配置外GuiceApplicationBuilder还提供一组针对路由的便捷方法GuiceApplicationBuilder.scala在测试中伪造路由非常实用// 用一段 PartialFunction 作为主路由未命中的请求回退到默认 Router def routes(routesFunc: PartialFunction[(String, String), Handler]): GuiceApplicationBuilder // 直接覆盖 Router 绑定 def router(router: Router): GuiceApplicationBuilder // 先尝试附加 Router失败后回退到默认 Router def additionalRouter(router: Router): GuiceApplicationBuilder其中routes(...)的实现依赖FakeRouterConfigFakeRouterProvider与AdditionalRouterProvider同文件 L253-L282本质上也是通过overrides覆盖Router的绑定。函数式测试指南中有现成用法ScalaFunctionalTestSpec.scalaval applicationWithRouter GuiceApplicationBuilder() .appRoutes { app val Action app.injector.instanceOf[DefaultActionBuilder] ({ case (GET, /Bob) Action { Ok(Hello Bob).as(text/html; charsetutf-8) } }) } .build()4. GuiceInjectorBuilder更一般化的纯净注入器GuiceInjectorBuilder 提供了更通用的 Guice 依赖注入配置。与GuiceApplicationBuilder不同它不会从环境自动加载配置或模块而是提供一个完全干净的状态让你自行添加配置与绑定。两者的公共接口定义在基类GuiceBuilder中见 GuiceInjectorBuilder.scala最终创建一个 Play 的 Injector。下面是用注入器构建器实例化一个组件的示例对应文档injector-imports、bind-imports、injector-builder片段import play.api.inject.guice.GuiceInjectorBuilder import play.api.inject.bind val injector new GuiceInjectorBuilder() .configure(key - value) .bindings(new ComponentModule) .overrides(bind[Component].to[MockComponent]) .injector() val component injector.instanceOf[Component]injector()的底层实现GuiceInjectorBuilder.scala值得注意——它根据环境模式决定 Guice 的Stageval stage environment.mode match { case Mode.Prod Stage.PRODUCTION case _ if eagerly Stage.PRODUCTION case _ Stage.DEVELOPMENT } val guiceInjector Guice.createInjector(stage, applicationModule())即在Prod模式或启用了 eager 加载时使用PRODUCTION阶段绑定在启动时严格校验、单例立即实例化否则使用DEVELOPMENT阶段。这解释了 Play 文档中「eager 绑定在 dev 与 prod 下初始化时机不同」的行为差异。4.1 Binder 选项与 eager 加载源码补充GuiceBuilder还暴露了几个底层 Binder 选项可让测试环境的注入语义更严格GuiceInjectorBuilder.scaladisableCircularProxies(disable true)禁止 Guice 通过代理接口来打破循环依赖。默认即为禁用BinderOption.defaults Set(DisableCircularProxies)见 BinderOption。可用disableCircularProxies(false)重新允许。requireExactBindingAnnotations(require true)要求注入点必须精确匹配绑定注解默认关闭。requireAtInjectOnConstructors(require true)要求构造器含默认构造器标注Inject默认关闭。requireExplicitBindings(require true)只注入模块中显式绑定的类默认关闭。eagerlyLoaded()将injector()阶段强制为PRODUCTION所有单例在注入器创建时立即初始化适合在测试中复现生产环境的启动行为。5. 实战在函数式测试中用 Mock 组件覆盖绑定下面是一个完整示例把一个组件替换成 Mock 实现来进行测试。这一场景正是依赖注入「针对同一组件绑定不同实现」动机的最佳体现。5.1 被测组件、模块与控制器首先定义一个组件接口包含默认实现和用于测试的 Mock 实现来自 Component.scalatrait Component { def hello: String } class DefaultComponent extends Component { def hello default } class MockComponent extends Component { def hello mock }该组件通过一个 Play 模块自动加载同上文件import play.api.inject.Binding import play.api.inject.Module import play.api.Configuration import play.api.Environment class ComponentModule extends Module { def bindings(env: Environment, conf: Configuration): Seq[Binding[?]] Seq( bind[Component].to[DefaultComponent] ) }组件被注入到一个控制器来自 controllers/Application.scalaimport jakarta.inject.Inject import play.api.mvc._ class Application Inject() (component: Component, cc: ControllerComponents) extends AbstractController(cc) { def index Action { Ok(component.hello) } }配套的路由文件 scalaguide.tests.guice.routes 只有一行GET / controllers.Application.index()5.2 在测试中覆盖绑定要为函数式测试构建Application只需覆盖组件的绑定对应文档override-bindings完整片段import play.api.inject.guice.GuiceApplicationBuilder import play.api.inject.bind val application new GuiceApplicationBuilder() .bindings(new ComponentModule) // 加载真实的模块绑定 DefaultComponent .overrides(bind[Component].to[MockComponent]) // 但把 Component 覆盖为 Mock .build()由于ComponentModule声明的是Component - DefaultComponent而overrides通过GuiceModules.override(...).with(...)叠加了Component - MockComponent最终注入器解析Component时得到的是MockComponent。实测断言ScalaGuiceApplicationBuilderSpec.scala确认访问GET /返回的响应体是mock。5.3 与函数式测试框架结合上面创建的application可以直接配合函数式测试辅助类使用基于 Specs2 的函数式测试指南通过WithApplication(application)、WithServer、WithBrowser等Around块运行测试使用route(app, FakeRequest(GET, /))发起请求、contentAsString/status等Helpers断言结果ScalaTest 生态GuiceApplicationBuilder产出的Application同样可用于 ScalaTest 的函数式测试写法也可以直接调用application.injector.instanceOf[SomeService]配合Injectingtrait在测试中获取任意被注入的组件进行断言。6. 小结与延伸阅读GuiceApplicationBuilder面向完整Application的构建器支持 Environment / Configuration / 模块 / 绑定 / 路由的全方位配置build()底层是injector().instanceOf[Application]。GuiceInjectorBuilder纯净状态下的注入器构建器不自动加载配置与模块build()直接返回PlayInjector。两者共用基类 GuiceBuilderin(env/path/mode/classLoader)、configure(...)、bindings(...)、overrides(...)、disable(...)、injector()、eagerlyLoaded()与 Binder 选项方法全部在此定义。测试中的典型套路加载真实模块 覆盖 Mock 绑定 追加测试专用配置然后交给函数式测试框架运行。如果想要进一步了解测试的整体框架与其余辅助设施WithApplication、WithServer、WithBrowser、PlaySpecification等请阅读 ScalaTestingYourApplication 与 ScalaTestingWithSpecs2若需深入了解绑定 DSL、Play 模块体系与GuiceApplicationLoader的自定义方式可回到依赖注入指南。【免费下载链接】playframeworkThe Community Maintained High Velocity Web Framework For Java and Scala.项目地址: https://gitcode.com/gh_mirrors/pl/playframework创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考