# WandererApp Test Code Quality Standards This document defines the quality standards and best practices for test code in the WandererApp project. All contributors should follow these standards to maintain a high-quality, maintainable test suite. ## Table of Contents 1. [Test Organization](#test-organization) 2. [Naming Conventions](#naming-conventions) 3. [Test Structure](#test-structure) 4. [Assertions & Expectations](#assertions--expectations) 5. [Test Data Management](#test-data-management) 6. [Mocking & Stubbing](#mocking--stubbing) 7. [Performance Standards](#performance-standards) 8. [Documentation Requirements](#documentation-requirements) 9. [Code Review Checklist](#code-review-checklist) ## Test Organization ### File Structure ``` test/ ├── unit/ # Pure unit tests (no external dependencies) ├── integration/ # Integration tests (may use database, etc.) ├── contract/ # API contract validation tests ├── e2e/ # End-to-end tests (future) └── support/ # Test helpers and utilities ``` ### Module Organization ```elixir defmodule WandererAppWeb.MapAPIControllerTest do # 1. Use statements use WandererAppWeb.ConnCase, async: true # 2. Aliases (alphabetically sorted) alias WandererApp.Api alias WandererApp.Test.Factory # 3. Module attributes @valid_attrs %{name: "Test Map", description: "Test"} @invalid_attrs %{name: nil} # 4. Setup callbacks setup :create_user setup :create_map # 5. Test cases grouped by describe blocks describe "index/2" do # Tests for index action end describe "create/2" do # Tests for create action end # 6. Private helper functions at the bottom defp create_user(_), do: # ... defp create_map(_), do: # ... end ``` ## Naming Conventions ### Test Files - **Pattern**: `{module_name}_test.exs` - **Examples**: - `map_controller_test.exs` - `user_auth_test.exs` - `system_factory_test.exs` ### Test Names - Start with an action verb - Be descriptive but concise - Include the condition and expected outcome - Use consistent terminology ```elixir # ✅ Good test names test "returns user's maps when authenticated" test "creates system with valid attributes" test "returns 404 when map not found" test "broadcasts update to all connected clients" test "rate limits requests after threshold exceeded" # ❌ Bad test names test "test maps" test "it works" test "map creation" test "error" ``` ### Describe Blocks - Use function names for unit tests: `describe "calculate_distance/2"` - Use endpoint paths for API tests: `describe "POST /api/maps/:id/systems"` - Use feature names for integration tests: `describe "user authentication flow"` ## Test Structure ### Standard Test Template ```elixir test "descriptive test name", %{conn: conn, user: user} do # Arrange - Set up test data map = Factory.create_map(%{user_id: user.id}) system_params = build_system_params() # Act - Perform the action conn = post(conn, "/api/maps/#{map.id}/systems", system_params) # Assert - Verify the outcome assert response = json_response(conn, 201) assert response["data"]["id"] assert response["data"]["attributes"]["name"] == system_params["name"] # Additional assertions for side effects assert_broadcast "system:created", %{system: _} assert Repo.get_by(System, name: system_params["name"]) end ``` ### Setup Callbacks ```elixir # Use named setup functions for clarity setup :create_test_user setup :authenticate_connection # Prefer named functions over anonymous functions setup do user = Factory.create_user() {:ok, user: user} end # Better: setup :create_user defp create_user(_) do user = Factory.create_user() {:ok, user: user} end ``` ### Test Isolation - Each test must be independent - Use `async: true` when possible - Clean up after tests using `on_exit` callbacks - Don't rely on test execution order ```elixir setup do # Set up test data file_path = "/tmp/test_#{System.unique_integer()}.txt" File.write!(file_path, "test content") # Ensure cleanup on_exit(fn -> File.rm(file_path) end) {:ok, file_path: file_path} end ``` ## Assertions & Expectations ### Assertion Guidelines ```elixir # ✅ Specific assertions assert user.name == "John Doe" assert length(items) == 3 assert {:ok, %User{} = user} = Api.create_user(attrs) assert %{"data" => %{"id" => ^expected_id}} = json_response(conn, 200) # ❌ Vague assertions assert user assert items != [] assert response ``` ### Pattern Matching in Assertions ```elixir # Use pattern matching for precise assertions assert {:ok, %System{} = system} = Api.create_system(attrs) assert %{ "data" => %{ "type" => "system", "id" => system_id, "attributes" => %{ "name" => "Jita", "security" => security } } } = json_response(conn, 200) # Verify specific fields assert system_id == system.id assert security > 0.5 ``` ### Error Assertions ```elixir # Assert specific errors assert {:error, changeset} = Api.create_user(%{}) assert "can't be blank" in errors_on(changeset).name # For API responses assert %{"errors" => errors} = json_response(conn, 422) assert %{ "status" => "422", "detail" => detail, "source" => %{"pointer" => "/data/attributes/name"} } = hd(errors) ``` ### Async Assertions ```elixir # Use assert_receive for async operations Phoenix.PubSub.subscribe(pubsub, "updates") trigger_async_operation() assert_receive {:update, %{id: ^expected_id}}, 1000 # Use refute_receive to ensure no message refute_receive {:update, _}, 100 ``` ## Test Data Management ### Factory Usage ```elixir # ✅ Good factory usage user = Factory.create_user(%{name: "Test User"}) map = Factory.create_map(%{user_id: user.id}) systems = Factory.create_list(3, :system, map_id: map.id) # Build without persisting attrs = Factory.build(:user) params = Factory.params_for(:system) # Create related data map = Factory.create_map_with_systems(system_count: 5) # ❌ Bad factory usage user = Factory.create_user(%{ id: 123, # Don't set IDs manually inserted_at: yesterday # Let the database handle timestamps }) ``` ### Test Data Principles 1. **Minimal Data**: Create only what's needed for the test 2. **Explicit Relations**: Make relationships clear in test setup 3. **Realistic Data**: Use realistic values, not "test" or "foo" 4. **Unique Data**: Generate unique values to avoid conflicts ```elixir # Generate unique data defp unique_email, do: "user#{System.unique_integer()}@example.com" defp unique_map_name, do: "Map #{System.unique_integer()}" # Use realistic data system_params = %{ "solar_system_id" => 30000142, # Real EVE system ID "name" => "Jita", "security_status" => 0.9, "constellation_id" => 20000020 } ``` ## Mocking & Stubbing ### Mock Guidelines ```elixir # Define mocks in test/support/mocks.ex Mox.defmock(Test.EVEAPIClientMock, for: WandererApp.EVEAPIClient.Behaviour) # In tests, set up expectations setup :verify_on_exit! test "handles EVE API errors gracefully" do # Use expect for required calls Test.EVEAPIClientMock |> expect(:get_character_info, 1, fn character_id -> assert character_id == 123456 {:error, :timeout} end) # Use stub for optional calls Test.LoggerMock |> stub(:error, fn _msg -> :ok end) # Test the behavior assert {:error, :external_service} = Characters.fetch_info(123456) end ``` ### Mocking Best Practices 1. **Mock at boundaries**: Only mock external services, not internal modules 2. **Verify expectations**: Use `verify_on_exit!` to ensure mocks are called 3. **Be specific**: Set specific expectations rather than permissive stubs 4. **Document mocks**: Explain why mocking is necessary ```elixir describe "with external service failures" do setup :verify_on_exit! test "retries failed requests up to 3 times" do # Document the mock scenario # Simulating intermittent network failures Test.HTTPClientMock |> expect(:get, 3, fn _url -> {:error, :timeout} end) assert {:error, :all_retries_failed} = Service.fetch_with_retry(url) end end ``` ## Performance Standards ### Test Execution Time - **Unit tests**: < 10ms per test - **Integration tests**: < 100ms per test - **Contract tests**: < 50ms per test - **Full suite**: < 5 minutes ### Performance Guidelines ```elixir # Tag slow tests @tag :slow test "processes large dataset" do # Test implementation end # Use async when possible use WandererAppWeb.ConnCase, async: true # Optimize database operations setup do # Use database transactions for isolation :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo) # Batch create test data users = Factory.insert_list(10, :user) {:ok, users: users} end # Avoid N+1 queries in tests test "loads associations efficiently" do maps = Map |> preload([:systems, :connections]) |> Repo.all() # Assertions... end ``` ### Resource Usage ```elixir # Clean up resources test "processes file uploads" do path = "/tmp/test_upload_#{System.unique_integer()}.txt" on_exit(fn -> File.rm(path) end) # Test implementation end # Limit concurrent resources @tag max_concurrency: 5 test "handles concurrent requests" do # Test implementation end ``` ## Documentation Requirements ### Test Documentation ```elixir defmodule WandererAppWeb.AuthenticationTest do @moduledoc """ Tests for authentication and authorization flows. These tests cover: - User login/logout - API key authentication - Permission checking - Session management """ describe "POST /api/login" do @tag :auth test "returns JWT token with valid credentials" do # When testing authentication endpoints, we need to ensure # the token contains proper claims and expiration user = Factory.create_user() conn = post(conn, "/api/login", %{ "username" => user.username, "password" => "valid_password" }) assert %{"token" => token} = json_response(conn, 200) assert {:ok, claims} = verify_token(token) assert claims["sub"] == user.id end end end ``` ### Complex Test Documentation ```elixir test "handles race condition in concurrent map updates" do # This test verifies that our optimistic locking prevents # lost updates when multiple clients update the same map # simultaneously. We simulate this by: # 1. Loading the same map in two connections # 2. Making different updates # 3. Verifying that the second update fails with 409 map = Factory.create_map() # Client 1 loads the map conn1 = get(conn, "/api/maps/#{map.id}") version1 = json_response(conn1, 200)["data"]["version"] # Client 2 loads the map conn2 = get(conn, "/api/maps/#{map.id}") version2 = json_response(conn2, 200)["data"]["version"] # Client 1 updates successfully conn1 = put(conn1, "/api/maps/#{map.id}", %{ "version" => version1, "name" => "Updated by Client 1" }) assert json_response(conn1, 200) # Client 2's update should fail conn2 = put(conn2, "/api/maps/#{map.id}", %{ "version" => version2, "name" => "Updated by Client 2" }) assert json_response(conn2, 409)["errors"]["detail"] =~ "conflict" end ``` ## Code Review Checklist ### Before Submitting Tests - [ ] All tests pass locally - [ ] Tests are properly isolated (can run individually) - [ ] No hardcoded values or magic numbers - [ ] Descriptive test names following conventions - [ ] Appropriate use of `async: true` - [ ] Factory usage follows guidelines - [ ] Mocks are properly verified - [ ] No flaky tests (run multiple times to verify) - [ ] Performance is acceptable (< 100ms for most tests) - [ ] Complex tests have documentation - [ ] Setup/teardown is clean and complete - [ ] Assertions are specific and meaningful - [ ] Error cases are tested - [ ] Edge cases are covered ### Review Points 1. **Test Coverage** - Are all code paths tested? - Are error conditions handled? - Are edge cases covered? 2. **Test Quality** - Are tests readable and understandable? - Do test names clearly describe what's tested? - Are assertions specific enough? 3. **Test Maintainability** - Will these tests be stable over time? - Are they resilient to small implementation changes? - Do they use appropriate abstractions? 4. **Performance Impact** - Do tests run quickly? - Is database usage optimized? - Are external calls properly mocked? ### Common Issues to Avoid ```elixir # ❌ Brittle tests that break with small changes test "returns exact JSON structure" do assert json_response(conn, 200) == %{ "data" => %{ "id" => "123", "type" => "user", "attributes" => %{ "name" => "John", "email" => "john@example.com", "created_at" => "2023-01-01T00:00:00Z", "updated_at" => "2023-01-01T00:00:00Z" } } } end # ✅ Flexible tests that check important properties test "returns user data" do response = json_response(conn, 200) assert response["data"]["type"] == "user" assert response["data"]["attributes"]["name"] == "John" assert response["data"]["attributes"]["email"] == "john@example.com" assert response["data"]["attributes"]["created_at"] end # ❌ Tests with race conditions test "updates are processed in order" do spawn(fn -> update_map(map, %{name: "First"}) end) spawn(fn -> update_map(map, %{name: "Second"}) end) Process.sleep(100) assert Repo.get!(Map, map.id).name == "Second" end # ✅ Deterministic tests test "last update wins" do {:ok, _} = update_map(map, %{name: "First"}) {:ok, updated} = update_map(map, %{name: "Second"}) assert updated.name == "Second" assert Repo.get!(Map, map.id).name == "Second" end ``` ## Continuous Improvement ### Metrics to Track 1. **Test Execution Time**: Monitor and optimize slow tests 2. **Flaky Test Rate**: Identify and fix unstable tests 3. **Coverage Percentage**: Maintain and improve coverage 4. **Test Maintenance Time**: Reduce time spent fixing tests ### Regular Reviews - Weekly: Review test failures and flaky tests - Monthly: Analyze test performance metrics - Quarterly: Update standards based on lessons learned ### Contributing to Standards These standards are living documentation. To propose changes: 1. Discuss in team meetings or Slack 2. Create a PR with proposed changes 3. Get consensus from team members 4. Update standards and communicate changes --- Remember: Good tests are an investment in code quality and developer productivity. Take the time to write them well.