假设我有一个带有两个参数的动作的控制器。
它调用两个服务,每个参数一个,服务都返回字符串
每个字符串都作为参数传递给模板
结果传递给Ok并返回。
我想写一个简单的单元测试来确保: 1 - 使用正确的参数调用正确的服务 2 - 服务的返回值将传递给模板的正确属性
最好的方法是什么?
答案 0 :(得分:3)
使用Mockito和Specs2,我模拟服务来验证他们的方法调用。
我的控制器由Spring实例化。这使我可以将其视为class
而不是object
。 =>这对于使controller
可测试至关重要。这是一个例子:
@Controller
class MyController @Autowired()(val myServices: MyServices) extends Controller
要为控制器启用Spring,您必须定义一个Global
对象,作为Play!文档说明:
object Global extends GlobalSettings {
val context = new ClassPathXmlApplicationContext("application-context.xml")
override def getControllerInstance[A](controllerClass: Class[A]): A = {
context.getBean(controllerClass)
}
}
我的单元测试不需要Spring;我只是将协作者(模拟器)传递给构造函数。
但是,关于渲染模板,我只测试结果类型(Ok,BadRequest,Redirection等...)。 实际上,我注意到让我的测试扫描整个渲染模板的详细信息(发送给它的参数等等)并不容易,只进行单元测试。
因此,为了断言使用正确的参数调用正确的模板,我相信运行Selenium的验收测试,或者可能的功能测试,如果您愿意,可以扫描整个预期结果。
2 - 服务的返回值传递给正确的值 模板的属性
检查这个很容易......怎么样?通过信任编译器!更喜欢将一些自定义类型传递给模板而不是简单的基元,例如:
phone: String
将成为:phone: Phone
。 (一个简单的价值对象)。
因此,不必担心将非预期顺序的属性传递给模板(在单元测试或实际生产代码中)。编译器确实会发出警告。
以下是使用specs2进行单元测试(简化)的示例:
(您将注意到包装器的使用:WithFreshMocks
)。
这个case class
允许在测试后刷新所有变量(在这种情况下为模拟)测试。
这是重置模拟的好方法。
class MyControllerSpec extends Specification with Mockito {
def is =
"listAllCars should retrieve all cars" ! WithFreshMocks().listAllCarsShouldRetrieveAllCars
case class WithFreshMocks() {
val myServicesMock = mock[MyServices]
val myController = new MyController(myServicesMock)
def listAllCarsShouldRetrieveAllCars = {
val FakeGetRequest = FakeRequest() //fakeRequest needed by controller
mockListAllCarsAsReturningSomeCars()
val result = myController.listAllCars(FakeGetRequest).asInstanceOf[PlainResult] //passing fakeRequest to simulate a true request
assertOkResult(result).
and(there was one(myServicesMock).listAllCars()) //verify that there is one and only one call of listAllCars. If listAllCars would take any parameters that you expected to be called, you could have precise them.
}
private def mockListAllCarsAsReturningSomeCars() {
myServicesMock.listAllCars() returns List[Cars](Car("ferrari"), Car("porsche"))
}
private def assertOkResult(result: PlainResult) = result.header.status must_== 200
}
答案 1 :(得分:2)
所以,我想出了一个蛋糕模式和基于mockito的解决方案:
给予服务:
trait Service {
def indexMessage : String
}
trait ServiceImpl {
def indexMessage = {
"Hello world"
}
}
然后控制器看起来像:
object Application extends ApplicationController
with ServiceImpl {
def template = views.html.index.apply
}
trait ApplicationController extends Controller
with Service {
def template: (String) => play.api.templates.Html
def index = Action {
Ok(template("controller got:" + indexMessage))
}
}
测试看起来像:
class ApplicationControllerSpec extends Specification with Mockito {
"cake ApplicationController" should {
"return OK with the results of the service invocation" in {
val expectedMessage = "Test Message"
val m = mock[(String) => play.api.templates.Html]
object ApplicationControllerSpec extends ApplicationController {
def indexMessage = expectedMessage
def template = m
}
val response = ApplicationControllerSpec.index(FakeRequest())
status(response) must equalTo(OK)
there was one(m).apply(Matchers.eq("controller got:" + expectedMessage))
}
}
}
让Mockito工作我遇到了很多麻烦 它需要额外的依赖,我在scala中使用matchers时遇到了很多麻烦(我很乐意在java中使用它)
最终我认为上面的答案更好,避免使用String和其他原始类型,你可以将它们包装在特定于任务的类型中,然后你得到编译器警告。
此外,我通常会避免在控制器中执行“controller got:”前缀等操作。
在这种情况下就是这样,所以我可以验证它是否经过,在现实世界中应该由其他一些组件完成(控制器仅用于管道IMO)