# Test Resources ```elixir Mix.install([{:ash, "~> 3.0}, {:simple_sat, "~> 0.1"}], consolidate_protocols: false ) Logger.configure(level: :warning) ExUnit.start() ``` ## Introduction We recommend testing your resources *thoroughly*. Often, folks think that testing an `Ash.Resource` is "testing the framework", and in some very simple cases this may be true, like a simple create that just accepts a few attributes. However, testing has two primary roles: 1. Confirming our understanding of the way that our application behaves *now* 2. Ensuring that our application does not change in unintended ways *later* To this end, we highly recommend writing tests even for your simple actions. A single test that confirms that, with simple inputs, the action returns what you expect, can be very powerful. Additionally, Ash offers unique ways of testing individual components of our resources, similar to a unit test. While you don't necessarily need to follow all steps below, we show the various ways you may want to go about testing your resources. ## Testing Resources * Add tests for action inputs using property testing * Add tests for calling action invocation using property testing * Add explicit tests for action inputs/invocation using property testing. * Add tests for our calculations * Test policies "in isolation" using `Ash.can?` (or `Domain.can_*`, provided by code interfaces) ## Examples ```elixir defmodule User do use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets actions do defaults [:read, create: [:admin?]] end attributes do uuid_primary_key :id attribute :admin?, :boolean do allow_nil? false default false end end end defmodule Tweet do use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets, authorizers: [Ash.Policy.Authorizer] attributes do uuid_primary_key :id attribute :text, :string do allow_nil? false constraints max_length: 144 public? true end attribute :hidden?, :boolean do allow_nil? false default false public? true end end calculations do calculate :tweet_length, :integer, expr(string_length(text)) end relationships do belongs_to :user, User, allow_nil?: false end actions do defaults [:read, update: [:text]] create :create do primary? true accept [:text] change relate_actor(:user) end end policies do policy action_type(:read) do description "If a tweet is hidden, only the author can read it. Otherwise, anyone can." authorize_if relates_to_actor_via(:user) forbid_if expr(hidden? == true) authorize_if always() end policy action_type(:create) do description "Anyone can create a tweet" authorize_if always() end policy action_type(:update) do description "Only an admin or the user who tweeted can edit their tweet" authorize_if actor_attribute_equals(:admin?, true) authorize_if relates_to_actor_via(:user) end end end defmodule Domain do use Ash.Domain, validate_config_inclusion?: false resources do resource Tweet do define :create_tweet, action: :create, args: [:text] define :update_tweet, action: :update, args: [:text] define :list_tweets, action: :read end resource User do define :create_user, action: :create end end end ``` ``` {:module, Domain, <<70, 79, 82, 49, 0, 2, 46, ...>>, [ Ash.Domain.Dsl.Resources.Resource, Ash.Domain.Dsl.Resources.Options, Ash.Domain.Dsl, %{opts: [], entities: [...]}, Ash.Domain.Dsl, Ash.Domain.Dsl.Resources.Options, ... ]} ``` ## Write some tests ```elixir defmodule ActionInvocationTest do use ExUnit.Case import ExUnitProperties describe "valid inputs" do # now if our action inputs are invalid when we think they should be valid, we will find out here property "accepts all valid input" do user = Domain.create_user!() check all(input <- Ash.Generator.action_input(Tweet, :create)) do {text, other_inputs} = Map.pop!(input, :text) assert Domain.changeset_to_create_tweet( text, other_inputs, authorize?: false, actor: user ).valid? end end # same as the above, but actually call the action. This tests the underlying action implementation # not just intiail validation property "succeeds on all valid input" do user = Domain.create_user!() check all(input <- Ash.Generator.action_input(Tweet, :create)) do {text, other_inputs} = Map.pop!(input, :text) Domain.create_tweet!(text, other_inputs, authorize?: false, actor: user) end end test "can tweet some specific text, in addition to any other valid inputs" do user = Domain.create_user!() check all( input <- Ash.Generator.action_input(Tweet, :create, %{text: "some specific text"}) ) do {text, other_inputs} = Map.pop!(input, :text) Domain.create_tweet!(text, other_inputs, actor: user) end end end describe "authorization" do test "allows a user to update their own tweet" do user = Domain.create_user!() tweet = Domain.create_tweet!("Hello world!", actor: user) assert Domain.can_update_tweet?(user, tweet, "Goodbye world!") end test "does not allow a user to update someone elses tweet" do user = Domain.create_user!() user2 = Domain.create_user!() tweet = Domain.create_tweet!("Hello world!", actor: user) refute Domain.can_update_tweet?(user2, tweet, "Goodbye world!") end test "allows an admin user to update someone elses tweet" do user = Domain.create_user!() user2 = Domain.create_user!(%{admin?: true}) tweet = Domain.create_tweet!("Hello world!", actor: user) assert Domain.can_update_tweet?(user2, tweet, "Goodbye world!") end end describe "calculations" do test "text length calculation computes the length of the text" do user = Domain.create_user!() tweet = Domain.create_tweet!("Hello world!", actor: user) assert Ash.calculate!(tweet, :tweet_length) == 12 end end end ExUnit.run() ``` ``` ....... Finished in 0.07 seconds (0.00s async, 0.07s sync) 2 properties, 5 tests, 0 failures Randomized with seed 42546 ``` ``` %{total: 7, failures: 0, skipped: 0, excluded: 0} ```