Steps for LiveView Testing - could someone sanity check these 5 high level steps?

I am getting ready to write tests for my app. This is completely new to me, so I’m going to first tackle LiveView tests.

I haven’t been able to find a high level summary of the steps to set up the entire test system. I want to make sure I understand the basic steps so that I know I’m headed in the right direction and that I haven’t missed anything.

Based on the articles I’ve read, I’ve pieced together the 5 steps below. Could someone sanity check this to see if my understanding is correct?

1. Set up Test Database:

I’m using PostgresSQL13. I have a my_app_test repository which was created during project setup so I don’t need to run ecto.create. But the repository is currently empty. To populate it with my tables, I need to run: MIX_ENV=test mix ecto.migrate

2. Populate fixed tables using seeds

Some of my tables have fixed data that does not change (e.g. geo data). The tables were populated using seed scripts. So I need to run these same scripts using this command:
MIX_ENV=test mix run priv/repo/seeds.exs

3. Ignore Sandbox Mode for now:

My understanding is that Sandbox Mode allows for concurrent transactional tests and it runs the entire test in a transaction (so nothing gets committed). If I’m understanding the documentation correctly, it uses the dev repository but it kills the transaction at the end so that nothing gets permanently added to the tables. I don’t need concurrent transactional tests and I’d prefer to use the test database, so I will skip this (unless someone says I really should use it). :slight_smile:

4. Set up Factories:

I will need to populate the database with some fake data. I saw this article discussing the advantages of ExMachina and Faker. I’m not entirely sure how I point ExMachina to my test database and not my dev database, but I’m assuming there is a configuration somewhere.

Then I read this page on Ecto Test Factories. It looks pretty straight forward, but a number of articles say that more complex tests will require more complex factories, so it’s better to leverage an existing library like ExMachina.

Any thoughts on this?

5. Write LiveView Tests:

My app is almost entirely in LiveView, so I’m going to focus on just writing LiveView tests. These tests will call functions in the context module and schema, so I don’t see the point of writing separate tests for those.

By focusing tests on my app’s forms and tables, I will be testing the accuracy of all the functions. And since it is using the test database, all the ecto calls and underlying relationships will also be verified. If it doesn’t display the expected output … then something has gone wrong.

Am I missing anything major by taking this approach?

My resources for writing these tests are the Pragmatic Studio course, German Valasco’s Testing LiveView course and the book Texting Elixir by Andrea Leopardi and Jeffrey Matthias. Any other resource recommendations would be welcomed! :slight_smile:

Does this all sound correct? Have I missed anything that I should research before I set up my test environment?

The target database is configured via the :database key and Ecto.Adapters.SQL.Sandbox is configured via the :pool key.

import Config

config :my_app, MyApp.Repo,
  username: "postgres",
  password: "postgres",
  database: "myapp_test",
  hostname: "localhost",
  pool: Ecto.Adapters.SQL.Sandbox

source: Testing with Ecto — Ecto v3.10.1

Generally speaking, mix test uses the test repo in Phoenix apps created via mix since:

Mix will default to the :dev environment, except for the test task that will default to the :test environment.
Introduction to Mix - The Elixir programming language

This means that the config/config.exs file generated by default will import the config/test.exs file courtesy of the last line import_config "#{config_env()}.exs" which then configures the database to be the "myapp_test..." database.


Thank you @codeanpeace. Do I need to use Sandbox if I’m not planning to run concurrent tests? And do you just use Ecto Test Factories to create factories?

The better question is why you run concurrent tests? In general we disable concurrency only for specific test or test module. Disabling concurrency completely would definitely slow down testing at least few times. Thus it’s not recommended unless you have a really good reason.

1 Like

It’s a bad practice. In general (as always there are exceptions) instead of seeding the database we create a fixtures since we also want to test creation and insertion of data.

1 Like

Thank you @Eiji. Based on your comments and the links that @codeanpeace sent, I’m realizing that I need to use Sandbox. I thought it was only used for complex systems, but I’m realizing now that it should always be used.

I was only planning to run the seeds script for the 3 tables that are static. The data never changes once the tables are populated. I use the same seed scripts to set up the dev database. That said, the seeds use the SomeModule.create_blah function. I’ll be sure to write a test for that one function.

Only for complex systems? No, since they are used in generators.
Literally always? No, only in most cases.

Pay attention that every default setup in Elixir/Phoenix is good for generic stuff (blog, phx gnerators etc.). Concurrent tests as same as PostgreSQL are defaults, but we have an option to change them (database) or turn them off (concurrency) depending on our use case.

SQLite may be better for a personal web apps where you access something via localhost. In that case you do not need thousands of PostgreSQL features, but instead you may want to quickly copy a database which is in just one single file …

In that case I’m not sure if database is good for them. If they are static (i.e. you do not change them) then you should read such data from somewhere at compile-time (like configuration) and keep in memory (unless there is lots of data).

Maybe meta-programming with pattern-matching would be good here? That’s because all the data is kept only in compile-time and you can quickly access it on runtime without taking lots of memory. In exchange all you need to keep in mind is that the generated app/release is bigger and you need to store said file somewhere in priv directory.

defmodule Example do
  # data = read_data_from_file(file_path)
  for %{"id" => id} = item <- data do
    def get_item(unquote(id)), do: unquote(data)