文档章节

elixir官方教程Mix与OTP(八) 文档,测试与with

ljzn
 ljzn
发布于 2016/08/11 23:47
字数 3232
阅读 112
收藏 0
点赞 0
评论 0

#文档,测试与with

  1. 文档测试
  2. with
  3. 运行命令

本章,我们将实现能够解析我们在第一章中描述的命令的代码:

CREATE shopping
OK

PUT shopping milk 1
OK

PUT shopping eggs 3
OK

GET shopping milk
1
OK

DELETE shopping eggs
OK

解析完成后,我们将更新我们的服务器来调遣解析后的命令到kv应用中.

#文档测试(Doctests)

在语言主页,我们提到Elixir将文档当做语言中的一等公民.我们已经在本教程中多次探索了这个概念,通过mix help,或输入h Enum等其他模块在IEx控制台中.

本节,我们将使用文档测试来实现解析功能,它允许我们从文档中直接编写测试.这帮助我们给文档提供精确的代码样本.

让我们在lib/kv_server/command.ex中创建命令解析器,并以文档测试开头:

defmodule KVServer.Command do
  @doc ~S"""
  Parses the given `line` into a command.

  ## Examples

      iex> KVServer.Command.parse "CREATE shopping\r\n"
      {:ok, {:create, "shopping"}}

  """
  def parse(line) do
    :not_implemented
  end
end

文档测试是在文档字符串中定义的,通过四个空格的缩进之后跟着iex>语句来指定.如果一个命令跨越多行,你可以想在IEx中一样使用...>.预期的结果应该在iex>...>的下一行开始,并以新的行或新的iex>前缀作为结尾.

还要注意的是我们使用@doc ~S"""来开始文档字符串.~S能够避免\r\n字符被转化成回车和换行,直到它们在测试中被执行.

要运行我们的文档测试,我们会在test/kv_server/command_test.exs中创建一个文件,并在测试中调用doctest KVServer.Command:

defmodule KVServer.CommandTest do
  use ExUnit.Case, async: true
  doctest KVServer.Command
end

运行这套测试,文档测试将会失败:

1) test doc at KVServer.Command.parse/1 (1) (KVServer.CommandTest)
   test/kv_server/command_test.exs:3
   Doctest failed
   code: KVServer.Command.parse "CREATE shopping\r\n" === {:ok, {:create, "shopping"}}
   lhs:  :not_implemented
   stacktrace:
     lib/kv_server/command.ex:11: KVServer.Command (module)

很好!

现在只需要让文档测试通过就行了.让我们来实现parse/1函数:

def parse(line) do
  case String.split(line) do
    ["CREATE", bucket] -> {:ok, {:create, bucket}}
  end
end

我们的实现是简单地用空格拆分命令行,然后匹配列表中的命令.使用String.split/1意味着我们的命令将会是空格不敏感的,开头和结尾的空格是无关紧要的,单词间连续的空格也是一样.让我们添加一些新的文档测试,来测试其它命令:

@doc ~S"""
Parses the given `line` into a command.

## Examples

    iex> KVServer.Command.parse "CREATE shopping\r\n"
    {:ok, {:create, "shopping"}}

    iex> KVServer.Command.parse "CREATE  shopping  \r\n"
    {:ok, {:create, "shopping"}}

    iex> KVServer.Command.parse "PUT shopping milk 1\r\n"
    {:ok, {:put, "shopping", "milk", "1"}}

    iex> KVServer.Command.parse "GET shopping milk\r\n"
    {:ok, {:get, "shopping", "milk"}}

    iex> KVServer.Command.parse "DELETE shopping eggs\r\n"
    {:ok, {:delete, "shopping", "eggs"}}

Unknown commands or commands with the wrong number of
arguments return an error:

    iex> KVServer.Command.parse "UNKNOWN shopping eggs\r\n"
    {:error, :unknown_command}

    iex> KVServer.Command.parse "GET shopping\r\n"
    {:error, :unknown_command}

