Giter Site home page Giter Site logo

thoughtbot / ex_machina Goto Github PK

View Code? Open in Web Editor NEW
1.9K 33.0 143.0 392 KB

Create test data for Elixir applications

Home Page: https://hex.pm/packages/ex_machina

License: MIT License

Elixir 99.63% Shell 0.37%
elixir testing factory-definitions factories

ex_machina's Introduction

ExMachina

Circle CI

ExMachina is part of the thoughtbot Elixir family of projects.

ExMachina makes it easy to create test data and associations. It works great with Ecto, but is configurable to work with any persistence library.

This README follows the main branch, which may not be the currently published version. Here are the docs for the latest published version of ExMachina.

Installation

To install in all environments (useful for generating seed data in dev/prod):

In mix.exs, add the ExMachina dependency:

def deps do
  # Get the latest from hex.pm. Works with Ecto 3.0
  [
    {:ex_machina, "~> 2.7.0"},
  ]
end

And start the ExMachina application. For most projects (such as Phoenix apps) this will mean adding :ex_machina to the list of applications in mix.exs. You can skip this step if you are using Elixir 1.4 or later.

def application do
  [mod: {MyApp, []},
   applications: [:ex_machina, :other_apps...]]
end

Install in just the test environment with Phoenix:

In mix.exs, add the ExMachina dependency:

def deps do
  [
    {:ex_machina, "~> 2.7.0", only: :test},
  ]
end

Add your factory module inside test/support so that it is only compiled in the test environment.

Next, be sure to start the application in your test/test_helper.exs before ExUnit.start:

{:ok, _} = Application.ensure_all_started(:ex_machina)

Install in just the test environment for non-Phoenix projects:

You will follow the same instructions as above, but you will also need to add test/support to your compilation paths (elixirc_paths) if you have not done so already.

In mix.exs, add test/support to your elixirc_paths for just the test env.

def project do
  [app: ...,
   # Add this if it's not already in your project definition.
   elixirc_paths: elixirc_paths(Mix.env)]
end

# This makes sure your factory and any other modules in test/support are compiled
# when in the test environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

Overview

Check out the docs for more details.

Define factories:

defmodule MyApp.Factory do
  # with Ecto
  use ExMachina.Ecto, repo: MyApp.Repo

  # without Ecto
  use ExMachina

  def user_factory do
    %MyApp.User{
      name: "Jane Smith",
      email: sequence(:email, &"email-#{&1}@example.com"),
      role: sequence(:role, ["admin", "user", "other"]),
    }
  end

  def article_factory do
    title = sequence(:title, &"Use ExMachina! (Part #{&1})")
    # derived attribute
    slug = MyApp.Article.title_to_slug(title)
    %MyApp.Article{
      title: title,
      slug: slug,
      # associations are inserted when you call `insert`
      author: build(:user),
    }
  end

  # derived factory
  def featured_article_factory do
    struct!(
      article_factory(),
      %{
        featured: true,
      }
    )
  end

  def comment_factory do
    %MyApp.Comment{
      text: "It's great!",
      article: build(:article),
    }
  end
end

Using factories (check out the docs for more details):

# `attrs` are automatically merged in for all build/insert functions.

