Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add insert, insert_pair and insert_list to ExMachina.Ecto, and ExMachina.Strategy #102

Merged
merged 4 commits into from
Mar 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 31 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,11 @@ build(:comment, attrs)
build_pair(:comment, attrs)
build_list(3, :comment, attrs)

# `create*` returns a saved comment.
# Associated records defined on the factory are built and saved.
create(:comment, attrs)
create_pair(:comment, attrs)
create_list(3, :comment, attrs)
# `insert*` returns an inserted comment. Only works with ExMachina.Ecto
# Associated records defined on the factory are inserted as well.
insert(:comment, attrs)
insert_pair(comment, attrs)
insert_list(3, :comment, attrs)

# `params_for` returns a plain map without any Ecto specific attributes.
# This is only available when using `ExMachina.Ecto`.
Expand All @@ -134,15 +134,16 @@ params_for(:comment, attrs)
## Usage in a test

```elixir
# Example of use in Phoenix with a factory that uses ExMachina.Ecto
defmodule MyApp.MyModuleTest do
use MyApp.ConnCase
# You can also import this in your MyApp.ConnCase if using Phoenix
import MyApp.Factory

test "shows comments for an article" do
conn = conn()
article = create(:article)
comment = create(:comment, article: article)
article = insert(:article)
comment = insert(:comment, article: article)

conn = get conn, article_path(conn, :show, article.id)

Expand Down Expand Up @@ -171,11 +172,11 @@ different ways of saving the record for different types of factories.

## Ecto Associations

ExMachina will automatically save any associations when you call `create/2`.
This includes `belongs_to` and anything that is automatically saved by using an
Ecto changesets, such as `has_many`, `has_one`, and embeds. Since we
automatically save these records for you, we advise that factory definitions
only use `build/2` when declaring associations, like so:
ExMachina will automatically save any associations when you call any of the
`insert` functions. This includes `belongs_to` and anything that is
inserted by Ecto when using `Repo.insert!`, such as `has_many`, `has_one`,
and embeds. Since we automatically save these records for you, we advise that
factory definitions only use `build/2` when declaring associations, like so:

```elixir
def factory(:article) do
Expand All @@ -188,7 +189,7 @@ def factory(:article) do
end
```

Using `create/2` in factory definitions may lead to performance issues and bugs,
Using `insert/2` in factory definitions may lead to performance issues and bugs,
as records will be saved unnecessarily.

## Flexible Factories with Pipes
Expand All @@ -206,22 +207,7 @@ end
build(:user) |> make_admin |> create |> with_article
```

## Using with Phoenix and Ecto

There is nothing special you need to do with Phoenix unless you decide to
`import` your factory module.

By default Phoenix imports `Ecto.Model` in the generated `ConnCase` and
`ModelCase` modules (found in `test/support/conn_case.ex` and
`test/support/model_case.ex`). To import your factory we recommend excluding
`build/2` or aliasing your factory instead.

```elixir
# in test/support/conn_case|model_case.ex

# Add `except: [build: 2] to the `Ecto.Model` import
import Ecto.Model, except: [build: 2]
```
## Using with Phoenix

If you want to keep the factories somewhere other than `test/support`,
change this line in `mix.exs`:
Expand All @@ -231,51 +217,36 @@ change this line in `mix.exs`:
defp elixirc_paths(:test), do: ["lib", "web", "test/support", "test/factories"]
```

## Using without Ecto
## Custom Strategies

You can use ExMachina without Ecto, by using just the `build` function, or by
defining `save_record/1` in your module.
You can use ExMachina without Ecto, by using just the `build` functions, or you
can define one or more custom strategies to use in your factory. You can also
use custom strategies with Ecto. Here's an example of a strategy for json
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ecto changed the function to Ecto.build_assoc so this no longer applies

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

encoding your factories. See the docs on [ExMachina.Strategy] for more info.

