├── test ├── test_helper.exs └── factory_girl_elixir_test.exs ├── .gitignore ├── .travis.yml ├── lib ├── factory_girl_elixir.ex └── factory_girl_elixir │ ├── factory.ex │ └── worker.ex ├── config └── config.exs ├── LICENSE ├── mix.exs └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - 17.0 4 | before_install: 5 | - git clone https://github.com/elixir-lang/elixir 6 | - cd elixir && make && cd .. 7 | before_script: 8 | - export PATH=`pwd`/elixir/bin:$PATH 9 | - mix local.hex --force 10 | - MIX_ENV=test mix do deps.get 11 | script: 12 | - mix test 13 | notifications: 14 | recipients: 15 | - duilio.ruggiero@gmail.com 16 | -------------------------------------------------------------------------------- /lib/factory_girl_elixir.ex: -------------------------------------------------------------------------------- 1 | defmodule FactoryGirlElixir do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | children = [ 10 | # Define workers and child supervisors to be supervised 11 | worker(FactoryGirlElixir.Worker, []) 12 | ] 13 | 14 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 15 | # for other strategies and supported options 16 | opts = [strategy: :one_for_one, name: FactoryGirlElixir.Supervisor] 17 | Supervisor.start_link(children, opts) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies. The Mix.Config module provides functions 3 | # to aid in doing so. 4 | use Mix.Config 5 | 6 | # Note this file is loaded before any dependency and is restricted 7 | # to this project. If another project depends on this project, this 8 | # file won't be loaded nor affect the parent project. 9 | 10 | # Sample configuration: 11 | # 12 | # config :my_dep, 13 | # key: :value, 14 | # limit: 42 15 | 16 | # It is also possible to import configuration files, relative to this 17 | # directory. For example, you can emulate configuration per environment 18 | # by uncommenting the line below and defining dev.exs, test.exs and such. 19 | # Configuration from the imported file will override the ones defined 20 | # here (which is why it is important to import them last). 21 | # 22 | # import_config "#{Mix.env}.exs" 23 | -------------------------------------------------------------------------------- /lib/factory_girl_elixir/factory.ex: -------------------------------------------------------------------------------- 1 | defmodule FactoryGirlElixir.Factory do 2 | defmacro __using__(_) do 3 | quote do 4 | import unquote(__MODULE__) 5 | 6 | def attributes_for(factory, override_attributes \\ []) do 7 | FactoryGirlElixir.Worker.get(factory, override_attributes) 8 | end 9 | 10 | def parametrize(factory) do 11 | Enum.map(factory, ¶metrized/1) 12 | |> Enum.into(%{}) 13 | end 14 | 15 | defp parametrized({key, val}) 16 | when is_atom(key) do 17 | {Atom.to_string(key), val} 18 | end 19 | defp parametrized(attr), do: attr 20 | end 21 | end 22 | 23 | defmacro field(name, value) do 24 | quote do 25 | factory = Module.get_attribute(__MODULE__, :factory) 26 | FactoryGirlElixir.Worker.put(factory, {unquote(name), unquote(value)}) 27 | end 28 | end 29 | 30 | defmacro factory(name, opts \\ [], block) 31 | defmacro factory(name, _opts, [do: block]) do 32 | quote do 33 | @factory unquote(name) 34 | 35 | unquote(block) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Duilio Ruggiero 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule FactoryGirlElixir.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :factory_girl_elixir, 6 | version: "0.1.1", 7 | elixir: "~> 1.0", 8 | description: description, 9 | package: package, 10 | deps: deps] 11 | end 12 | 13 | # Configuration for the OTP application 14 | # 15 | # Type `mix help compile.app` for more information 16 | def application do 17 | [applications: [], 18 | mod: {FactoryGirlElixir, []}] 19 | end 20 | 21 | # Dependencies can be hex.pm packages: 22 | # 23 | # {:mydep, "~> 0.3.0"} 24 | # 25 | # Or git/path repositories: 26 | # 27 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1"} 28 | # 29 | # Type `mix help deps` for more examples and options 30 | defp deps do 31 | [] 32 | end 33 | 34 | defp description do 35 | """ 36 | Minimal implementation of Ruby's factory_girl in Elixir. 37 | """ 38 | end 39 | 40 | defp package do 41 | [contributors: ["Duilio Ruggiero"], 42 | licenses: ["MIT"], 43 | links: %{github: "https://github.com/sinetris/factory_girl_elixir"}] 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/factory_girl_elixir_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FactoryGirlElixirTest do 2 | use ExUnit.Case 3 | 4 | defmodule Factory do 5 | use FactoryGirlElixir.Factory 6 | 7 | factory :user do 8 | field :name, "joe" 9 | field :password, "secret" 10 | field :email, &("foo#{&1}@example.com") 11 | end 12 | 13 | factory :assets do 14 | field :name, "bob" 15 | end 16 | end 17 | 18 | test "factories" do 19 | assert "secret" == Factory.attributes_for(:user).password 20 | assert "bob" == Factory.attributes_for(:assets).name 21 | end 22 | 23 | test "generates a sequence" do 24 | FactoryGirlElixir.Worker.reset 25 | 26 | assert "foo1@example.com" == Factory.attributes_for(:user).email 27 | assert "foo2@example.com" == Factory.attributes_for(:user).email 28 | end 29 | 30 | test "parametrize simple factory" do 31 | parametrized_assets = %{"name" => "bob"} 32 | assert parametrized_assets == Factory.attributes_for(:assets) |> Factory.parametrize 33 | end 34 | 35 | test "parametrize factory with sequence" do 36 | FactoryGirlElixir.Worker.reset 37 | 38 | parametrized_user = %{"email" => "foo1@example.com", "password" => "secret", "name" => "joe"} 39 | assert parametrized_user == Factory.attributes_for(:user) |> Factory.parametrize 40 | end 41 | 42 | test "override attributes" do 43 | FactoryGirlElixir.Worker.reset 44 | 45 | user_attributes = %{email: "foo1@example.com", password: "not-secret", name: "andrea"} 46 | assert user_attributes == Factory.attributes_for(:user, password: "not-secret", name: "andrea") 47 | end 48 | 49 | test "overriding dinamic attributes don't increment counter" do 50 | FactoryGirlElixir.Worker.reset 51 | 52 | user_attributes = %{email: "foo1@example.com", password: "secret", name: "joe"} 53 | assert user_attributes == Factory.attributes_for(:user) 54 | user_attributes = %{email: "user@example.com", password: "secret", name: "joe"} 55 | assert user_attributes == Factory.attributes_for(:user, email: "user@example.com") 56 | user_attributes = %{email: "foo2@example.com", password: "secret", name: "joe"} 57 | assert user_attributes == Factory.attributes_for(:user) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/factory_girl_elixir/worker.ex: -------------------------------------------------------------------------------- 1 | defmodule FactoryGirlElixir.Worker do 2 | use GenServer 3 | 4 | def start_link(_opts \\ []) do 5 | GenServer.start_link(__MODULE__, :ok, [name: :factory_girl]) 6 | end 7 | 8 | ## Server callbacks 9 | def init(_options) do 10 | state = [counters: [], attributes: []] 11 | {:ok, state} 12 | end 13 | 14 | def put(factory, attribute) do 15 | GenServer.call(:factory_girl, {:put, {factory, attribute}}) 16 | end 17 | 18 | def get(factory, override_attributes) do 19 | GenServer.call(:factory_girl, {:get, factory, override_attributes}) 20 | end 21 | 22 | def reset do 23 | GenServer.call(:factory_girl, :reset) 24 | end 25 | 26 | def handle_call(:reset, _from, state) do 27 | attributes = state[:attributes] || [] 28 | 29 | new_state = [counters: [], attributes: attributes] 30 | {:reply, :ok, new_state} 31 | end 32 | 33 | def handle_call({:put, {factory, assign}}, _from, state) do 34 | attributes = Keyword.get(state[:attributes], factory) || [] 35 | 36 | new_state = Keyword.put(state, :attributes, Keyword.put(state[:attributes], factory, [assign|attributes])) 37 | {:reply, :ok, new_state} 38 | end 39 | 40 | def handle_call({:get, factory, override_attributes}, _from, state) do 41 | attributes = Keyword.get(state[:attributes], factory) || [] 42 | attributes = Dict.merge(attributes, override_attributes) 43 | 44 | counters = state[:counters][factory] || [] 45 | {counters, reply} = expand_functions(attributes, counters) 46 | 47 | new_state = Keyword.put(state, :counters, Keyword.put(state[:counters], factory, counters)) 48 | {:reply, reply, new_state} 49 | end 50 | 51 | defp expand_functions(attributes, counters) do 52 | expand_functions(attributes, counters, %{}) 53 | end 54 | 55 | defp expand_functions([], counters, acc), do: {counters, acc} 56 | defp expand_functions([{key, val}|tail], counters, acc) 57 | when is_function(val) do 58 | seq_next = (Keyword.get(counters, key) || 0) + 1 59 | counters = Keyword.put(counters, key, seq_next) 60 | acc = Dict.put(acc, key, val.(seq_next)) 61 | expand_functions(tail, counters, acc) 62 | end 63 | defp expand_functions([{key, val}|tail], counters, acc) do 64 | acc = Dict.put(acc, key, val) 65 | expand_functions(tail, counters, acc) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FactoryGirlElixir 2 | 3 | [![Build Status](https://travis-ci.org/sinetris/factory_girl_elixir.svg?branch=master)](https://travis-ci.org/sinetris/factory_girl_elixir) 4 | 5 | Minimal implementation of Ruby's [factory_girl](http://github.com/thoughtbot/factory_girl) in Elixir. 6 | 7 | This is a rewrite of the [factory_boy](https://github.com/inkr/factory_boy) project to make it work on Elixir v1.0.x. 8 | 9 | ## Usage 10 | 11 | Add FactoryGirlElixir as a dependency in your `mix.exs` file. 12 | 13 | ```elixir 14 | defp deps do 15 | [ 16 | {:factory_girl_elixir, "~> 0.1.1"} 17 | ] 18 | end 19 | ``` 20 | 21 | You should also update your applications list to include both projects: 22 | 23 | ```elixir 24 | def application do 25 | [applications: [:factory_girl_elixir]] 26 | end 27 | ``` 28 | 29 | After you are done, run `mix deps.get` in your shell to fetch the dependencies. 30 | 31 | 32 | ## Defining a Factory 33 | 34 | ```elixir 35 | defmodule Factory do 36 | use FactoryGirlElixir.Factory 37 | 38 | factory :user do 39 | field :password, "secret" 40 | # create a sequence using an anonymous functions 41 | field :username, fn(n) -> 42 | "username#{n}" 43 | end 44 | # or an anonymous functions shortcut 45 | field :email, &("foo#{&1}@example.com") 46 | end 47 | 48 | factory :assets do 49 | field :name, "bob" 50 | end 51 | end 52 | ``` 53 | 54 | Then query the module to get a list of attributes for your record 55 | 56 | ```elixir 57 | user = Factory.attributes_for(:user) #=> %{password: "secret", username: "username1", email: "foo1@example.com"} 58 | parametrized_user = user |> Factory.parametrize 59 | #=> %{"email" => "foo1@example.com", "password" => "secret", "username" => "username1"} 60 | ``` 61 | 62 | You can override attributes 63 | 64 | ```elixir 65 | user = Factory.attributes_for(:user, email: "user@example.org") 66 | #=> %{password: "secret", username: "username1", email: "user@example.org"} 67 | ``` 68 | 69 | ## Contributing 70 | 71 | 1. [Fork](https://help.github.com/articles/fork-a-repo) this repo 72 | 2. Create a topic branch - `git checkout -b my_branch` 73 | 3. Push to your branch - `git push origin my_branch` 74 | 4. Create a [Pull Request](http://help.github.com/pull-requests/) from your 75 | branch 76 | 5. That's it! 77 | 78 | ## Copyright and license 79 | 80 | Copyright (c) 2014 Duilio Ruggiero. Code released under [the MIT license](LICENSE). 81 | --------------------------------------------------------------------------------