Chapter 10

测试、部署与生态

ExUnit 测试哲学、mix release 自包含发布、Fly.io 零配置云部署,以及 Elixir 生态全景图

ExUnit 测试框架

ExUnit 是 Elixir 内置的测试框架,无需额外安装:

# test/blog/posts_test.exs
defmodule Blog.PostsTest do
  use Blog.DataCase   # Phoenix 提供,带数据库沙箱

  alias Blog.Posts

  describe "create_post/1" do

    test "有效参数时成功创建" do
      user  = insert(:user)   # ExMachina 工厂
      attrs = %{title: "测试文章", body: "这是一篇测试文章内容", author_id: user.id}

      assert {:ok, post} = Posts.create_post(attrs)
      assert post.title == "测试文章"
      assert post.published == false
    end

    test "标题为空时返回错误" do
      attrs = %{title: "", body: "内容"}
      assert {:error, changeset} = Posts.create_post(attrs)
      assert "can't be blank" in errors_on(changeset).title
    end

    test "标题过短时返回错误" do
      attrs = %{title: "短", body: "内容足够长的正文"}
      assert {:error, changeset} = Posts.create_post(attrs)
      assert "should be at least 5 character(s)" in errors_on(changeset).title
    end

  end
end

测试 GenServer

defmodule Blog.ShoppingCartTest do
  use ExUnit.Case, async: true   # async: true 并发执行测试

  setup do
    {:ok, cart} = ShoppingCart.start_link()
    %{cart: cart}
  end

  test "初始购物车为空", %{cart: cart} do
    assert ShoppingCart.get_cart(cart) == []
  end

  test "添加商品后总价正确", %{cart: cart} do
    ShoppingCart.add_item(cart, %{id: 1, name: "键盘", price: 299})
    ShoppingCart.add_item(cart, %{id: 2, name: "鼠标", price: 199})
    assert ShoppingCart.total(cart) == 498
  end
end

测试 LiveView

defmodule BlogWeb.CounterLiveTest do
  use BlogWeb.ConnCase
  import Phoenix.LiveViewTest

  test "初始显示 0", %{conn: conn} do
    {:ok, view, html} = live(conn, "/counter")
    assert html =~ "计数器:0"
  end

  test "点击增加后计数器 +1", %{conn: conn} do
    {:ok, view, _html} = live(conn, "/counter")
    html = view |> element("button", "+") |> render_click()
    assert html =~ "计数器:1"
  end
end

ExMachina:测试数据工厂

# test/support/factory.ex
defmodule Blog.Factory do
  use ExMachina.Ecto, repo: Blog.Repo

  def user_factory do
    %Blog.Accounts.User{
      email:           sequence(:email, &"user#{&1}@example.com"),
      username:        sequence(:username, &"user#{&1}"),
      hashed_password: Bcrypt.hash_pwd_salt("password123")
    }
  end

  def post_factory do
    %Blog.Posts.Post{
      title:     sequence(:title, &"测试文章 #{&1}"),
      body:      "这是一段足够长的文章正文内容,用于测试验证。",
      published: false,
      author:    build(:user)
    }
  end
end

# 在测试中使用
user  = insert(:user)
post  = insert(:post, author: user, published: true)
posts = insert_list(5, :post)   # 批量创建 5 篇

mix release:自包含二进制发布

# 生产环境构建(包含 Erlang 运行时,无需目标机器安装 Elixir)
MIX_ENV=prod mix do deps.get, assets.deploy, release

# 发布包位于 _build/prod/rel/blog/

# 启动(前台)
_build/prod/rel/blog/bin/blog start

# 守护进程方式
_build/prod/rel/blog/bin/blog daemon

# 连接到运行中的节点(热调试!)
_build/prod/rel/blog/bin/blog remote

# 运行迁移
_build/prod/rel/blog/bin/blog eval "Blog.Release.migrate()"

Docker 化部署

# Dockerfile(Phoenix 官方推荐多阶段构建)
FROM hexpm/elixir:1.17.0-erlang-27.0-debian-bookworm-20240701-slim AS build

WORKDIR /app

RUN apt-get update -y && apt-get install -y build-essential git nodejs npm

COPY mix.exs mix.lock ./
COPY config config
RUN mix deps.get --only prod
RUN MIX_ENV=prod mix deps.compile

COPY assets assets
COPY priv priv
COPY lib lib

RUN MIX_ENV=prod mix assets.deploy
RUN MIX_ENV=prod mix release

# 最终运行镜像(极小,仅含运行时)
FROM debian:bookworm-20240701-slim AS app

RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=build /app/_build/prod/rel/blog ./

EXPOSE 4000
ENV PHX_SERVER=true
ENTRYPOINT ["/app/bin/blog", "start"]

Fly.io 部署(Elixir 官方推荐)

Fly.io 对 Elixir/Phoenix 有原生支持,自动识别项目并生成配置:

# 安装 flyctl
curl -L https://fly.io/install.sh | sh

# 初始化(自动检测 Phoenix,生成 fly.toml 和 Dockerfile)
fly launch

# 设置数据库(Postgres)
fly postgres create --name blog-db
fly postgres attach blog-db

# 部署
fly deploy

# 查看日志
fly logs

# 进入生产 REPL(这是 Elixir 的独特优势!)
fly ssh console -C "/app/bin/blog remote"
# iex> :observer.start()  实时查看进程树!

Elixir 生产调试的超能力fly ssh console 连接到生产节点后,你可以运行任意 Elixir 代码、查看进程状态、修改运行中的状态——完全无需重启。这是 BEAM 热代码更新与进程透明性带来的独特体验,其他语言运行时几乎无法实现。

Elixir 生态与学习资源

核心生态库

库名用途类比
Ecto数据库 ORMActiveRecord / Prisma
PhoenixWeb 框架Rails / Django
LiveView实时 UIReact + WebSocket
Oban后台任务队列Sidekiq / BullMQ
AbsintheGraphQL 服务端graphql-ruby
TeslaHTTP 客户端axios / Got
ExDoc文档生成JSDoc / Sphinx
Credo代码分析(Lint)ESLint
Dialyxir类型检查(Dialyzer)TypeScript
Broadway数据处理管道Apache Kafka Streams

学习资源推荐

Phoenix LiveView 未来展望

LiveView 正在不断演进,几个值得关注的方向:

💜

课程总结:恭喜你完成了 Elixir / Phoenix LiveView 全部 10 章!你已掌握:BEAM VM 并发模型、Elixir 函数式语法、OTP 容错框架、Phoenix MVC、LiveView 实时 UI、Channels 广播、Ecto 数据建模、测试与 Fly.io 生产部署。Elixir 是一门小众但极具深度的语言,学习它不只是多一项技能,更是重新理解并发与可靠系统的一次思维升级。