菜谱视图

我们执行 mix phx.gen.html 命令时,它会生成如下文件:

  • a schema in web/models

  • a view in web/views

  • a controller in web/controllers

  • a migration file for the repository

  • default CRUD templates in web/templates

  • test files for generated model and controller

其中有两个测试文件,但是没有视图的测试文件 - 为什么?难道视图不重要?不不不,只要是代码,都会有测试的必要 - 有时没写,只是一个优先级或是投入产出比上的考虑。那如何入手?

查看 test/views 目录,现在已经有三个文件。我们可以借鉴其中的 error_view_test.exs 文件。

首先在 test/views 目录下新建一个 recipe_view_test.exs 文件,然后准备好如下内容:

defmodule TvRecipeWeb.RecipeViewTest do
  use TvRecipeWeb.ConnCase, async: true

  # Bring render/3 and render_to_string/3 for testing custom views
  import Phoenix.View

end

前面章节里我们曾经提到过,templates 文件会被编译成 view 模块下的函数。比如我们的 web/templates/recipe/index.html.eex 文件,最终会变成 TvRecipe.RecipeView 模块中的一个函数:

def render("index.html", assigns) do
  # 返回编译后的 eex 模板
end

换句话说,我们其实可以在 TvRecipe.RecipeView 中定义 render("index.html", assigns),而不必再写一个 index.html.eex 模板。只是模板对我们的开发更为友好,所以才从 View 中分离出来。

因此,测试模板即测试 View 中的函数。

那么,测试什么?测试我们的期望与事实是否相符。

recipe/index.html.eex 模板说,我们想在模板页面上显示哪些数据?Phoenix 最后的输出是否确保显示了?

来加一个测试:

diff --git a/test/views/recipe_view_test.exs b/test/views/recipe_view_test.exs
index be4148a..8174c14 100644
--- a/test/views/recipe_view_test.exs
+++ b/test/views/recipe_view_test.exs
@@ -4,4 +4,28 @@ defmodule TvRecipe.RecipeViewTest do
   # Bring render/3 and render_to_string/3 for testing custom views
   import Phoenix.View

+  alias TvRecipe.Recipes.Recipe
+  @recipe1 %{id: "1", name: "淘米", title: "侠饭", season: "1", episode: "1", content: "洗掉米表面的淀粉", user_id: "999"}
+  @recipe2 %{id: "2", name: "煮饭", title: "侠饭", season: "1", episode: "1", content: "浸泡", user_id: "888"}
+
+  test "render index.html", %{conn: conn} do
+    recipes = [struct(Recipe, @recipe1), struct(Recipe, @recipe2)]
+    content = render_to_string(TvRecipeWeb.RecipeView, "index.html", conn: conn, recipes: recipes)
+    # 页面上包含标题 Listing recipes
+    assert String.contains?(content, "Listing Recipes")
+    for recipe <- recipes do
+      # 页面上包含菜谱名
+      assert String.contains?(content, recipe.name)
+      # 页面上包含节目名
+      assert String.contains?(content, recipe.title)
+      # 包含 season
+      assert String.contains?(content, recipe.season)
+      # 包含 episode
+      assert String.contains?(content, recipe.episode)
+      # 不包含所有者 id
+      refute String.contains?(content, recipe.user_id)
+      # 因为 content 很长,我们不在 index.html 里显示
+      refute String.contains?(content, recipe.content)
+    end
+  end
+
 end

你可能要问,测试里的 season 为什么是 "1" 而不是 1,要知道我们给它定义的类型是 integer

是的,我们本应该把它写为 1,但那样的话,我们的测试代码里就要做类型转换:

assert String.contains?(content, Integer.to_string(recipe.season))

为了省事,我们就直接把 1 写成了 "1" - 这并不会有问题,因为 Phoenix 在编译模板时,也会做类型转换

iex> EEx.eval_string "foo <%= bar %>", [bar: 123]
"foo 123"