```elixir
defmodule MyApp.JsonFactory do
use ExMachina
[ExMachina.Strategy]: https://hexdocs.pm/ex_machina/ExMachina.Strategy.html

def factory(:user) do
%User{name: "John"}
end
```elixir
defmodule MyApp.JsonEncodeStrategy do
use ExMachina.Strategy, function_name: :json_encode

def save_record(record) do
# Poison is a library for working with JSON
def handle_json_encode(record, _opts) do
Poison.encode!(record)
end
end

# Will build and then return a JSON encoded version of the map
MyApp.JsonFactories.create(:user)
```

You can do something similar while also using Ecto by defining a new function.
This gives you the power to call `create` and save to Ecto, or call `build_json`
or `create_json` to return encoded JSON objects.

```elixir
defmodule MyApp.Factory do
use ExMachina.Ecto, repo: MyApp.Repo
use ExMachina
# Using this will add json_encode/2, json_encode_pair/2 and json_encode_list/2
use MyApp.JsonEncodeStrategy

def factory(:user) do
%User{name: "John"}
end

# builds the object and then encodes it as JSON
def build_json(factory_name, attrs) do
build(factory_name, attrs) |> Poison.encode!
end

# builds the object, saves it to Ecto and then encodes it
def create_json(factory_name, attrs) do
create(factory_name, attrs) |> Poison.encode!
end
end

