Skip to content

Conversation

Munksgaard
Copy link

@Munksgaard Munksgaard commented Jul 19, 2025

This is a WIP PR that adds support for defining "generators", StreamData based functions that allow users to create property tests (using ExUnitProperties), and which overloads the existing build functions to use generators if they exist.

The goal is to allow users to define generators like this:

defmodule MyApp.Factory do
  # without Ecto
  use ExMachina

  def user_generator do
    gen all name <- StreamData.string(:ascii) do
      %MyApp.User{
        name: name
      }
    end
  end
end

And then, in tests, build(:user) would return the first element created from that StreamData stream, while generate(:user) (a new function) would return the stream itself, to be used in property tests. The benefit is that users wouldn't have to both create a ExMachina factory for their regular unit tests and a StreamData generator for their property tests.

Users may define both a generator and a factory (e.g. user_generator and user_factory), in which case the factory will take precedence over the generator when calling build, but the generator will still be used for generate. Using lazy attributes in generators is not supported, because using StreamData solves the same problem.

I am not 100% sure this is useful in its current incarnation, how it should play together with the sequence function or how to test this properly, but some preliminary local testing using another application seemed to indicate that the basic functionality works as expected.

As such, I am not looking to get this merged as is, but to start a conversation about whether something like this is desirable in ExMachina and, if so, how we might bring this PR (or another PR) to a state that could get it merged.

Inspiration: #351 and https://hexdocs.pm/ecto_stream_factory/readme.html

This is a WIP PR that adds support for defining "generators", `StreamData` based
functions that allow users to create property tests (using `ExUnitProperties`),
and which overloads the existing `build` functions to use generators if they
exist.

The goal is to allow users to define generators like this:

```elixir
defmodule MyApp.Factory do
  # without Ecto
  use ExMachina

  def user_generator do
    gen all name <- do
      %MyApp.User{
        name: name
      }
    end
  end
end
```

And then, in tests, `build(:user)` would return the first element created from
that `StreamData` stream, while `generate(:user)` (a new function) would return
the stream itself, to be used in property tests. The benefit is that users
wouldn't have to both create a ExMachina factory for their regular unit tests
and a `StreamData` generator for their property tests.

Users may define both a generator and a factory (e.g. `user_generator` and
`user_factory`), in which case the factory will take precedence over the
generator when calling `build`, but the generator will still be used for
`generate`. Using lazy attributes in generators is not supported, because using
`StreamData` solves the same problem.

I am not 100% sure this is useful in it's current incarnation, how it should
play together with the `sequence` function or how to test this properly, but
some preliminary local testing using another application seemed to indicate that
the basic functionality works as expected.

As such, I am not looking to get this merged as is, but to start a conversation
about whether something like this is desirable in ExMachina and, if so, how we
might bring this PR (or another PR) to a state that could get it merged.

Inspiration: beam-community#351 and
https://hexdocs.pm/ecto_stream_factory/readme.html
@ibarchenkov
Copy link

Hey Philip, thanks for bringing my old idea of marrying StreamData with ExMachina to life!
I'm wondering - wouldn't it be better for ExMachina users and maintainers to isolate the two generation strategies from each other?
For example, we could keep the "static" strategy intact:

defmodule MyApp.Factory do
  use ExMachina.Ecto, repo: MyApp.Repo
  
  def thing_factory do
    ...
  end
end

And introduce a separate module for StreamData-based generators:

defmodule MyApp.Factory do
  use ExMachina.EctoStreamData, repo: MyApp.Repo

  # _factory functions would be forbidden here.
  # sequence/1, sequence/2, and sequence/3 might also be disallowed
  # if they don’t make sense in the context of StreamData
  def thing_generator do
    ...
  end
end

@Munksgaard
Copy link
Author

Hi @ibarchenkov, you're very welcome! Thanks for doing the hard initial work.

Good question. Would that mean that users would have to choose which "strategy" to use? E.g. if I use ExMachina.EctoStreamData, then I cannot define factories for some things, but must define generators for everything?

Also, I guess I'm a little concerned that this could create an explosion of different strategies down the line, one for each combination of "options". Not a huge problem right now, but you'd end up with at least four strategies: ExMachina, ExMachina.Ecto, ExMachina.StreamData and ExMachina.EctoStreamData, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants