EctoStreamFactory helps to utilize StreamData generators as Ecto factories.
You can define one factory and use it in the following scenarios:
- Regular unit/integration/acceptance tests
- Property-based tests
- Load/stress/performance tests
- Database seeding
You can read about property-based testing in Fred Hebert's book.
Installation
Add EctoStreamFactory dependency to mix.exs:
def deps do
[
{:ecto_stream_factory, "~> 0.1", only: [:test, :dev]}
]
end
Create a factory module in test/support/factory.ex. Generator functions should have "_generator" suffix:
defmodule MyApp.Factory do
use EctoStreamFactory, repo: MyApp.Repo
def user_generator do
gen all name <- string(:alphanumeric, min_length: 1),
age <- integer(15..80),
email <- email_generator() do
%MyApp.User{name: name, age: age, email: email}
end
end
def post_generator do
gen all author <- user_generator(),
body <- string(:alphanumeric, min_length: 10) do
%EctoStreamFactory.Post{author: author, body: body}
end
end
def email_generator do
gen all username <- string(:alphanumeric, min_length: 1),
domain <- member_of(~w(gmail.com protonmail.com yandex.com)) do
"#{username}#{System.unique_integer([:positive, :monotonic])}@#{domain}"
end
end
end
Make sure that mix.exs is configured to compile the factory for required environments:
def project do
[
elixirc_paths: elixirc_paths(Mix.env())
]
end
defp elixirc_paths(env) when env in [:test, :dev], do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
Optionally import the factory in .iex.exs to simplify its usage inside iex -S mix console on your development machine:
import MyApp.Factory
For a vanilla Elixir project you can import the factory in every test module with ExUnti.CaseTemplate.
Create a file test/support/case.ex and add:
defmodule MyApp.Case do
use ExUnit.CaseTemplate
using do
quote do
import MyApp.Factory
end
end
end
Then in your test files replase use ExUnit.Case with use MyApp.Case
For a Phoenix project you can import the factory in support/data_case.ex, support/conn_case.ex, support/channel_case:
defmodule MyAppWeb.ConnCase do
using do
quote do
...
import MyApp.Factory
end
end
end
Usage in regular tests
iex> build(:email)
"S1@protonmail.com"
iex> build(:user)
%User{id: nil, name: "a", age: 33, email: "I2@yandex.com"}
iex> build(:user, name: "Bob")
%User{id: nil, name: "Bob", age: 28, email: "S3@gmail.com"}
iex> build(:post, text: "Hello world")
%Post{id: nil, text: "Hello world", author: %User{id: nil, name: "b", age: 28, email: "l4@gmail.com"}}
iex> build_list(2, :user, name: fn n -> "user#{n}" end)
[%User{id: nil, name: "user1", age: 51, email: "n5@gmail.com"}, %User{id: nil, name: "user2", age: 40, email: "O6@yandex.com"}]
iex> insert(:user)
%User{id: 1, name: "b", age: 23, email: "az7@gmail.com"}
iex> insert(:user, [email: "az7@gmail.com"], on_conflict: :nothing)
%User{id: nil, name: "c", age: 44, email: "az7@gmail.com"}
iex> insert(:post, author: build(:user, name: "Jane"))
%Post{id: 1, text: "kjfwi245lfh", author: %User{id: 2, name: "Jane", age: 34, email: "jhg8@yandex.com"}}
iex> insert_list(2, :user, age: 18)
[%User{id: 3, name: "bc", age: 18, email: "kl9@protonmail.com"}, %User{id: 4, name: "bd", age: 18, email: "hj10@yandex.com"}]
Usage in property-based tests
defmodule MyAppTest do
use MyApp.Case, async: true
use ExUnitProperties
describe "user properties" do
property "contact info contains user name and email" do
check all user <- user_generator() do
info = MyApp.User.contact_info(user)
assert String.starts_with?(info, user.name)
assert info =~ user.email
end
end
property "adult users older than 18" do
check all user <- user_generator() do
assert MyApp.User.adult?(user) == user.age >= 18
end
end
end
end
Usage in seeding the database
In priv/repo/seeds.exs
import MyApp.Factory
insert_list(100, :post)
Then run it with mix run priv/repo/seeds.exs