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

Allow delayed evaluation of attributes #408

Merged
merged 2 commits into from
Dec 24, 2020

Commits on Dec 12, 2020

  1. Use a real struct (not named struct) in ExMachinaTest

    We used to have a struct factory defined as `%{__struct__: FooBar}` but
    it was confusing since the factory name was `struct_factory`.
    
    So we change to be `foo_bar_factory` and create a module with a struct
    called `FooBar`. This has the benefit of allowing us to assert
    `%FooBar{}` in tests -- something we couldn't do with the previous
    format.
    germsvel committed Dec 12, 2020
    Configuration menu
    Copy the full SHA
    648117c View commit details
    Browse the repository at this point in the history

Commits on Dec 23, 2020

  1. Allow delayed evaluation of attributes

    The problem
    ------------
    
    People often run into this problem: they want a different email per
    account, but they do this:
    
    ```elixir
    build_pair(:account, email: build(:email))
    ```
    
    The problem is that `build/2` is just a function call. So the above is
    equivalent to this:
    
    ```elixir
    email = build(:email)
    build_pair(:account, email: email) # same email
    ```
    
    In other words, we get the same email factory for all of the accounts.
    That's especially confusing if we're using a `sequence` in the `email`
    factory.
    
    The problem is made worse when using it with Ecto. We can imagine the
    following scenario:
    
    ```elixir
    insert_pair(:account, user: build(:user))
    ```
    
    If the user factory has a uniqueness constraint, `insert_pair/2` will
    raise an error because we'll try to insert a user with the same value
    (even if using a sequence).
    
    Solution
    --------
    
    The solution is to delay evaluation of the attributes. We do this
    allowing attributes to be functions.
    
    The trick then lies in the `build/2` function. We make it a terminal
    function in that it will evaluate any lazy attributes recursively. To do
    that, we update the `build/2` function to evaluate function attributes
    after merging any passed-in attributes.
    
    Previous implementations tried to solve the issue of delayed evaluation
    by introducing a `build_lazy/2` function. One of those was a simple
    alias to an anonymous function `fn -> build(:factory_name) end`. The
    other was a more complex approach that introduced a new private struct
    `%ExMachina.InstanceTemplate{}` to hold the data necessary to build the
    instance of that factory.
    
    We opt for the simpler approach because:
    
    - (a) it leaves room for flexibility in the future (we can add something
      like `build_lazy` alias if we want), and
    
    - (b) it opens the door for allowing the parent factory to be passed
      into the anonymous function in a factory definition:
    
    ```elixir
    def account_factory do
      %Account{
        status: fn account -> build(:status, private: account.private) end
      }
    end
    ```
    
    Not interacting with "full-control" factories
    ---------------------------------------------
    
    We opt for not evaluating lazy attributes in "full-control" factories.
    The whole point of allowing users to have full control of their factory
    attributes is for them to do with them what they will.
    
    We do expose a `evaluate_lazy_attributes/1` helper function, just like
    we expose a `merge_attributes/2` function so that users can emulate
    ExMachina's default behavior.
    germsvel committed Dec 23, 2020
    Configuration menu
    Copy the full SHA
    c51ca67 View commit details
    Browse the repository at this point in the history