gophertroll
TDD in Elixir with tests that hit the database
I’m new to Elixir, and I’m wondering if I might be doing something wrong in my tests. Right now, I have only 105 tests, but they take ~30 seconds to run. For TDD, this is much too slow. The tests that seem to be the slowest are those where I need one or more database record to exist in order to test that I can then add other related records. For example, I might need a user so that I can create a bank account for that user, and I need both the user and the bank account in order to add or query for transactions. In one such test set, 9 unit tests are taking 2+ seconds, and in another 15 tests are taking ~3.5 seconds.
I’ve seen other posts that suggest that unit tests in Elixir should be very fast, even when they interact with the database. I’ve also seen some posts about using the async flag, but, based on those posts, I’m confused as to whether that is good or bad when the tests use the database. In Java, I would likely isolate the data access layer so that it could be mocked for testing or replaced with an in-memory database to improve speed, but it seems those techniques are not generally used with Elixir.
Are there some general tips, tricks, or common pitfalls that might help me speed up these tests?
Most Liked
LostKobrakai
Just to add more context: Bcrypt is slow by design. Password hashing needs to be slow to provide the security they give. Usually in tests one would dial down all the knobs, which allow it to be faster, or even replace hashing with a dummy.
idi527
![]()
One way would be to wrap it in a behaviour module
defmodule MyApp.Accounts.PasswordHash do
@callback hash(String.t) :: String.t
@callback verify(String.t, String.t) :: boolean
impl_mod = Application.get_env(MyApp.Accounts.PasswordHash, :impl)
defdelegate hash(value), to: impl_mod.hash(value)
defdelegate verify(value, hash), do: impl_mod.verify(value, hash)
end
and have different implementations for test and dev/prod, where in the former one it would return a string
# this would probably be defined in test/support/
defmodule MyApp.Accounts.PasswordHashDummy do
@behaviour MyApp.Accounts.PasswordHash
def hash(value), do: "hashed_" <> value
def verify(value, hash) do
"hashed_" <> value == hash
end
end
and in the latter – delegate to bcrypt
# defined in lib/
defmodule MyApp.Accounts.PasswordHashDefault do
@behaviour MyApp.Accounts.PasswordHash
defdelegate hash(value), do: Bcrypt.hash_pwd(value)
defdelegate verify(value, hash), do: Bcrypt.verify_pass(value, hash)
end
and the appropriate implementation module would be set in env’s config
# config/config.exs
config MyApp.Accounts.PasswordHash, impl: MyApp.Accounts.PasswordHashDefault
# config/test.exs
config MyApp.Accounts.PasswordHash, impl: MyApp.Accounts.PasswordHashDummy
and the rest of the app would use MyApp.Accounts.PasswordHash.hash/1 and MyApp.Accounts.PasswordHash.verify/2.
sasajuric
Did you try reducing log_rounds in test env, as advised in step 3 of the installation guide? This is what I typically do in all of the projects, and tests are running smooth then. I never had to do this complex mocking of bcrypt.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