运行测试:

$ mix test
Compiling 1 file (.ex)
...

  1) test render index.html (TvRecipe.RecipeViewTest)
     test/views/recipe_view_test.exs:9
     Expected false or nil, got true
     code: String.contains?(content, recipe.user_id())
     stacktrace:
       test/views/recipe_view_test.exs:25: anonymous fn/3 in TvRecipe.RecipeViewTest.test render index.html/1
       (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
       test/views/recipe_view_test.exs:15: (test)

....................................................

Finished in 0.8 seconds
58 tests, 1 failure

一个错误发生,因为目前的 index.html.eex 中包含了 contentuser_id 的内容。

我们调整下 index.html.eex 文件:

diff --git a/web/templates/recipe/index.html.eex b/web/templates/recipe/index.html.eex
index b6ff40b..1dc8a3a 100644
--- a/web/templates/recipe/index.html.eex
+++ b/web/templates/recipe/index.html.eex
@@ -7,8 +7,6 @@
       <th>Title</th>
       <th>Season</th>
       <th>Episode</th>
-      <th>Content</th>
-      <th>User</th>

       <th></th>
     </tr>
@@ -20,8 +18,6 @@
       <td><%= recipe.title %></td>
       <td><%= recipe.season %></td>
       <td><%= recipe.episode %></td>
-      <td><%= recipe.content %></td>
-      <td><%= recipe.user_id %></td>

       <td class="text-right">
         <%= link "Show", to: recipe_path(@conn, :show, recipe), class: "btn btn-default btn-xs" %>

再运行测试:

mix test
........................................................

Finished in 0.8 seconds
58 tests, 0 failures

悉数通过。

我们知道,index.html.eex 页面上有一个 New recipe 的按钮,那我们的测试里是否需要体现?ShowEditDelete 这些按钮呢?要不要给它们写测试?

测试测什么,测到怎么的粒度,我觉得没有标准答案,更多时候要根据项目情况去权衡。但如果一定要有一个什么做为参考,我会选择设计稿。设计稿上定下来的元素,我们就尽量在测试里体现 - 否则设计人员很容易找上门来,说怎么少了这少了那。

在结束本节之前,别忘了在菜单栏上加上“菜谱”,不然我们就只能通过修改 url 访问菜谱相关页面了:

diff --git a/test/controllers/user_controller_test.exs b/test/controllers/user_controller_test.exs
index a1b75c6..7bd839c 100644
--- a/test/controllers/user_controller_test.exs
+++ b/test/controllers/user_controller_test.exs
@@ -29,6 +29,7 @@ defmodule TvRecipe.UserControllerTest do
     # 注册后自动登录,检查首页是否包含用户名
     conn = get conn, Routes.page_path(conn, :index)
     assert html_response(conn, 200) =~ Map.get(@valid_attrs, :username)
+    assert html_response(conn, 200) =~ "菜谱"
   end

   test "does not create resource and renders errors when data is invalid", %{conn: conn} do
diff --git a/web/templates/layout/app.html.eex b/web/templates/layout/app.html.eex
index b13f370..49240c9 100644
--- a/web/templates/layout/app.html.eex
+++ b/web/templates/layout/app.html.eex
@@ -19,6 +19,7 @@
             <li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
             <%= if @current_user do %>
               <li><%= link @current_user.username, to: Routes.user_path(@conn, :show, @current_user) %></li>
+              <li><%= link "菜谱", to: Routes.recipe_path(@conn, :index) %></li>
               <li><%= link "退出", to: Routes.session_path(@conn, :delete, @current_user), method: "delete" %></li>
             <% else %>
               <li><%= link "登录", to: Routes.session_path(@conn, :new) %></li>

运行测试:

$ mix test
..........................................................

Finished in 0.8 seconds
58 tests, 0 failures

上一章:Recipe 控制器 下一章:添加视频地址

最后更新于