We practice test-first development by starting with a feature description before writing any implementation code. Features are described in plain language using Gherkin syntax, making them easy to understand and review.
- Write a Feature File
Create a file in test/features/ (e.g., calculator.feature):
Feature: Calculator
Scenario: Adding two numbers
Given I have entered 5 into the calculator
And I have entered 7 into the calculator
When I press add
Then the result should be 12- Write a Test Module
Create a test file in test/lib/ (e.g., calculator_test.exs):
defmodule CalculatorTest do
use Cucumber, feature: "calculator.feature"
defstep "I have entered {int} into the calculator", context do
number = List.first(context.args)
numbers = Map.get(context, :numbers, [])
{:ok, %{numbers: numbers ++ [number]}}
end
defstep "I press add", context do
result = Enum.sum(context.numbers)
{:ok, %{result: result}}
end
defstep "the result should be {int}", context do
expected = List.first(context.args)
assert context.result == expected
:ok
end
endFeature files are the single source of truth for new functionality. Each feature file should:
- Be saved in
test/features/as{feature_name}.feature. - Use clear Gherkin syntax (
Feature,Scenario,Given,When,Then). - Cover all relevant scenarios, including both success and error cases.
- Describe what the user will experience in each scenario, including edge cases and validation errors.
- Be updated as understanding evolves—feature files are living documents.
Feature: User Login
Scenario: Successful login
Given the user is on the login page
When the user enters valid credentials
Then the user is redirected to the dashboard
Scenario: Login with invalid password
Given the user is on the login page
When the user enters an incorrect password
Then an error message is displayed
Scenario: Login with missing fields
Given the user is on the login page
When the user submits the form without entering credentials
Then a validation error is shown- All user interactions are described
- Both success and error scenarios are included
- Edge cases and validation are covered
- User experience is clear in each scenario
- Gherkin syntax is used consistently
- Run the Tests
Run your tests as usual:
mix test
This approach ensures that every feature starts with a clear specification and is always covered by automated tests.
Create reusable test fixtures to set up test data consistently across test scenarios:
- Place fixtures in
test/support/directory - Create specific fixture modules for each domain concept (e.g.,
SoireeFixture) - Design fixtures to be idempotent (can be run multiple times without side effects)
- Make fixtures flexible with optional parameters for different test needs
Example fixture:
defmodule Huddlz.SoireeFixture do
def create_sample_soirees(count \\ 3) do
# Create test host with consistent email for lookup
host = get_or_create_test_host("test.host@example.com")
# Create soirées with sequential information
for i <- 1..count do
create_soiree(%{
title: "Test Soirée #{i}",
host_id: host.id
})
end
end
endFor more complex data needs, create generators that produce realistic test data:
- Use the
Ash.Generatormodule to create test data generators - Place generators in the appropriate domain module:
lib/huddlz/domain/generators/ - Use libraries like
Fakerto create diverse, realistic test data - Make generators customizable with options to override default values
Example generator:
defmodule Huddlz.Soirees.Generators.SoireeGenerator do
use Ash.Generator
def soiree(opts \\ []) do
seed_generator(
%Soiree{
title: sequence(:title, &"Soirée #{&1}"),
description: Faker.Lorem.paragraph(),
starts_at: random_future_date()
},
overrides: opts
)
end
endWhen testing LiveView components:
- Use
PhoenixTestfor all LiveView interactions (imported viaHuddlzWeb.ConnCase) - Test user interactions with
fill_in/3,select/3,click_button/2, andvisit/2 - Verify page content using
assert_has/3andrefute_has/3 - Structure assertions to verify behavior, not implementation
- Use background setup steps to establish a known state
# Basic navigation and assertions
session = conn |> visit("/groups")
assert_has(session, "h1", text: "Groups")
# Form interactions
session
|> fill_in("Name", with: "Book Club")
|> select("Group type", option: "Public")
|> click_button("Create Group")
|> assert_has(".alert", text: "Group created")
# Authentication
conn
|> login(user) # Helper from ConnCase
|> visit("/protected/page")Note: PhoenixTest requires proper labels with for attributes on form inputs. If your forms use placeholders without labels, add visually hidden labels:
<label for="search-query" class="sr-only">Search</label>
<input id="search-query" name="query" placeholder="Search..." />