# ash-testing > Comprehensive guide for testing Ash Framework resources, actions, policies, validations, relationships, calculations, and aggregates. Use when writing tests for Ash resources, testing code interfaces, validating authorization policies, testing changesets and validations, setting up test data with Ash.Generator, mocking external services, or ensuring test isolation with DataCase. Triggers include test files for Ash resources, mentions of Ash.Test, Ash.Generator, policy testing, assert_has_error, or testing Ash actions. - Author: mark-a-12 - Repository: mark-a-12/panels - Version: 20260126021713 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-07 - Source: https://github.com/mark-a-12/panels - Web: https://mule.run/skillshub/@@mark-a-12/panels~ash-testing:20260126021713 --- --- name: ash-testing description: Comprehensive guide for testing Ash Framework resources, actions, policies, validations, relationships, calculations, and aggregates. Use when writing tests for Ash resources, testing code interfaces, validating authorization policies, testing changesets and validations, setting up test data with Ash.Generator, mocking external services, or ensuring test isolation with DataCase. Triggers include test files for Ash resources, mentions of Ash.Test, Ash.Generator, policy testing, assert_has_error, or testing Ash actions. --- # Ash Framework Testing Guide Write robust, maintainable tests for Ash Framework applications by following these patterns and best practices. ## Core Testing Principles 1. **Test through code interfaces** - Use your domain's code interfaces, not raw Ash calls 2. **Use raising versions** - Prefer `!` functions in tests when actions should succeed 3. **Test behavior, not implementation** - Focus on expected outcomes 4. **Isolate tests** - Each test should be independent with fresh data 5. **Use Ash.Generator** - Generate test data declaratively ## Quick Decision Guide | What to Test | Approach | |--------------|----------| | Action success | Call code interface, assert on result | | Validation errors | Use `Ash.Test.assert_has_error/3` | | Policy authorization | Use `can_action_name?/2` functions | | Calculations | Load calculation, assert value | | Aggregates | Load aggregate, assert count/sum/etc | | Relationships | Create related records, load and assert | ## Test Structure Overview ```elixir defmodule MyApp.Blog.PostTest do use MyApp.DataCase, async: true import MyApp.Generator # Your test data generators describe "create_post/2" do test "creates post with valid data" do author = generate(user()) assert {:ok, post} = MyApp.Blog.create_post( %{title: "Hello", body: "World"}, actor: author ) assert post.title == "Hello" assert post.author_id == author.id end test "validates title is required" do author = generate(user()) MyApp.Blog.create_post(%{body: "No title"}, actor: author) |> Ash.Test.assert_has_error(Ash.Error.Invalid, fn error -> error.field == :title end) end end end ``` ## Detailed Guides - **Test Setup & DataCase**: See [references/test-setup.md](references/test-setup.md) - **Test Data Generators**: See [references/test-generators.md](references/test-generators.md) - **Testing Actions**: See [references/testing-actions.md](references/testing-actions.md) - **Testing Policies**: See [references/testing-policies.md](references/testing-policies.md) - **Testing Validations**: See [references/testing-validations.md](references/testing-validations.md) - **Testing Relationships**: See [references/testing-relationships.md](references/testing-relationships.md) - **Testing Calculations & Aggregates**: See [references/testing-calculations.md](references/testing-calculations.md) - **Mocking External Services**: See [references/mocking.md](references/mocking.md) ## Essential Imports ```elixir # In your test module import Ash.Test # assert_has_error, etc. import MyApp.Generator # Your data generators require Ash.Query # For filter macros ``` ## Key Testing Functions ### From `Ash.Test` ```elixir # Assert specific error type and condition Ash.Test.assert_has_error(result, Ash.Error.Invalid, fn error -> error.field == :email && error.message =~ "invalid" end) # Check if changeset/query has errors Ash.Test.has_error?(result, Ash.Error.Forbidden) ``` ### Authorization Checks ```elixir # Generated can_* functions on your domain MyApp.Blog.can_create_post?(user) MyApp.Blog.can_update_post?(user, post) MyApp.Blog.can_delete_post?(user, post, data: post) ``` ### From `Ash.Generator` ```elixir # Generate data from action inputs generate(user()) # Single record generate_many(user(), 5) # Multiple records seed_generator(...) # Direct DB insert action_input(Resource, :action) # Generate valid inputs ``` ## Common Patterns ### Testing Success Cases ```elixir test "creates user successfully" do assert {:ok, user} = MyApp.Accounts.register_user(%{ email: "test@example.com", password: "secure123" }) assert user.email == "test@example.com" end # Or with raising version when it should always work test "admin can delete any post" do admin = generate(user(role: :admin)) post = generate(post()) deleted = MyApp.Blog.delete_post!(post, actor: admin) assert deleted.id == post.id end ``` ### Testing Error Cases ```elixir test "rejects duplicate email" do generate(user(email: "taken@example.com")) MyApp.Accounts.register_user(%{ email: "taken@example.com", password: "password" }) |> Ash.Test.assert_has_error(Ash.Error.Invalid, fn error -> error.field == :email && error.message =~ "already taken" end) end ``` ### Testing Policies ```elixir test "users can only update their own posts" do [author, other] = generate_many(user(), 2) post = generate(post(author: author)) assert MyApp.Blog.can_update_post?(author, post) refute MyApp.Blog.can_update_post?(other, post) end ``` ## Avoiding Common Mistakes ### 1. Use Unique Values in Concurrent Tests ```elixir # BAD - Can cause deadlocks %{email: "test@example.com"} # GOOD - Globally unique %{email: "test-#{System.unique_integer([:positive])}@example.com"} ``` ### 2. Don't Pattern Match on Tuples ```elixir # BAD {:ok, user} = MyApp.Accounts.register_user(params) # GOOD - More informative failures assert {:ok, user} = MyApp.Accounts.register_user(params) # BETTER - For expected success user = MyApp.Accounts.register_user!(params) ``` ### 3. Test Through Code Interfaces ```elixir # BAD - Direct Ash calls user = Ash.create!(MyApp.User, %{name: "Test"}) # GOOD - Code interface user = MyApp.Accounts.create_user!(%{name: "Test"}) ``` ### 4. Handle Async Tests Properly ```elixir # For tests that modify shared state use MyApp.DataCase, async: false # For isolated read tests use MyApp.DataCase, async: true ``` ## Running Tests ```bash # Run all tests mix test # Run with coverage mix test --cover # Run specific test file mix test test/my_app/blog/post_test.exs # Run specific test mix test test/my_app/blog/post_test.exs:42 ``` ## Debugging Tests ```elixir # Inspect changeset errors case MyApp.Blog.create_post(params) do {:ok, post} -> post {:error, error} -> IO.inspect(error, label: "Error") flunk("Expected success") end # Use IEx.pry for debugging require IEx IEx.pry() ```