4+1视图模型与c4模型
在上一篇文章中,我介绍了关于(可视)组件的思考,并使用任务浏览器作为用户界面“组件”的示例。
我解释说
- 查看模型,例如保存相关数据的普通Groovy对象(POGO),例如
class TaskBrowser
- 标记库(
LayoutTagLib
)和标记(def taskBrowser
)以将关联HTML(views/layouts/components/_taskBrowser.gsp
)呈现到页面
允许更多可维护和可测试的代码。
让我们把钱花在嘴边, 看看如何测试用过的标签库 。
零件
因此,这些是方程式中的(简化的)部分。
任务 –域类
class Task {
enum Type { PERSONAL, WORK }
String title
Type type
}
TaskBrowser –只是一个带有数据的POGO
class TaskBrowser {
List tasks = []
/**
* Month to start with.
*
* @return number between 1 and 12
*/
int getStartMonth() {
def nowDate = new Date()
nowDate[Calendar.MONTH] + 1
}
}
HomeController –在index
操作中创建任务浏览器。
class HomeController {
def taskService
def index() {
[taskBrowser: new TaskBrowser(tasks: taskService.retrieveTasks())]
}
}
home / index.gsp – index
操作的GSP
<!doctype html>
<html>
<head>
<meta name="layout" content="main" />
<title>Tasks</title>
</head>
<body>
<g:taskBrowser taskBrowser="${taskBrowser}"/>
</body>
</html>
views / layouts / components / _taskBrowser.gsp –任务浏览器HTML
<div class="row month-${startMonth}" id="task-browser">
<div class="six columns">
<g:if test="${tasks}">
<%-- 500 lines more... --%>
LayoutTagLib –最后,标签库
/**
* Renders the task browser.
*
* @attr taskBrowser REQUIRED a task browser instance
* @attr filter Optionally a {@link Task.Type} to show only those tasks
*/
def taskBrowser = { attrs ->
if (!attrs.taskBrowser) {
throwTagError("Tag [taskBrowser] is missing " +
"required attribute [taskBrowser]")
}
TaskBrowser browser = attrs.taskBrowser
// filter tasks by type
def tasks = browser.tasks
if (attrs.filter && attrs.filter instanceof Task.Type) {
tasks = browser.tasks.findAll { task -> task.type == attrs.filter }
}
out << render(template: '/layouts/components/taskBrowser',
model: [tasks : tasks,
months : browser.months,
startMonth : browser.startMonth
])
}
单元测试
如果您查看标记库代码,那么有一些有趣的事情足以涵盖单元测试。
通常,每当您使用Grails命令创建控制器,服务或标签库时,也会创建关联的单元测试。 如果没有,您以后可以随时创建一个
grails create-unit-test LayoutTagLib
无论如何,我们从LayoutTagLibSpec
开始,它最初是很空的。
import grails.test.mixin.TestFor
import spock.lang.Specification
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
def setup() {
}
def cleanup() {
}
void "test something"() {
expect:"fix me"
true == false
}
}
@TestFor
批注是Grails框架的一部分。 它不仅指示被测类 (也就是我们应该在此处测试的实际单元),而且还为我们提供了该类的具体实例。
以后再说。
现在我们可以执行我们的第一个测试,称为…
任务浏览器标签应默认显示所有任务
尽管最基本的测试方法"test something"
以“ test…”开始,但我尝试省略该部分。 我们显然是在创建测试,并且在前面重复“ test xxx”没有其他价值,但会占用空间。
如果我们正在TaskBrowser的单元测试中(例如TaskBrowserSpec
),我将从测试方法中跳过被测类的名称,例如“ 任务浏览器标签 应该显示……”。 因为我们使用的是更通用的LayoutTagLib
所以我想知道讨论的是哪个标签-当然还有更多,所以我还是从“任务浏览器标签…”开始
我通常首先在测试方法中放置“ Given / When / Then Spock”标签。 这有助于我思考自己的头脑
- 有哪些先决条件? (给)
- 实际调用的代码是什么? (什么时候)
- 有什么要断言或验证的? (然后)
这是我现在拥有的:
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
void "task browser tag should show all tasks by default"() {
given:
when:
then:
}
}
标签的实际调用在When下。 由于@TestFor
批注指向标记库类,因此我们可以使用隐式的tagLib变量来工作,该变量每次都引用一个干净的LayoutTagLib
实例。
不要在单元测试中做到这一点,因为这是前 - TestFor
方式:
def layoutTagLib = new LayoutTagLib()
// or <span class="pl-k">def</span> layoutTagLib <span class="pl-k">=</span> applicationContext<span class="pl-k">.</span>getBean(Layout<span class="pl-k">TagLib</span>)
layoutTagLib.taskBrowser(...)
但请使用tagLib
,卢克。
tagLib.taskBrowser(...)
所以我们有这个:
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
void "task browser tag should show all tasks by default"() {
given:
when:
tagLib.taskBrowser()
then:
}
}
我知道此测试的快乐路径流程需要一个TaskBrowser
实例。 默认情况下,应该至少有一项任务来验证标签是否应显示该标签。 因此,让我们添加它们:
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
void "task browser tag should show all tasks by default"() {
given:
def task = new Task(title: "My task")
def browser = new TaskBrowser(tasks: [task])
when:
tagLib.taskBrowser(taskBrowser: browser)
then:
true
}
}
嘿,在那么块中为什么是true
? 这是因为我们需要在When之后有一个Then块,以便此时能够执行一次此测试。 通常我们会用Expect作为
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
void "task browser tag should show all tasks by default"() {
given:
expect:
}
}
但我太懒了,无法将其更新为Expect并稍后将其更改回When / Then &#55357;&#56841; 我们需要再后来就无妨。 Grails在标签库的单元测试中所做的实际上是使用提供的模型来渲染模板/layouts/components/_taskBrowser.gsp 。
还记得LayoutTagLib
的代码吗?
/**
* Renders the task browser.
*
* @attr taskBrowser REQUIRED a task browser instance
* @attr filter Optionally a {@link Task.Type} to show only those tasks
*/
def taskBrowser = { attrs ->
if (!attrs.taskBrowser) {
throwTagError("Tag [taskBrowser] is missing " +
"required attribute [taskBrowser]")
}
TaskBrowser browser = attrs.taskBrowser
// filter tasks by type
def tasks = browser.tasks
if (attrs.filter && attrs.filter instanceof Task.Type) {
tasks = browser.tasks.findAll { task -> task.type == attrs.filter }
}
out << render(template: '/layouts/components/taskBrowser',
model: [tasks : tasks,
months : browser.months,
startMonth : browser.startMonth
])
}
如果上述(简单化的)测试成功,则_taskBrowser.gsp
及其逻辑未因异常而失败。 您可以在模板中输入错误,然后查看评估失败。 仅涵盖GSP的评估可能值得测试,但是我们没有检查任何内容。
我们如何知道是否引用了正确的模板? 我们如何知道是否正确传递了正确的模型?
不可避免的真相
如果查看Grails文档的“ 测试”一章 ,您将看到一个测试SimpleTagLib
响应的简单示例。
class SimpleTagLib {
static namespace = 's'
def hello = { attrs, body ->
out << "Hello ${attrs.name ?: 'World'}"
}
@TestFor(SimpleTagLib)
class SimpleTagLibSpec extends Specification {
void "test tag calls"() {
expect:
tagLib.hello().toString() == 'Hello World'
我们的标签并不像返回“ Hello World”那样简单-我们的标签将500行复杂任务浏览器HTML呈现到输出缓冲区。 验证这种简单方法并不是那么简单。
这里有几种方法。
#1 –检查零件
通常, contains
用于代码段中,只需检查项目是否存在。 我们可以尝试验证输出中某处是否存在单个任务“我的任务”,如下所示:
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
void "task browser tag should show all tasks by default"() {
given:
def task = new Task(title: "My task")
def browser = new TaskBrowser(tasks: [task])
when:
def result = tagLib.taskBrowser(taskBrowser: browser).toString()
then:
result.contains "My task"
}
}
我们已经成功验证了“我的任务”在500行输出中可见。
(请注意,在继续之前,请Swift加强测试–确保我们始终至少有多个测试项目来检查处理集合的逻辑,而不是单个项目)
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
void "task browser tag should show all tasks by default"() {
given:
def task1 = new Task(title: "My 1st task")
def task2 = new Task(title: "My 2nd task")
def browser = new TaskBrowser(tasks: [task1, task2])
when:
def result = tagLib.taskBrowser(taskBrowser: browser).toString()
then:
result.contains "My 1st task"
result.contains "My 2nd task"
}
}
缺点是我们将标记库逻辑 (显示或过滤任务)的测试与HTML的呈现 (任务标题的存在)结合在一起
为了减轻这种情况,我们应该……
#2 –控制要渲染的部分
正如控制器的测试中,我们可以使用的功能ControllerUnitTestMixin
嘲笑用于渲染视图 。
使用隐式的getViews()
或getGroovyPages()
,它们返回一个Map
供我们操作。 用我们自己的自定义内容覆盖实际模板,在其中我们控制模型的呈现方式和呈现方式。
首先,通过使测试失败,确保我们实际上覆盖了正确的模板路径。 taskBrowser
标记表示render(template: '/layouts/components/taskBrowser'...
因此我们必须将替代内容放在键'/layouts/components/_taskBrowser.gsp'
—编写模板路径的格式存在差异。
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
void "task browser tag should show all tasks by default"() {
given:
def task1 = new Task(title: "My 1st task")
def task2 = new Task(title: "My 2nd task")
def browser = new TaskBrowser(tasks: [task1, task2])
when:
views['/layouts/components/_taskBrowser.gsp'] = 'bogus'
def result = tagLib.taskBrowser(taskBrowser: browser).toString()
then:
result.contains "My 1st task"
result.contains "My 2nd task"
}
}
这正确地失败了…
Condition not satisfied:
result.contains "My 1st task"
| |
bogus false
…所以我们知道我们拥有正确的钥匙。
现在选择适当的内容。
只需打印任务集合即可为我们提供所需的一切,以验证现在我们的2个任务已传递给模型到我们的自定义模板。
Condition not satisfied:
result.contains "My 1st task"
| |
| false
[sample.Task : (unsaved), sample.Task : (unsaved)]
ang!
我们可以(也不应该)不依赖于Task类(我们的模型)的String
表示形式。
不要添加toString()
方法! 我们可以调整测试,以通过唯一属性(例如标题)来检测是否存在正确的项目 。
@TestFor(LayoutTagLib)
class LayoutTagLibSpec extends Specification {
void "task browser tag should show all tasks by default"() {
given:
def task1 = new Task(title: "My 1st task")
def task2 = new Task(title: "My 2nd task")
def browser = new TaskBrowser(tasks: [task1, task2])
when:
views['/layouts/components/_taskBrowser.gsp'] = '${tasks.title}'
// make sure only titles are rendered e.g. [My 1st task, My 2nd task]
def result = tagLib.taskBrowser(taskBrowser: browser).toString()
then:
result.contains "My 1st task"
result.contains "My 2nd task"
}
}
这样成功了!
而且很丑。 我们仍然依赖于将渲染结果( StreamCharBuffer
) StreamCharBuffer
为String
,需要将其整体与我们期望的内容进行比较或检查是否有部分内容。 对于测试一些小HTML代码段,这很好。
我将在以后的文章中分享一些技巧,因为该模型变得太复杂了,无法通过这种机制进行测试。 现在,我们将使用这种机制。
冲洗并重复
我经常将第一个简单的测试作为进一步测试的基础。 在第二项测试中,我们需要验证任务浏览器是否实际上可以针对诸如Personal或Work之类的任务类型进行一次过滤。
通过将filter
属性传递给标签来引入过滤filter
。 生成的测试可能如下所示。 我们正在检查的是仅呈现具有该类型的一项任务,而不呈现另一项。
void "task browser tag should show only personal tasks"() {
given:
def task1 = new Task(title: "My 1st task", type: Type.PERSONAL)
def task2 = new Task(title: "My 2nd task", type: Type.WORK)
def browser = new TaskBrowser(tasks: [task1, task2])
and:
def filterType = Type.PERSONAL
when:
views['/layouts/components/_taskBrowser.gsp'] = '${tasks.title}'
def result = tagLib.taskBrowser(taskBrowser: browser,
filter: filterType).toString()
then:
result.contains "My 1st task"
!result.contains("My 2nd task")
}
(通常,我会给given:
, when:
等块提供各种标签,但我将其留给读者练习。)
如果我不厌倦与罗马人和角斗士度过一个周末,我会写一些更多的变化和结尾部分,但是我希望只是在Ted Vinke Blog的上方写下对某些人有帮助。
测试愉快!
翻译自: https://www.javacodegeeks.com/2016/06/youre-using-tag-libraries-view-models-test-right.html
4+1视图模型与c4模型