# `build*` returns an unsaved comment.
# Associated records defined on the factory are built.
attrs = %{body: "A comment!"} # attrs is optional. Also accepts a keyword list.
build(:comment, attrs)
build_pair(:comment, attrs)
build_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`.
params_for(:comment, attrs)

# `params_with_assocs` is the same as `params_for` but inserts all belongs_to
# associations and sets the foreign keys.
# This is only available when using `ExMachina.Ecto`.
params_with_assocs(:comment, attrs)

# Use `string_params_for` to generate maps with string keys. This can be useful
# for Phoenix controller tests.
string_params_for(:comment, attrs)
string_params_with_assocs(:comment, attrs)

Delayed evaluation of attributes

build/2 is a function call. As such, it gets evaluated immediately. So this code:

insert_pair(:account, user: build(:user))

Is equivalent to this:

user = build(:user)
insert_pair(:account, user: user) # same user for both accounts

Sometimes that presents a problem. Consider the following factory:

def user_factory do
  %{name: "Gandalf", email: sequence(:email, "gandalf#{&1}@istari.com")}
end

If you want to build a separate user per account, then calling insert_pair(:account, user: build(:user)) will not give you the desired result.

In those cases, you can delay the execution of the factory by passing it as an anonymous function:

insert_pair(:account, user: fn -> build(:user) end)

You can also do that in a factory definition:

def account_factory do
  %{user: fn -> build(:user) end}
end

You can even accept the parent record as an argument to the function:

def account_factory do
  %{user: fn account -> build(:user, vip: account.premium) end}
end

Note that the account passed to the anonymous function is only the struct after it's built. It's not an inserted record. Thus, it does not have data that is only accessible after being inserted into the database (e.g. id).

Full control of factory

By default, ExMachina will merge the attributes you pass into build/insert into your factory. But if you want full control of your attributes, you can define your factory as accepting one argument, the attributes being passed into your factory.

def custom_article_factory(attrs) do
  title = Map.get(attrs, :title, "default title")

  article = %Article{
    author: "John Doe",
    title: title
  }

  # merge attributes and evaluate lazy attributes at the end to emulate
  # ExMachina's default behavior
  article
  |> merge_attributes(attrs)
  |> evaluate_lazy_attributes()
end

NOTE that in this case ExMachina will not merge the attributes into your factory, and it will not evaluate lazy attributes. You will have to do this on your own if desired.

Non-map factories

Because you have full control of the factory when defining it with one argument, you can build factories that are neither maps nor structs.

# factory definition
def room_number_factory(attrs) do
  %{floor: floor_number} = attrs
  sequence(:room_number, &"#{floor_number}0#{&1}")
end

# example usage
build(:room_number, floor: 5)
# => "500"

build(:room_number, floor: 5)
# => "501"

NOTE that you cannot use non-map factories with Ecto. So you cannot insert(:room_number).

Usage in a test

# Example of use in Phoenix with a factory that uses ExMachina.Ecto
defmodule MyApp.MyModuleTest do
  use MyApp.ConnCase
  # If using Phoenix, import this inside the using block in MyApp.ConnCase
  import MyApp.Factory

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

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

    assert html_response(conn, 200) =~ article.title
    assert html_response(conn, 200) =~ comment.body
  end
end

Where to put your factories

If you are using ExMachina in all environments:

Start by creating one factory module (such as MyApp.Factory) in lib/my_app/factory.ex and putting all factory definitions in that module.

If you are using ExMachina in only the test environment:

Start by creating one factory module (such as MyApp.Factory) in test/support/factory.ex and putting all factory definitions in that module.

Later on you can easily create different factories by creating a new module in the same directory. This can be helpful if you need to create factories that are used for different repos, your factory module is getting too big, or if you have different ways of saving the record for different types of factories.

Splitting factories into separate files

This example shows how to set up factories for the testing environment. For setting them in all environments, please see the To install in all environments section

Start by creating main factory module in test/support/factory.ex and name it MyApp.Factory. The purpose of the main factory is to allow you to include only a single module in all tests.

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

The main factory includes MyApp.ArticleFactory, so let's create it next. It might be useful to create a separate directory for factories, like test/factories. Here is how to create a factory:

# test/factories/article_factory.ex
defmodule MyApp.ArticleFactory do
  defmacro __using__(_opts) do
    quote do
      def article_factory do
        %MyApp.Article{
          title: "My awesome article!",
          body: "Still working on it!"
        }
      end
    end
  end
end

This way you can split your giant factory file into many small files. But what about name conflicts? Use pattern matching to avoid them!

# test/factories/post_factory.ex
defmodule MyApp.PostFactory do
  defmacro __using__(_opts) do
    quote do
      def post_factory do
        %MyApp.Post{
          body: "Example body"
        }
      end

      def with_comments(%MyApp.Post{} = post) do
        insert_pair(:comment, post: post)
        post
      end
    end
  end
end

# test/factories/video_factory.ex
defmodule MyApp.VideoFactory do
  defmacro __using__(_opts) do
    quote do
      def video_factory do
        %MyApp.Video{
          url: "example_url"
        }
      end

      def with_comments(%MyApp.Video{} = video) do
        insert_pair(:comment, video: video)
        video
      end
    end
  end
end

If you place your factories outside of test/support make sure they will compile by adding that directory to the compilation paths in your mix.exs file. For example for the test/factories files above you would modify your file like so:

# ./mix.exs
...
  defp elixirc_paths(:test), do: ["lib", "test/factories", "test/support"]
...

Ecto

Ecto Associations

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:

def article_factory do
  %Article{
    title: "Use ExMachina!",
    # associations are inserted when you call `insert`
    comments: [build(:comment)],
    author: build(:user),
  }
end

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

Passing options to Repo.insert!/2

ExMachina.Ecto uses Repo.insert!/2 to insert records into the database. Sometimes you may want to pass options to deal with multi-tenancy or return some values generated by the database. In those cases, you can use c:ExMachina.Ecto.insert/3:

For example,

# return values from the database
insert(:user, [name: "Jane"], returning: true)

# use a different prefix
insert(:user, [name: "Jane"], prefix: "other_tenant")

Flexible Factories with Pipes

def make_admin(user) do
  %{user | admin: true}
end

def with_article(user) do
  insert(:article, user: user)
  user
end

build(:user) |> make_admin |> insert |> with_article

Using with Phoenix

If you want to keep the factories somewhere other than test/support, change this line in mix.exs:

# Add the folder to the end of the list. In this case we're adding `test/factories`.
defp elixirc_paths(:test), do: ["lib", "test/support", "test/factories"]

Custom Strategies

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 encoding your factories. See the docs on ExMachina.Strategy for more info.

defmodule MyApp.JsonEncodeStrategy do
  use ExMachina.Strategy, function_name: :json_encode

  def handle_json_encode(record, _opts) do
    Poison.encode!(record)
  end
end

defmodule MyApp.Factory do
  use ExMachina
  # Using this will add json_encode/2, json_encode_pair/2 and json_encode_list/3
  use MyApp.JsonEncodeStrategy

  def user_factory do
    %User{name: "John"}
  end
end

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

Contributing

Before opening a pull request, please open an issue first.

$ git clone https://github.com/thoughtbot/ex_machina.git
$ cd ex_machina
$ mix deps.get
$ mix test

Once you've made your additions and mix test passes, go ahead and open a PR!

License

ExMachina is Copyright ยฉ 2015 thoughtbot. It is free software, and may be redistributed under the terms specified in the LICENSE file.

About thoughtbot

thoughtbot

ExMachina is maintained and funded by thoughtbot, inc. The names and logos for thoughtbot are trademarks of thoughtbot, inc.

We love open source software, Elixir, and Phoenix. See our other Elixir projects, or hire our Elixir Phoenix development team to design, develop, and grow your product.

Inspiration

ex_machina's People

Contributors

arsduo avatar btkostner avatar davidkuhta avatar dependabot[bot] avatar doomspork avatar drapergeek avatar fs02 avatar geofflane avatar germsvel avatar jbosse avatar jesenko avatar jsteiner avatar kianmeng avatar lcezermf avatar leonid-shevtsov avatar lukerollans avatar mhanberg avatar moxley avatar nathanl avatar ngrash avatar paulcsmith avatar pdawczak avatar philipgiuliani avatar potibas avatar rradz avatar rranelli avatar schrockwell avatar scrogson avatar sorentwo avatar suhrawardi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ex_machina's Issues

Allow using factories with non maps/structs

I needed to generate an id for use with mandrill. I did this

  def factory(:mandrill_id) do
    sequence(:mandrill_id, &"mandrill_message_id_#{&1}")
  end

The problem is that build attempts to merge a map so you have to do factory(:mandrill_id). I think it would be better if you could use build so it matches with the other factories

EDIT: This might be a bad idea. Might be better to just create a function called mandrill_id that uses the sequence. Problem solved. Though maybe there's another use case for wanting to build something other than a map or keyword list?

Association being inserted again by create()

I'm trying to use ExMachina alongside Ecto to test some code with some models with lots of relations, but I keep running into an error where create() tries to insert a relation a second time.

Here's what my test code looks like so far:

def factory(:contact) do
  %Contact{
  }
end

def factory(:contact_profile) do
  %ContactProfile{
    city: "Plano",
    region: "TX",
  }
end

contact_profile = create(:contact_profile)
contact = create(:contact, contact_profile: contact_profile)

After the last line, I keep getting this error:

     ** (Ecto.ConstraintError) constraint error when attempting to insert model:

         * unique: contact_profiles_pkey

As far as the model goes, the ContactProfile model has a belongs_to relationship with Contact, and Contact has a has_one relationship with ContactProfile.

Clarify what the `attrs` parameter is and how it's used

I just got done with a hangout with a few other Elixir/Phoenix users. One thing that was confusing to them was what the attrs are for. Since the example has them being used for assocs, it seems like that's all you could use them for. It is also unclear at what point the attrs are passed in, and what they are.

I think we should explain that the attrs are what you pass in as the second option to the build/create functions. Also maybe an example that uses attrs to build an email address from first and last name or something?

Raise helpful error if you accidentally call insert twice

I accidentally did this:

appointment_booking = create(:appointment_booking)
  |> set_appointment_price(500)
  |> insert

And got this error:

* (Ecto.ConstraintError) constraint error when attempting to insert model:

         * unique: appointment_booking_pkey

     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.

I think it would be helpful if it said something like:

you called `insert` on #{the record} but it was already saved. You can only call insert on records that haven't been inserted yet

Or something along those lines. This is pretty low priority, but might be nice to have

Mix test function definition has an extra colon

When running mix test I see the following error:

     ** (ExMachina.UndefinedFactoryError) No factory defined for :user.

     Please check for typos or define your factory:

         def :user_factory do
           ...
         end

Notice that it should be user_factory, not :user_factory.

Pass the factory module and factory name to the options in custom strategies

That way people can do things with the module. For example, you could create an Ecto strategy that let's you use a changeset like in #78

Also convert keyword lists to map so they part of the map can be matched. Keyword lists require a full match

defmodule ExMachina.EctoWithChangesetStrategy do
  use ExMachina.Strategy, function_name: :insert

  def handle_insert(record, %{repo: repo, factory_module: module, factory_name: factory_name}) do
    changeset_function_name = to_string(factory_name) <> "_changeset" |> String.to_atom
    if Code.ensure_loaded?(module) && function_exported(module, changeset_function_name, 1) do
      apply(module, changset_function_name, record) |> repo.insert!
    else
      repo.insert! record
    end
  end
end

remove nil values from params

If we don't specify some fields, these fields are nil value on the map returned by params_for/2.
Most of the time we don't want to put nil into database. So wy not just remove them ?

params
  |> Enum.filter(fn {_, v} -> v != nil end)
  |> Enum.into(%{})

Ecto 1.1 Changes a bunch of things

The breaking change is is Ecto.Association.loaded? is no longer a function, but they've added Ecto.assoc_loaded? as a public function.

There are some deprecation warnings too from changes in the handling of assoc, embed, etc inside of changesets is different now and causes warnings in ex_machina right now.

.warning: changing assocs with change/2 or put_change/3 is deprecated, please use put_assoc/3 instead
    (stdlib) lists.erl:1262: :lists.foldl/3
    (ecto) lib/ecto/changeset.ex:247: Ecto.Changeset.change/2
    (ex_machina) lib/ex_machina/ecto.ex:128: ExMachina.Ecto.save_record/3
    test/ex_machina/ecto_has_many_test.exs:77: ExMachina.EctoHasManyTest."test create/2 saves overriden `has_many` associations"/1
.warning: changing assocs with change/2 or put_change/3 is deprecated, please use put_assoc/3 instead
    (stdlib) lists.erl:1262: :lists.foldl/3
    (ecto) lib/ecto/changeset.ex:247: Ecto.Changeset.change/2
    (ex_machina) lib/ex_machina/ecto.ex:128: ExMachina.Ecto.save_record/3
    test/ex_machina/ecto_has_many_test.exs:85: ExMachina.EctoHasManyTest."test create/1 creates a record without any associations"/1
.warning: changing assocs with change/2 or put_change/3 is deprecated, please use put_assoc/3 instead
    (stdlib) lists.erl:1262: :lists.foldl/3
    (ecto) lib/ecto/changeset.ex:247: Ecto.Changeset.change/2
    (ex_machina) lib/ex_machina/ecto.ex:128: ExMachina.Ecto.save_record/3
    test/ex_machina/ecto_has_many_test.exs:67: ExMachina.EctoHasManyTest."test create/1 saves `has_many` records defined in the factory"/1
.warning: changing assocs with change/2 or put_change/3 is deprecated, please use put_assoc/3 instead
    (stdlib) lists.erl:1262: :lists.foldl/3
    (ecto) lib/ecto/changeset.ex:247: Ecto.Changeset.change/2
    (ex_machina) lib/ex_machina/ecto.ex:128: ExMachina.Ecto.save_record/3
    (ex_machina) lib/ex_machina/ecto.ex:142: anonymous fn/3 in ExMachina.Ecto.persist_belongs_to_associations/2
.warning: changing embeds with change/2 or put_change/3 is deprecated, please use put_embed/3 instead
    (stdlib) lists.erl:1262: :lists.foldl/3
    (ecto) lib/ecto/changeset.ex:247: Ecto.Changeset.change/2
    (ex_machina) lib/ex_machina/ecto.ex:128: ExMachina.Ecto.save_record/3
    test/ex_machina/ecto_embeds_test.exs:42: ExMachina.EctoEmbedsTest."test create/1 saves `embeds_one` record defined in the factory"/1
.warning: changing embeds with change/2 or put_change/3 is deprecated, please use put_embed/3 instead
    (stdlib) lists.erl:1262: :lists.foldl/3
    (ecto) lib/ecto/changeset.ex:247: Ecto.Changeset.change/2
    (ex_machina) lib/ex_machina/ecto.ex:128: ExMachina.Ecto.save_record/3
    test/ex_machina/ecto_embeds_test.exs:50: ExMachina.EctoEmbedsTest."test create/2 saves `embeds_one` record when overridden"/1

Lazy attributes?

What do y'all think about adding support for "lazy" attributes using functions?

I've got an implementation ready to go. This commit explains everything and includes some examples that should justify why I think this is useful:

moklett@2bc2fe7

Here's a little example:

def hero_factory do
  %{
    creature: "Bat",
    nickname: &"#{&1.creature}man",
    vehicle: &"#{&1.creature}mobile"
  }
end

build(:hero)
# => %{creature: "Bat", nickname: "Batman", vehicle: "Batmobile"}

build(:hero, creature: "Spider", vehicle: "webs")
# => %{creature: "Spider", nickname: "Spiderman", vehicle: "webs"}

Should I open a pull request?

Thanks!

Don't reproduce FactoryGirl please

Hello,

Let's get one thing straight - FactoryGirl is a wicked and brittle: instead of encouraging PORO usage someone decided to take metaprogramming homework onto the next level and created a DSL that merely resemble the Rails story: it's cool for simple things but requires to learn a language inside a language to approach to the complexity of real-world applications. And that doesn't even guarantee to cover answer all the questions.
Why should developers spend time to learn a dialect if they already knows Ruby?
Too much of magic kills all the fun (beyond the certain point), and I believe you as a maintainers of the FC know that better then anyone else.
Please, do not repeat this story with Elixir, do not abuse metaprogramming - use it point-wise and stand by explicitness.
Thanks for all your work and contribution in open-source.

Change save_record to create

Instead of having a separate save_record function we could probably just make it the create function and have a default create that raises, similar to what we already do for save_record. I think this is easier to understand conceptually. You add a create function to handle create calls. Pretty simple

Sometimes dropping the primary key isn't desired

Found this problem using params_for, etc.

defp drop_ecto_fields(record = %{__struct__: struct, __meta__: %{__struct__: Ecto.Schema.Metadata}}) do
    ...
    |> Map.drop(struct.__schema__(:primary_key))

In the simple case the primary key is a synthetic id that is auto-generated by the database. In the not so simple case, you have overridden the primary key so that it's not auto-generated by the database. It could be a business key that is meaningful. It could be a one-to-one mapping where the primary key is a foreign key reference to another table.

e.g.

# This has a normal auto-incrementing ID
schema "plays" do
  has_one :scoring
end

# This is a one-to-one modeled in the database by having the PK be a FK to the plays.id column.
@primary_key {:play_id, :integer, []}
schema "scoring" do
  field :type, :string
  field :points, :integer

  belongs_to :play, MyApp.Play, define_field: false, references: :id
  belongs_to :team, MyApp.Team, references: :id
end

In the latter case, the primary key is also a foreign key reference to another table and is a required field, so using params_for and friends creates invalid params since it drops the PK.

Change `fields_for` to `params_for`

I often use fields_for when creating or updating records in API endpoints or for testing controllers. I think params_for makes more sense in these cases.

post comment_path(conn, :create), params_for(:comment)

`Create` fails for abstract / polymorphic tables

Using polymorphic associations and passing the assoc_id to build/2 allows me to build records, but create fails as its trying to build with the abstract table name rather than the concrete one. The fun thing is, I can manually pipe whatever was built into a Repo.insert! and my insertion is successful.

# factory
  def factory(:vendor_image) do
    name = sequence(:filename, &"file-#{&1}.jpg")
    build_assoc(%Vendor{}, :images, [
      filename: name,
      alt: "Caption text for #{name}",
      medium: "priv/images/medium-#{name}",
      original: "priv/images/original-#{name}"
    ])
  end
# usage..
v = Factory.create(:vendor)

# fails, because its attempting to insert into the abstract table
i = Factory.create(:vendor_image, assoc_id: v.id)
# succeeds 
i = Factory.build(:vendor_image, assoc_id: v.id) |> Repo.insert!

Create a file for each factory.

When app is growing and models amount is growing too, it's simpler just to have a bunch of files, one for each model, like rails factory_girl gem does.

But it looks like this gem doesn't either ability to create factories in separate files and documentation. WDYT?

Mismatch between readme usage instruction in master and current release version

Hi,
I tried to work by the instructions. If I am correct, the readme refers to the new way of declaring factory methods, and the current version specified by the readme - 0.6.1 - does not yet include that code.

Readme:

 def user_factory do
    %User{
      name: "Jane Smith",
      email: sequence(:email, &"email-#{&1}@example.com"),
    }
  end

build(:comment, attrs)

Current release code in 0.6.1 ex_machina.ex:

def build(module, factory_name, attrs \\ %{}) do
    attrs = Enum.into(attrs, %{})
    module.factory(factory_name) |> Map.merge(attrs)
  end

New code in ex_machina.ex:
https://github.com/thoughtbot/ex_machina/blob/master/lib/ex_machina.ex#L98

def build(module, factory_name, attrs \\ %{}) do
    attrs = Enum.into(attrs, %{})
    function_name = Atom.to_string(factory_name) <> "_factory" |> String.to_atom
    if Code.ensure_loaded?(module) && function_exported?(module, function_name, 0) do
      apply(module, function_name, []) |> do_merge(attrs)
    else
      raise UndefinedFactoryError, factory_name
    end
  end

Pattern for handling `has_one` and `has_many` associations

Is there a way to handle creation of has_one and has_many associations? Namely, to have something like:

def factory(:article) do
  %Article{
    title: "My Awesome Article",
    comments: %Comment{
      body: "I will be always here!"
    }
  }
end

and to create it

article_with_comment = create(:article)

Current, if I try to do this I get error ... Assocs can only be manipulated via changesets, be it on insert, update or delete. when Repo.insert! is called.

If instead model to insert is converted to changeset first, associations are inserted as well,

%{__struct__: model} = record
struct(model) |> Ecto.Changeset.change(drop_ecto_fields(record)) |> repo.insert!

Not sure if this is the right approach though...

fields_for/params_for and belongs_to associations

fields_for (now params_for) doesn't populate the IDs of relations when you build them.

Imagine you're modeling a play in a sport...

  schema "plays" do
    belongs_to :game, Namespace.Game
    belongs_to :possession_team, Namespace.Team
...
  end

With

def factory(:play) do
  %Namespace.Play{
    game: create(:game),
    possession_team: create(:team),
  }
end

When you do fields_for(:play) game_id and possession_team_id are nil. It makes it harder to use for sending data to an API, for example. It would be nice if those could be populated.

Technically when you do build(:play) the same is true. It seems like Ecto doesn't populate those IDs until after it's saved?

"Global" Sequences Unexpected

This is not a bug per se, but just something surprising that we ran into. Maybe it deserves some documentation? Mostly putting it here to get some feedback.

The sequence is a global Agent that stores the sequence globally for a given atom which was unexpected. The sequences for a given factory are not necessarily sequential. In general, depending on the sequences of a type to be sequential is probably bad of course.

This can cause problems though when the size of the field is limited and you have a lot of lookups, you exponentially grow that sequence and can't figure out why.

i.e. You have multiple models/tables that have some of the same columns

  def factory(:lookup_type) do
    %MyModule.LookupType{
      abbreviation: sequence(:abbreviation, &"D#{&1}"),
      name: sequence(:name, &"Name#{&1}"),
    }
  end

  def factory(:another_lookup_type) do
    %MyModule.AnotherLookupType{
      abbreviation: sequence(:abbreviation, &"D#{&1}"),
      name: sequence(:name, &"Name#{&1}"),
    }
  end

I think factory_girl works on a sequence per factory, I guess I expected ex_machina to be the same.

Make it easier to work with Time, Date and DateTime Ecto types

See #54 for why this would be nice.

I think it would be good to add to_date_time, to_date, to_time and corresponding sigil_t sigil_d and sigil_(not_sure_yet) sigils that can be imported and would convert a regular Erlang timestamp to the appropriate Ecto type. I imagine you could do something like this to make it easy to use:

defmodule MyApp.Factory do
  # This would import the functions for use inside the factory
  use ExMachina.Ecto, repo: MyRepo

  # I think this is nice because importing the sigils is optional so there are no accidental collisions
  defmacro __using__(_opts) do
    quote do
      import MyApp.Factory
      # Imports the sigils so you can them in your tests
      import ExMachina.TimeHelpers
    end
  end
end

# In a test

use MyApp.Factory # import the factory and the time helpers
create(:article, inserted_at: ~t(GoodTimes.a_day_ago)) # GoodTimes generates erlang time stamps

Raise a when invalid attributes are passed to `create` / `build` etc. (use struct!/2)

Elixir will raise an error if you try to pass an unknown key when creating a struct like so:

iex(2)> %Foo{invalid: "invalid"}
** (CompileError) iex:3: unknown key :invalid for struct Foo

While not every ExMachina factory generates a struct, some do. When we use build, we convert the passed data to a map which doesn't raise errors for invalid keys. Would it make sense to look for a __struct__ field, and if it's there, try to convert it to a struct to raise any errors?

Elixir 1.2 introduced struct! which might help.

Is there any other way of doing this?

Add ExMachina.with_attrs/1

Recently I had to create a lot of events that all had the same user_id

user_id = 123
create(:event, name: "cancelled", user_id: user_id)
create(:event, name: "started exercise", user_id: user_id)
create(:event, name: "finished exercise", user_id: user_id)

I think it would be nice to have a function that allows you do to something like this

ExMachina.with(user_id: user_id) do
    create(:event, name: "cancelled")
    create(:event, name: "started exercise")
end

I'm not exactly sure how this would work, or if this is the syntax/naming we want, but I think we could come up with something

starting ex_machina

(Newish to elixir, so apologies in advance ๐Ÿ˜ƒ)

The README says to add ex_machina to the list of applications. Doesn't this mean that ex_machina is started in all envs, not just the test? I've seen a few test-only libraries recommend adding a line to test_helper.exs to start the app in tests only. Is this a different use-case?

Make a sequence/1 function that generates a default string

A lot of times I just want a unique value like this:

title: sequence(:title, fn(n) -> "Title #{n}" end)
body: sequence(:comment_body, fn(n) -> "Comment Body #{n}" end)

For simple cases like this I think it would be helpful if you could do something like

title: sequence(:title) #generates "Title #{n}"
body: sequence(:comment_body) #generates "Comment Body #{n}"

Thoughts on this? Would it be better to use Faker or would this be useful?

Factories not being wrapped in changesets?

Thanks so much for making this! I'm very new to Elixir and Phoenix, so I might be wrong. But I discovered something today that makes me think that factories made with the create function are not being wrapped in changesets before hitting the db.

I have a User model with the following changeset definition. The model also has a null: false constraint on encrypted password

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> validate_length(:email, min: 1)
    |> validate_length(:password, min: 1)
    |> validate_confirmation(:password, message: "Password does not match")
    |> unique_constraint(:email, message: "Email has already been taken")
    |> generate_encrypted_password
  end

  defp generate_encrypted_password(changeset) do
    pass = changeset.params["password"]
    put_change(changeset, :encrypted_password, Comeonin.Bcrypt.hashpwsalt(pass))
  end

The User factory:

  def factory(:user) do
    %User{email: "[email protected]", password: "password",
          password_confirmation: "password"}
  end

And a test to test for unique emails:

  test "unique emails" do
    user = create(:user)
    changeset = User.changeset(user, %{email: user.email, password: "password",
                                     password_confirmation: "password"})
    refute changeset.valid?
  end

When I run this test, I get a not_null_violation as the encrypted password field is blank. Dropping IEx.pry into my changeset function indicates the changeset function (and generate_encrypted_password) are not being called by Ex Machina. I tried this for other models in my app with the same result.

When I make a record normally, by creating a changeset then calling Repo.insert things work fine.

The source for ExMachina.Ecto.save_record doesn't appear to use a changeset, though I might be misreading this
https://github.com/thoughtbot/ex_machina/blob/master/lib/ex_machina/ecto.ex#L93

Remove `attrs` as second parameter

I've been thinking about this a lot. It seems attrs can be confusing and tricky to debug (see #54 and #51). I wonder if it would be better to remove it and instead handle associations after build. I imagine it would look something like this:

def factory(:comment) do
  %Comment{
    body: "My Comment",
    author: belongs_to(:author)
  }
end

def belongs_to(key)  do
  %ExMachina.Ecto.BelongsTo{key: key}
end

Then in the build function it could iterate through the keys and if it comes along a %ExMachina.Ecto.BelongsTo struct it would build one. I think this makes factory definition and use a lot more clear, and clean. We'd lose the ability to use attrs inside the factory, but that causes bugs in a lot of cases anyway (#54 (comment))

So I think it's better to leave the factory definitions as clean and clear as possible and do the transformation later

Make `build` work when also importing Ecto.Model

By default the Phoenix ConnCase imports Ecto.Model. This can lead to ambiguous calls to build. I believe we may be able to solve this by using guards in our function. That ensure the first argument is an atom.

From the Kernel.SpecialForms docs

Ambiguous function/macro names

If two modules A and B are imported and they both contain a foo function with an arity of 1, an error is only emitted if an ambiguous call to foo/1 is actually made; that is, the errors are emitted lazily, not eagerly.

Raise a helpful error letting people know to define factory/1 instead of factory/2

We added an error here: https://github.com/thoughtbot/ex_machina/blob/0.5.0/lib/ex_machina.ex#L212

but that will never be called since internally we are calling factory/1. Instead that error message should be added to https://github.com/thoughtbot/ex_machina/blob/0.5.0/lib/ex_machina.ex#L16 UndefinedFactory. Something like:

"""
No factory defined for #{inspect factory_name}. This may be because you have defined a factory with attrs like this: def factory(:my_factory, attrs)

This has been changed in 0.5.0 to just be: def factory(:my_factory). The assoc/2 function has also been removed. Please define your factory function like this:

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

Add params_with_assocs/2 to ExMachina.Ecto

Based on the issue by @geofflane in #104. I would love help on this if someone has time, if not I hope to tackle this in a few weeks.

  • Add params_with_assocs/2 to ExMachina.Ecto
  • Should accept the same arguments as ExMachina.Ecto.params_for/2
  • It will insert belongs_to relationships from the factory, but not insert the factory itself. e.g. if you call params_with_assocs(:comment) it should insert the comment's belongs_to associations, but not the comment itself.
  • After inserting the associations it should set the association's id. So if a Comment belongs_to a Post, params_with_assocs(:comment) would insert the post and use the post's primary key and put on on the comment.
  • It should also strip out the Ecto associations and other Ecto fields. This will work the same as params_for/2 works currently. https://github.com/thoughtbot/ex_machina/blob/f6285d2033933424d78870e70810acae8885e1fb/lib/ex_machina/ecto.ex#L75
  • You should be able to override associations, e.g. params_with_assocs(:comment, post: build(:post)). If the post is built then it will insert it and set the appropriate foreign key. If the association has already been inserted it will not try to insert it again, and will set the appropriate foreign key.

Proposal to change factory definition

The factory definition syntax right now uses the same function name, but pattern matches on the first argument. This works ok, but it does cause a few problems

  • You can't easily use ctags/symbol view to jump to factories because they are all the same function
  • You can't add functions that are used by an factory in between other factories since they are all the same function and elixir will raise a warning.

I propose that we change the factory definition syntax to {factory_name}_factory. This will also make it so we can add callbacks that are grouped near the factory in the future

defmodule Factory do
  # This is nice because you can add functions for the user factory near it instead of at the bottom
  # Also works better with symbol view so it doesn't show a bunch of `factory` functions.
  def user_factory do
    %User{
      first_name: "Paul",
      last_name: "Smith",
    }
  end

  # You can now define helper functions near the factory that it applies to
  def make_admin(user) do
    assing_to_admin_group(user)
  end

  # In the future we could add callbacks like this. If we keep the current syntax you'd have to put these
  # at the bottom of the file which makes it hard to see what a factory can do
  def after_build_user(user, attrs) do
    email = attrs.first_name <> attrs.last_name <> "@foo.com"
    Map.put_new(user, :email, email)
  end

  def comment_factory do
    %Comment{
      body: "This is my comment"
    }
  end
end

You would still call these with the same function build(:user), etc.

Use do_build/2 and do_create/2 internally so that you can have custom build/create functions

I was thinking about a way to do things after/before create/build or to do intersting things with the attrs if needed. I think it would be awesome to be able to define a custom function like so

def create(:article, attrs \\ []) do
  do_create(:article, attrs) |> with_comment
end

def build(:user, attrs \\ []) do
  email = attrs.first_name <> attrs.last_name <> "@foo.com"
  do_build(:user, Map.put_new(attrs, email: email)) 
end

do_build and do_create would do what create/2 and build/2 do today, and create/build would just call do_create and do_build with the passed in arguments. This would be an easier change and would allow for a very flexible build/create process

How do you handle password hashing?

I have a User model that hashes the user's password when you call User.changeset/2(gist here). How do I create a factory that correctly hashes the password when I do the following?

    MyApp.Factory.build(:user, password: "supersecret")

I'm new to elixir, so if there is a better approach for automatically hashing the password, please let me know.

Contributing / development section in the readme?

Say I want to get the project set up locally, how would I go about doing that?
(I assume it's something simple like mix deps.get && mix test)
Seems like something worth documenting so there is a low barrier to contribute ๐Ÿ˜„

Automatically cast datetimes?

I had(?) to do this:

  def factory(:package, _attrs) do
    {:ok, time} = Ecto.DateTime.cast({{2001, 2, 3}, {4, 5, 6}})

    %Package{
      hex_updated_at: time,
    }

Would be nice if you could just pass a string like "2001-02-03T04:05:06Z" and it would be automatically cast.

Casting happens if you do

Repo.insert! Package.changeset(%Package{}, %{name: "my_package", description: "My package.", hex_updated_at: my_time_string})

for example.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.