Weird constraint error when testing schema

$ mix hex.info
Hex:    0.17.1
Elixir: 1.5.1
OTP:    20.1

Built with: Elixir 1.5.1 and OTP 18.3

MariaDB 10.2.9

I’m writing some tests for my User schema:

defmodule Myflow.Wechat.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias Myflow.Wechat.User


  schema "wechat_users" do
    field :open_id, :string
    field :timezone, :string, default: "Asia/Shanghai"

    timestamps()
  end

  @doc false
  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:open_id, :timezone])
    |> validate_required([:open_id])
    |> unique_constraint(:open_id)
  end
end

Unique index is already created in migration:

defmodule Myflow.Repo.Migrations.CreateWechatUsers do
  use Ecto.Migration

  def change do
    create table(:wechat_users) do
      add :open_id, :string
      add :timezone, :string

      timestamps(size: 6)
    end

    create unique_index(:wechat_users, [:open_id])
  end
end

And here’s my test:

defmodule Myflow.Wechat.UserTest do
  use Myflow.DataCase

  alias Myflow.Wechat.User
  alias Myflow.Repo

  @valid_attrs %{open_id: "xxx"}
  @invalid_attrs %{}

  test "open_id should not be duplicated" do
    changeset = User.changeset(%User{}, @valid_attrs)
    assert {:ok, changeset} = Repo.insert changeset
    assert {:error, changeset} = Repo.insert changeset
  end
end

When I run mix test, I got:

1) test open_id should not be duplicated (Myflow.Wechat.UserTest)
     test/myflow/wechat/user_test.exs:10
     ** (Ecto.ConstraintError) constraint error when attempting to insert struct:

         * unique: PRIMARY

     If you would like to convert this constraint into an error, please
     call unique_constraint/3 in your changeset and define the proper
     constraint name. The changeset has not defined any constraint.

     code: assert {:error, changeset} = Repo.insert changeset
     stacktrace:
       (ecto) lib/ecto/repo/schema.ex:570: anonymous fn/4 in Ecto.Repo.Schema.constraints_to_errors/3
       (elixir) lib/enum.ex:1255: Enum."-map/2-lists^map/1-0-"/2
       (ecto) lib/ecto/repo/schema.ex:555: Ecto.Repo.Schema.constraints_to_errors/3
       (ecto) lib/ecto/repo/schema.ex:219: anonymous fn/14 in Ecto.Repo.Schema.do_insert/4
       test/myflow/wechat/user_test.exs:13: (test)

The weird part is, if I run Repo.insert changeset twice in iex,

Myflow.Repo.insert u
[debug] QUERY OK db=5.8ms
INSERT INTO `wechat_users` (`open_id`,`timezone`,`inserted_at`,`updated_at`) VALUES (?,?,?,?) ["xxx", "Asia/Shanghai", {{2017, 11, 3}, {1, 2, 14, 214822}}, {{2017, 11, 3}, {1, 2, 14, 214829}}]
{:ok,
 %Myflow.Wechat.User{__meta__: #Ecto.Schema.Metadata<:loaded, "wechat_users">,
  id: 6, inserted_at: ~N[2017-11-03 01:02:14.214822], open_id: "xxx",
  timezone: "Asia/Shanghai", updated_at: ~N[2017-11-03 01:02:14.214829]}}
(search)`r': Myflow.Repo.insert u
[debug] QUERY ERROR db=2.3ms
INSERT INTO `wechat_users` (`open_id`,`timezone`,`inserted_at`,`updated_at`) VALUES (?,?,?,?) ["xxx", "Asia/Shanghai", {{2017, 11, 3}, {1, 2, 22, 790847}}, {{2017, 11, 3}, {1, 2, 22, 790853}}]
{:error,
 #Ecto.Changeset<action: :insert,
  changes: %{open_id: "xxx", timezone: "Asia/Shanghai"},
  errors: [open_id: {"has already been taken", []}],
  data: #Myflow.Wechat.User<>, valid?: false>}

Everything works as I expected.

Just don’t know what could be wrong with my test code here. Any help?

You are using a changeset, but then, You turn it to a User on the second line

changeset = User.changeset(%User{}, @valid_attrs)
assert {:ok, changeset} = Repo.insert changeset

Maybe try this

changeset = User.changeset(%User{}, @valid_attrs)
assert {:ok, _user} = Repo.insert changeset
assert {:error, changeset} = Repo.insert changeset

Thanks! What a mistake I have made!