"""

现在轮到你来让测试通过!你完成之后,可以对比一下我们的解决方案:

def parse(line) do
  case String.split(line) do
    ["CREATE", bucket] -> {:ok, {:create, bucket}}
    ["GET", bucket, key] -> {:ok, {:get, bucket, key}}
    ["PUT", bucket, key, value] -> {:ok, {:put, bucket, key, value}}
    ["DELETE", bucket, key] -> {:ok, {:delete, bucket, key}}
    _ -> {:error, :unknown_command}
  end
end

注意我们是如何优雅地解析命令的,不需要添加一大堆 的if/else从句来检查命令名和参数数量!

最后,你可能会发现每个文档测试都被认为是不同的测试,因为我们这套测试最后报告了7个测试.这是因为ExUnit是这样辨认两个不同测试的定义的:

iex> KVServer.Command.parse "UNKNOWN shopping eggs\r\n"
{:error, :unknown_command}

iex> KVServer.Command.parse "GET shopping\r\n"
{:error, :unknown_command}

中间没有隔一行的话,ExUnit就会将其编译为一个测试:

iex> KVServer.Command.parse "UNKNOWN shopping eggs\r\n"
{:error, :unknown_command}
iex> KVServer.Command.parse "GET shopping\r\n"
{:error, :unknown_command}

你可以阅读ExUnit.DocTest文档来获取更多关于文档测试的内容.

#with

现在我们能够解析命令了,我们终于可以开始实现运行命令的逻辑了.让我们为这个函数添加一个存根定义:

defmodule KVServer.Command do
  @doc """
  Runs the given command.
  """
  def run(command) do
    {:ok, "OK\r\n"}
  end
end

在我们实现这个函数之前,让我们修改服务器,使其开始使用我们新的parse/1run/1函数.记住,我们的read_line/1函数会在客户端关闭套接字时崩溃,所以让我们也抓住机会修复它.打开lib/kv_server.ex:

defp serve(socket) do
  socket
  |> read_line()
  |> write_line(socket)

  serve(socket)
end

defp read_line(socket) do
  {:ok, data} = :gen_tcp.recv(socket, 0)
  data
end

defp write_line(line, socket) do
  :gen_tcp.send(socket, line)
end

替换成:

defp serve(socket) do
  msg =
    case read_line(socket) do
      {:ok, data} ->
        case KVServer.Command.parse(data) do
          {:ok, command} ->
            KVServer.Command.run(command)
          {:error, _} = err ->
            err
        end
      {:error, _} = err ->
        err
    end

  write_line(socket, msg)
  serve(socket)
end

defp read_line(socket) do
  :gen_tcp.recv(socket, 0)
end

defp write_line(socket, {:ok, text}) do
  :gen_tcp.send(socket, text)
end

defp write_line(socket, {:error, :unknown_command}) do
  # Known error. Write to the client.
  :gen_tcp.send(socket, "UNKNOWN COMMAND\r\n")
end

defp write_line(_socket, {:error, :closed}) do
  # The connection was closed, exit politely.
  exit(:shutdown)
end

defp write_line(socket, {:error, error}) do
  # Unknown error. Write to the client and exit.
  :gen_tcp.send(socket, "ERROR\r\n")
  exit(error)
end

启动我们的服务器,现在我们可以向它发送命令.现在我们可以得到两个不同的回复:当命令已知时回复"OK",否则回复"UNKNOWN COMMAND":

$ telnet 127.0.0.1 4040
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
CREATE shopping
OK
HELLO
UNKNOWN COMMAND

这意味着我们的实现已经朝着正确的方向运行,但是这看起来不太优雅,对吗?

之前的实现使用了资源管线,使得逻辑很清晰.然而,现在我们需要处理不同的错误代码,我们的服务器逻辑嵌套在了许多case调用中.

幸运的是,Elixir v1.2引入了一个叫做with的结构,它能够简化像上面那样的代码.让我们用它来重写server/1函数吧:

defp serve(socket) do
  msg =
    with {:ok, data} <- read_line(socket),
         {:ok, command} <- KVServer.Command.parse(data),
         do: KVServer.Command.run(command)

  write_line(socket, msg)
  serve(socket)
end

好多了!明智的语法,with的理解和for很类似.with将会获取<-右边的返回值,并与左边进行模式匹配.如果匹配成功,with会进入下一个表达式.如果匹配失败,未匹配的值将会被返回.

换句话说,我们将case/2中的每个表达式转化成了with中的步骤.只要任何一步中返回值不能匹配{:ok, x},with就会跳出,并返回未匹配的值.

你可在我们的文档中获取更多关于with的信息.

#运行命令

最后一步是实现KVServer.Command.run/1,来使:kv应用运行解析后的命令.它的实现如下所示:

@doc """
Runs the given command.
"""
def run(command)

def run({:create, bucket}) do
  KV.Registry.create(KV.Registry, bucket)
  {:ok, "OK\r\n"}
end

def run({:get, bucket, key}) do
  lookup bucket, fn pid ->
    value = KV.Bucket.get(pid, key)
    {:ok, "#{value}\r\nOK\r\n"}
  end
end

def run({:put, bucket, key, value}) do
  lookup bucket, fn pid ->
    KV.Bucket.put(pid, key, value)
    {:ok, "OK\r\n"}
  end
end

def run({:delete, bucket, key}) do
  lookup bucket, fn pid ->
    KV.Bucket.delete(pid, key)
    {:ok, "OK\r\n"}
  end
end

defp lookup(bucket, callback) do
  case KV.Registry.lookup(KV.Registry, bucket) do
    {:ok, pid} -> callback.(pid)
    :error -> {:error, :not_found}
  end
end

这个实现很简单:我们只需要派遣到KV.Registry服务器,它是我们在:kv应用启动时注册的.因为我们的:kv_server依赖于:kv应用,所以完全可以依赖它所提供的服务器/服务.

注意到我们也定义了一个名为lookup/2的私有函数来完成一个常用功能:搜索桶,如果存在就返回它的pid,否则返回{:error, :not_found}.

此外,由于我们现在返回的是{:error, :not_found},我们应该修改KV.Server中的write_line/2函数使之也能来打印这个错误:

defp write_line(socket, {:error, :not_found}) do
  :gen_tcp.send(socket, "NOT FOUND\r\n")
end

我们的服务器功能基本完成了!我们只需要添加测试.这一次,我们把测试留到最后,因为有一些重要的决定要做.

KVServer.Command.run/1的实现是直接发送命令到由:kv应用注册的KV.Registry服务器.这意味着这个服务器是全局的,如果我们有两个测试同时发送信息给它,我们的测试将会相互冲突(很可能失败).我们需要决定是使用相互独立且能同步运行的单元测试,还是运行在全局状态顶部的集成测试,但是每次测试就要调用应用的全栈.

目前我们只写过单元测试,而且是直接测试单个模块.然而,为了使KVServer.Command.run/1能像一个单元一样被测试,我们需要改变它的实现,不再直接发送命令到KV.Registry进程,而是传送一个作为参数的服务器.这意味着我们需要改变run的签名到def run(command, pid),以及对:create命令的实现:

def run({:create, bucket}, pid) do
  KV.Registry.create(pid, bucket)
  {:ok, "OK\r\n"}
end

当对KVServer.Command进行测试时,我们需要启动一个KV.Registry的实例,类似于我们在apps/kv/test/kv/registry_test.exs中做的那样,并将其作为一个参数传送给run/2.

这已经成为我们一直在测试中使用的方法,它的优点是:

\1. 我们的实现不会与任何特定的服务器名耦合 \2. 我们可以保持同步运行测试,因为这里没有共用状态

然而,它的缺点是我们的API为了容纳所有的外部参数而变得非常大.

替代方案是编写集成测试,它依赖于全局服务器名来使用整个堆栈,从TCP服务器到桶.集成测试的缺点是它们会比单元测试慢得多,因此它们必须节制地使用.例如,我们不应该使用集成测试在我们的命令解析实现中来测试一个边界情况.

现在我们将编写一个集成测试.集成测试会使用一个TCP客户端来发送命令到我们的服务器,并断言我们将得到预期的回复.

让我们在test/kv_server_test.exs中实现如下所示的集成测试:

defmodule KVServerTest do
  use ExUnit.Case

  setup do
    Application.stop(:kv)
    :ok = Application.start(:kv)
  end

  setup do
    opts = [:binary, packet: :line, active: false]
    {:ok, socket} = :gen_tcp.connect('localhost', 4040, opts)
    {:ok, socket: socket}
  end

  test "server interaction", %{socket: socket} do
    assert send_and_recv(socket, "UNKNOWN shopping\r\n") ==
           "UNKNOWN COMMAND\r\n"

    assert send_and_recv(socket, "GET shopping eggs\r\n") ==
           "NOT FOUND\r\n"

    assert send_and_recv(socket, "CREATE shopping\r\n") ==
           "OK\r\n"

    assert send_and_recv(socket, "PUT shopping eggs 3\r\n") ==
           "OK\r\n"

    # GET returns two lines
    assert send_and_recv(socket, "GET shopping eggs\r\n") == "3\r\n"
    assert send_and_recv(socket, "") == "OK\r\n"

    assert send_and_recv(socket, "DELETE shopping eggs\r\n") ==
           "OK\r\n"

    # GET returns two lines
    assert send_and_recv(socket, "GET shopping eggs\r\n") == "\r\n"
    assert send_and_recv(socket, "") == "OK\r\n"
  end

  defp send_and_recv(socket, command) do
    :ok = :gen_tcp.send(socket, command)
    {:ok, data} = :gen_tcp.recv(socket, 0, 1000)
    data
  end
end

我们的集成测试检查了所有的服务器接口,包括未知命令和未找到错误.因为是在处理ETS表格和链接进程,所以不必关闭套接字.一旦测试进程退出,套接字会自动关闭.

这一次,因为我们的测试依赖于全局数据,所以我们没有将async: true传送给use ExUnit.Case.而且,为了保证我们的测试始终在一个干净的状态,在每个测试之前我们停止再启动了:kv应用.事实上,停止:kv应用会在终端打印一个警告:

18:12:10.698 [info] Application kv exited: :stopped

为了避免在测试过程中打印日志,ExUnit提供了一个叫做:capture_log的干净特性.通过在每次测试前设置@tag :capture_log,或者为整个测试设置@moduletag :capture_log,在测试运行时,ExUnit会自动捕获日志中的任何东西.如果测试失败,捕获的日志会被打印在ExUnit报告旁边.

启动之前,添加如下调用:

@moduletag :capture_log

当测试崩溃时,你会看到如下报告:

1) test server interaction (KVServerTest)
   test/kv_server_test.exs:17
   ** (RuntimeError) oops
   stacktrace:
     test/kv_server_test.exs:29

   The following output was logged:

   13:44:10.035 [info]  Application kv exited: :stopped

从这个简单的集成测试中,我们可以知道为什么集成测试可能很慢.不止因为这种测试不能同步运行,还因为要求停止再启动:kv应用这种昂贵的启动配置.

最后,应当由你和你的团队来找到适用于你的应用的最好的测试策略.你需要平衡代码质量,信心,和测试套件的运行时.例如,最开始我们可能只用集成测试来测试服务器,但是如果服务器在之后的发布中持续成长,或者它成为了一个频繁发生bug的应用的一部分,那么考虑将其打碎并编写更多加强的比集成测试轻量得多的单元测试就变得非常重要.

在下一章,我们终于要通过添加一个桶路由机制来使得我们的系统成为分布式的.我们也将学习应用配置.

© 著作权归作者所有

共有 人打赏支持
ljzn
粉丝 29
博文 69
码字总数 96245
作品 0
南平
程序员
elixir官方教程Mix与OTP(一) Mix入门

Mix入门 我们的第一个项目 编辑项目 执行测试 环境 探索 在本教程中,我们将学习如何构建一个完整的Elixir应用,包括监督树,配置,测试等等. 这个应用的功能是分布式键值仓库.我们将把键值对安排...

ljzn ⋅ 2016/08/07 ⋅ 2

elixir官方入门教程 学习资料

下一步该去哪 构建你的第一个Elixir项目 元编程 社区与其它资源 Erlang基础 想要学习更多?继续阅读! 构建你的第一个Elixir项目 为了开始你的第一个项目,Elixir装载了一个叫做Mix的构建工具....

ljzn ⋅ 2016/08/06 ⋅ 0

elixir官方入门教程 介绍

介绍 安装 交互模式 运行脚本 提出疑问 欢迎! 在本教程中我们将教给你Elixir的基础,语法,如何定义模块,如何操作常用数据结构的特性等等.本章将确保Elixir安装好了,并且你能够成功运行Elixir的...

ljzn ⋅ 2016/08/06 ⋅ 0

elixir官方教程Mix与OTP(六) 依赖和伞形项目

依赖和雨伞项目 外部依赖 内部依赖 雨伞项目 伞内依赖 总结 本章,我们将讨论如何在Mix中管理依赖. 我们的应用已经完成,所以是时候实现能够处理我们在第一章中定义的请求的服务器了: 然而,我们...

ljzn ⋅ 2016/08/11 ⋅ 0

(译)循序渐进学习Elixir

——Chris Bell Learning Elixir at Made by Many 在今年奥兰多的Elixir大会上演讲之后,就经常有人问我:你们团队是如何学习Elixir和OTP的? 和学习其他语言一样,学习Elixir也需要付出时间...

ljzn ⋅ 2016/09/29 ⋅ 0

elixir官方教程Mix与OTP(九) 分布式任务与配置

分布式任务与配置 我们的第一个分布式代码 同步/等待 分布式任务 路由层 测试过滤器与标签 应用环境与配置 总结 本章,我们将回到应用并添加一个路由层,它能让我们根据桶名来在节点之间分布请...

ljzn ⋅ 2016/08/12 ⋅ 0

elixir官方教程Mix与OTP(四) 主管与应用

主管与应用 我们的第一个主管 理解应用 开启应用 应用回调 项目还是应用? 简单的一对一主管 监督树 观察者 测试中的共用状态 现在,我们的应用有个一个能监控几十个桶,不是几百个,的注册表.尽...

ljzn ⋅ 2016/08/10 ⋅ 0

Elixir v1.2.0-rc.1 发布,函数式编程语言

Elixir v1.2.0-rc.1 发布,Elixir v1.2 依赖于 Erlang 18 的大量特性,至少要求 Erlang 18+ 版本。 此版本更新内容如下: 改进 [Mix] Raise readable error message when parsertools is not...

oschina ⋅ 2015/12/31 ⋅ 2

elixir官方教程Mix与OTP(二) 代理

代理 状态问题 代理 ExUnit回调 其它代理动作 代理中的客户端/服务器 本章我们将创建一个名为的模块.这个模块的作用是以一种可以被其它进程读取和修改的方式存储我们的键值对. 如果你跳过了入...

ljzn ⋅ 2016/08/10 ⋅ 0

elixir官方入门教程 进程

进程 和 链接 任务 状态 在Elixir中,所有代码都运行在进程内。进程相互独立,并发地运行,通过传送信息来交流。进程不是Elixir中唯一的并发基础,但它意味着能够构建分布式的,可容错的程序...

ljzn ⋅ 2016/08/04 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

vbs 取文件大小 字节

dim namedim fs, s'name = Inputbox("姓名")'msgbox(name)set fs = wscript.createobject("scripting.filesystemobject") 'fs为FSO实例if (fs.folderexists("c:\temp"))......

vga ⋅ 今天 ⋅ 1

高并发之Nginx的限流

首先Nginx的版本号有要求,最低为1.11.5 如果低于这个版本,在Nginx的配置中 upstream web_app { server 到达Ip1:端口 max_conns=10; server 到达Ip2:端口 max_conns=10; } server { listen ...

算法之名 ⋅ 今天 ⋅ 0

Spring | IOC AOP 注解 简单使用

写在前面的话 很久没更新笔记了,有人会抱怨:小冯啊,你是不是在偷懒啊,没有学习了。老哥,真的冤枉:我觉得我自己很菜,还在努力学习呢,正在学习Vue.js做管理系统呢。即便这样,我还是不...

Wenyi_Feng ⋅ 今天 ⋅ 0

博客迁移到 https://www.jianshu.com/u/aa501451a235

博客迁移到 https://www.jianshu.com/u/aa501451a235 本博客不再更新

为为02 ⋅ 今天 ⋅ 0

win10怎么彻底关闭自动更新

win10自带的更新每天都很多,每一次下载都要占用大量网络,而且安装要等得时间也蛮久的。 工具/原料 Win10 方法/步骤 单击左下角开始菜单点击设置图标进入设置界面 在设置窗口中输入“服务”...

阿K1225 ⋅ 今天 ⋅ 0

Elasticsearch 6.3.0 SQL功能使用案例分享

The best elasticsearch highlevel java rest api-----bboss Elasticsearch 6.3.0 官方新推出的SQL检索插件非常不错,本文一个实际案例来介绍其使用方法。 1.代码中的sql检索 @Testpu...

bboss ⋅ 今天 ⋅ 0

informix数据库在linux中的安装以及用java/c/c++访问

一、安装前准备 安装JDK(略) 到IBM官网上下载informix软件:iif.12.10.FC9DE.linux-x86_64.tar放在某个大家都可以访问的目录比如:/mypkg,并解压到该目录下。 我也放到了百度云和天翼云上...

wangxuwei ⋅ 今天 ⋅ 0

PHP语言系统ZBLOG或许无法重现月光博客的闪耀历史[图]

最近在写博客,希望通过自己努力打造一个优秀的教育类主题博客,名动江湖,但是问题来了,现在写博客还有前途吗?面对强大的自媒体站点围剿,还有信心和可能型吗? 至于程序部分,我选择了P...

原创小博客 ⋅ 今天 ⋅ 0

IntelliJ IDEA 2018.1新特性

工欲善其事必先利其器,如果有一款IDE可以让你更高效地专注于开发以及源码阅读,为什么不试一试? 本文转载自:netty技术内幕 3月27日,jetbrains正式发布期待已久的IntelliJ IDEA 2018.1,再...

Romane ⋅ 今天 ⋅ 0

浅谈设计模式之工厂模式

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 在工厂模式中,我们在创建对象时不会对客户端暴露创建逻...

佛系程序猿灬 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部