# Will build and then return a JSON encoded version of the user.
MyApp.JsonFactories.json_encode(:user)
```

## Contributing
Expand Down
107 changes: 6 additions & 101 deletions lib/ex_machina.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule ExMachina do
In depth examples are in the [README](README.html)
"""

defmodule UndefinedFactory do
defmodule UndefinedFactoryError do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is Elixir convention to add Error to all errors

@moduledoc """
Error raised when trying to build or create a factory that is undefined.
"""
Expand All @@ -15,26 +15,15 @@ defmodule ExMachina do
def exception(factory_name) do
message =
"""
No factory defined for #{inspect factory_name}. This may be because you
defined a factory with two parameters like this:
No factory defined for #{inspect factory_name}.

def factory(#{inspect factory_name}, attrs)

As of ExMachina 0.5.0, we no longer call factory/2. Please define your
factory function without the second attrs parameter:
Please check for typos or define your factory:

def factory(#{inspect factory_name}) do
...
end

The assoc/3 function has also been removed. belongs_to relationships
can now be used with build:

def factory(#{inspect factory_name}) do
parent: build(:parent)
end
"""
%UndefinedFactory{message: message}
%UndefinedFactoryError{message: message}
end
end

Expand All @@ -61,7 +50,7 @@ defmodule ExMachina do
quote do
@before_compile unquote(__MODULE__)

import ExMachina, only: [sequence: 1, sequence: 2, factory: 2]
import ExMachina, only: [sequence: 1, sequence: 2]

def build(factory_name, attrs \\ %{}) do
ExMachina.build(__MODULE__, factory_name, attrs)
Expand All @@ -74,40 +63,9 @@ defmodule ExMachina do
def build_list(number_of_factories, factory_name, attrs \\ %{}) do
ExMachina.build_list(__MODULE__, number_of_factories, factory_name, attrs)
end

def create(built_record) when is_map(built_record) do
ExMachina.create(__MODULE__, built_record)
end

def create(factory_name, attrs \\ %{}) do
ExMachina.create(__MODULE__, factory_name, attrs)
end

def create_pair(factory_name, attrs \\ %{}) do
ExMachina.create_pair(__MODULE__, factory_name, attrs)
end

def create_list(number_of_factories, factory_name, attrs \\ %{}) do
ExMachina.create_list(__MODULE__, number_of_factories, factory_name, attrs)
end
end
end

defmacro factory(factory_name, do: _block) do
raise """
The factory and assoc macros have been removed. Please use regular
functions instead.

def factory(#{factory_name}) do
%{
...
some_assoc: build(:some_assoc)
...
}
end
"""
end

@doc """
Shortcut for creating unique values. Similar to sequence/2

Expand Down Expand Up @@ -188,59 +146,6 @@ defmodule ExMachina do
end)
end

@doc """
Builds and saves a factory with the passed in factory_name

If using ExMachina.Ecto it will use the Ecto Repo passed in to save the
record automatically.

If you are not using ExMachina.Ecto, you need to define a `save_record/1`
function in your module. See `save_record` docs for more information.

## Example

def factory(:user, _attrs) do
%{name: "John Doe", admin: false}
end

# Saves and returns %{name: "John Doe", admin: true}
create(:user, admin: true)
"""

def create(module, built_record) when is_map(built_record) do
module.save_record(built_record)
end

def create(module, factory_name, attrs \\ %{}) do
ExMachina.build(module, factory_name, attrs) |> module.save_record
end

@doc """
Creates and returns 2 records with the passed in factory_name and attrs

## Example

# Returns a list of 2 saved users
create_pair(:user)
"""
def create_pair(module, factory_name, attrs \\ %{}) do
ExMachina.create_list(module, 2, factory_name, attrs)
end

@doc """
Creates and returns X records with the passed in factory_name and attrs

## Example

# Returns a list of 3 saved users
create_list(3, :user)
"""
def create_list(module, number_of_factories, factory_name, attrs \\ %{}) do
Enum.map(1..number_of_factories, fn(_) ->
ExMachina.create(module, factory_name, attrs)
end)
end

defmacro __before_compile__(_env) do
# We are using line -1 because we don't want warnings coming from
# save_record/1 when someone defines there own save_record/1 function.
Expand All @@ -249,7 +154,7 @@ defmodule ExMachina do
Raises a helpful error if no factory is defined.
"""
def factory(factory_name) do
raise UndefinedFactory, factory_name
raise UndefinedFactoryError, factory_name
end

@doc """
Expand Down
49 changes: 14 additions & 35 deletions lib/ex_machina/ecto.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
defmodule ExMachina.Ecto do
@moduledoc """
Module for building and inserting factories with Ecto

This module works much like the regular `ExMachina` module, but adds a few
nice things that make working with Ecto easier.

* It uses `ExMachina.EctoStrategy`, which adds `insert/1`, `insert/2`,
`insert_pair/2`, `insert_list/3`.
* Adds a `params_for` function that is useful for working with changesets or
sending params to API endpoints.

More in-depth examples are in the [README](README.html).
"""
defmacro __using__(opts) do
verify_ecto_dep
if repo = Keyword.get(opts, :repo) do
quote do
use ExMachina

@repo unquote(repo)
use ExMachina.EctoStrategy, repo: unquote(repo)

def params_for(factory_name, attrs \\ %{}) do
ExMachina.Ecto.params_for(__MODULE__, factory_name, attrs)
Expand All @@ -14,24 +26,6 @@ defmodule ExMachina.Ecto do
def fields_for(factory_name, attrs \\ %{}) do
raise "fields_for/2 has been renamed to params_for/2."
end

def save_record(record) do
ExMachina.Ecto.save_record(@repo, record)
end

defp assoc(_, factory_name, _ \\ nil) do
raise """
assoc/3 has been removed. Please use build instead. Built records will be automatically saved when you call create.

def factory(#{factory_name}) do
%{
...
some_assoc: build(:some_assoc)
...
}
end
"""
end
end
else
raise ArgumentError,
Expand Down Expand Up @@ -81,19 +75,4 @@ defmodule ExMachina.Ecto do
defp drop_ecto_fields(record) do
raise ArgumentError, "#{inspect record} is not an Ecto model. Use `build` instead."
end

@doc """
Saves a record and all associated records using `Repo.insert!`

## Example

# Will save the article and list of comments
create(:article, comments: [build(:comment)])
"""
def save_record(repo, %{__meta__: %{__struct__: Ecto.Schema.Metadata}} = record) do
repo.insert!(record)
end
def save_record(_, record) do
raise ArgumentError, "#{inspect record} is not an Ecto model. Use `build` instead"
end
end
Loading