├── test ├── test_helper.exs ├── fixtures │ └── mockable.ex ├── callbacks_test.exs ├── case │ └── case_test.exs ├── mocks_test.exs └── syntax │ └── expect_test.exs ├── .gitignore ├── script └── ci │ ├── docs.sh │ ├── test.sh │ └── prepare.sh ├── mix.lock ├── .travis.yml ├── lib ├── pavlov.ex ├── syntax │ ├── sugar.ex │ └── expect.ex ├── mocks │ ├── matchers │ │ └── messages.ex │ └── matchers.ex ├── utils │ └── memoize.ex ├── mocks.ex ├── callbacks.ex ├── matchers │ └── messages.ex ├── matchers.ex └── case.ex ├── config └── config.exs ├── LICENSE ├── mix.exs └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Pavlov.start 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /docs 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /script/ci/docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export MIX_ENV="docs" 3 | export TRAVIS_PULL_REQUEST="false" 4 | mix deps.get 5 | mix inch.report 6 | -------------------------------------------------------------------------------- /script/ci/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export MIX_ENV="test" 4 | export PATH="$HOME/dependencies/erlang/bin:$HOME/dependencies/elixir/bin:$PATH" 5 | 6 | mix test 7 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "0.1.12"}, 2 | "ex_doc": {:hex, :ex_doc, "0.6.2"}, 3 | "inch_ex": {:hex, :inch_ex, "0.2.3"}, 4 | "meck": {:hex, :meck, "0.8.2"}, 5 | "mock": {:hex, :mock, "0.1.0"}, 6 | "poison": {:hex, :poison, "1.3.0"}} 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | otp_release: 3 | - 17.4 4 | install: 5 | - mix local.hex --force 6 | - mix local.rebar --force 7 | - mix deps.get --only test 8 | script: 9 | - mix test 10 | after_script: 11 | - MIX_ENV=docs mix deps.get 12 | - MIX_ENV=docs mix inch.report 13 | -------------------------------------------------------------------------------- /lib/pavlov.ex: -------------------------------------------------------------------------------- 1 | defmodule Pavlov do 2 | @moduledoc """ 3 | The main Pavlov module. 4 | """ 5 | 6 | @doc """ 7 | Starts execution of the test suites. 8 | """ 9 | def start do 10 | Pavlov.Utils.Memoize.ResultTable.start_link 11 | ExUnit.start [exclude: :pending] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/fixtures/mockable.ex: -------------------------------------------------------------------------------- 1 | defmodule Fixtures.Mockable do 2 | @doc false 3 | def do_something do 4 | {:ok, "did something"} 5 | end 6 | 7 | @doc false 8 | def do_something_else do 9 | {:ok, "did something else"} 10 | end 11 | 12 | @doc false 13 | def do_with_args(arg) do 14 | arg 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /script/ci/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | wget http://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb 3 | sudo dpkg -i erlang-solutions_1.0_all.deb 4 | sudo apt-get update 5 | sudo apt-get install elixir 6 | 7 | # Fetch and compile dependencies and application code (and include testing tools) 8 | export MIX_ENV="test" 9 | cd $HOME/$CIRCLE_PROJECT_REPONAME 10 | mix local.rebar --force 11 | mix local.hex --force 12 | mix do deps.get, deps.compile, compile 13 | -------------------------------------------------------------------------------- /lib/syntax/sugar.ex: -------------------------------------------------------------------------------- 1 | defmodule Pavlov.Syntax.Sugar do 2 | @moduledoc """ 3 | Provides an alternative DSL for BDD methods. 4 | """ 5 | 6 | for describe_alias <- [:context] do 7 | @doc """ 8 | An alias for `describe`. 9 | """ 10 | defmacro unquote(describe_alias)(description, var \\ quote(do: _), contents) do 11 | quote do 12 | Pavlov.Case.describe(unquote(description), unquote(var), unquote(contents)) 13 | end 14 | end 15 | end 16 | 17 | for xdescribe_alias <- [:xcontext] do 18 | @doc """ 19 | An alias for `xdescribe`. 20 | """ 21 | defmacro unquote(xdescribe_alias)(description, var \\ quote(do: _), contents) do 22 | quote do 23 | Pavlov.Case.xdescribe(unquote(description), unquote(var), unquote(contents)) 24 | end 25 | end 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sprout 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/mocks/matchers/messages.ex: -------------------------------------------------------------------------------- 1 | defmodule Pavlov.Mocks.Matchers.Messages do 2 | @moduledoc false 3 | 4 | @doc false 5 | def message_for_matcher(matcher_name, [module, method], :assertion) do 6 | method = inspect method 7 | 8 | case matcher_name do 9 | :have_received -> "Expected #{module} to have received #{method}" 10 | end 11 | end 12 | def message_for_matcher(matcher_name, [module, method, args], :assertion) do 13 | method = inspect method 14 | args = inspect args 15 | 16 | case matcher_name do 17 | :have_received -> "Expected #{module} to have received #{method} with #{args}" 18 | end 19 | end 20 | 21 | @doc false 22 | def message_for_matcher(matcher_name, [module, method], :refutation) do 23 | method = inspect method 24 | 25 | case matcher_name do 26 | :have_received -> "Expected #{module} not to have received #{method}" 27 | end 28 | end 29 | def message_for_matcher(matcher_name, [module, method, args], :refutation) do 30 | method = inspect method 31 | args = inspect args 32 | 33 | case matcher_name do 34 | :have_received -> "Expected #{module} not to have received #{method} with #{args}" 35 | end 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /test/callbacks_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PavlovCallbackTest do 2 | use Pavlov.Case, async: true 3 | import Pavlov.Syntax.Expect 4 | 5 | describe "Callbacks" do 6 | describe "before :each" do 7 | before :each do 8 | {:ok, hello: "world"} 9 | end 10 | 11 | it "runs the callback before the first test", context do 12 | expect context |> to_have_key :hello 13 | expect context[:hello] |> to_eq "world" 14 | end 15 | 16 | it "runs the callback before the second test", context do 17 | expect context |> to_have_key :hello 18 | expect context[:hello] |> to_eq "world" 19 | end 20 | 21 | context "With a Nested context" do 22 | it "runs the callback before the test", context do 23 | expect context |> to_have_key :hello 24 | expect context[:hello] |> to_eq "world" 25 | end 26 | end 27 | end 28 | 29 | describe "before :all" do 30 | before :all do 31 | {:ok, [context: :setup_all]} 32 | end 33 | 34 | it "runs the callback before all tests", context do 35 | expect context[:context] |> to_eq :setup_all 36 | end 37 | 38 | context "With a Nested context" do 39 | it "runs the callback before the test", context do 40 | expect context[:context] |> to_eq :setup_all 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/utils/memoize.ex: -------------------------------------------------------------------------------- 1 | defmodule Pavlov.Utils.Memoize do 2 | @moduledoc false 3 | alias Pavlov.Utils.Memoize.ResultTable 4 | 5 | defmacro defmem(header, do: body) do 6 | { name, _meta, vars } = header 7 | 8 | quote do 9 | def unquote(header) do 10 | case ResultTable.get(unquote(name), unquote(vars)) do 11 | { :hit, result } -> result 12 | :miss -> 13 | result = unquote(body) 14 | ResultTable.put(unquote(name), unquote(vars), result) 15 | result 16 | end 17 | end 18 | end 19 | end 20 | 21 | def flush do 22 | ResultTable.flush 23 | end 24 | 25 | # gen_server keeping results for function calls 26 | defmodule ResultTable do 27 | @moduledoc false 28 | use GenServer 29 | 30 | def start_link do 31 | GenServer.start_link(__MODULE__, HashDict.new, name: :result_table) 32 | end 33 | 34 | def handle_call({ :get, fun, args }, _sender, dict) do 35 | if Dict.has_key?(dict, { fun, args }) do 36 | { :reply, { :hit, dict[{ fun, args }] }, dict } 37 | else 38 | { :reply, :miss, dict } 39 | end 40 | end 41 | def handle_call({ :flush }, _sender, dict) do 42 | { :reply, :hit, Dict.drop(dict, Dict.keys(dict)) } 43 | end 44 | 45 | def handle_cast({ :put, fun, args, result }, dict) do 46 | { :noreply, Dict.put(dict, { fun, args }, result) } 47 | end 48 | 49 | def get(fun, args), do: GenServer.call(:result_table, { :get, fun, args }) 50 | def put(fun, args, result), do: GenServer.cast(:result_table, { :put, fun, args, result }) 51 | def flush, do: GenServer.call(:result_table, { :flush }) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Pavlov.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.2.2" 5 | 6 | def project do 7 | [app: :pavlov, 8 | version: @version, 9 | elixir: "~> 1.0", 10 | deps: deps, 11 | elixirc_paths: paths(Mix.env), 12 | registered: [:pavlov_let_defs, :pavlov_callback_defs], 13 | 14 | # Hex 15 | description: description, 16 | package: package, 17 | 18 | # Docs 19 | name: "Pavlov", 20 | docs: [source_ref: "v#{@version}", 21 | source_url: "https://github.com/sproutapp/pavlov"]] 22 | end 23 | 24 | # Configuration for the OTP application 25 | # 26 | # Type `mix help compile.app` for more information 27 | def application do 28 | [applications: [:logger]] 29 | end 30 | 31 | # Dependencies can be Hex packages: 32 | # 33 | # {:mydep, "~> 0.3.0"} 34 | # 35 | # Or git/path repositories: 36 | # 37 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 38 | # 39 | # Type `mix help deps` for more examples and options 40 | defp deps do 41 | [ 42 | {:meck, "~> 0.8.2"}, 43 | {:ex_doc, "~> 0.6", only: :docs}, 44 | {:earmark, "~> 0.1", only: :docs}, 45 | {:inch_ex, only: :docs} 46 | ] 47 | end 48 | 49 | defp description do 50 | """ 51 | Pavlov is a BDD library for your Elixir projects, allowing you to write 52 | expressive unit tests that tell the story of how your application behaves. 53 | The syntax tries to follow RSpec's wherever possible. 54 | """ 55 | end 56 | 57 | defp package do 58 | [contributors: ["Bruno Abrantes"], 59 | licenses: ["MIT"], 60 | links: %{"GitHub" => "https://github.com/sproutapp/pavlov"}] 61 | end 62 | 63 | defp paths(:test), do: ["lib", "test/fixtures"] 64 | defp paths(_), do: ["lib"] 65 | end 66 | -------------------------------------------------------------------------------- /lib/syntax/expect.ex: -------------------------------------------------------------------------------- 1 | defmodule Pavlov.Syntax.Expect do 2 | @moduledoc """ 3 | Expect syntax for writing better specs. 4 | """ 5 | 6 | import ExUnit.Assertions 7 | import Pavlov.Matchers.Messages 8 | 9 | @doc """ 10 | Sets an expectation on a value. 11 | In this example, `eq` is a typical matcher: 12 | 13 | ## Example 14 | expect(actual) |> to_eq(expected) 15 | """ 16 | def expect(subject) do 17 | subject 18 | end 19 | 20 | # Dynamically defines methods for included matchers, such that: 21 | # For a given matcher `eq`, defines a method `to_eq`. 22 | Enum.each Pavlov.Matchers.__info__(:functions), fn({method, _}) -> 23 | method_name = :"to_#{method}" 24 | 25 | @doc false 26 | def unquote(method_name)(expected, actual \\ nil) do 27 | args = case actual do 28 | nil -> [expected] 29 | _ -> [actual, expected] 30 | end 31 | 32 | result = apply(Pavlov.Matchers, unquote(method), args) 33 | 34 | case result do 35 | false -> flunk message_for_matcher(unquote(:"#{method}"), args, :assertion) 36 | _ -> assert result 37 | end 38 | end 39 | end 40 | 41 | # Dynamically defines methods for included matchers, such that: 42 | # For a given matcher `eq`, defines a method `not_to_eq`. 43 | Enum.each Pavlov.Matchers.__info__(:functions), fn({method, _}) -> 44 | method_name = :"not_to_#{method}" 45 | 46 | @doc false 47 | def unquote(method_name)(expected, actual \\ nil) do 48 | args = case actual do 49 | nil -> [expected] 50 | _ -> [actual, expected] 51 | end 52 | 53 | result = apply(Pavlov.Matchers, unquote(method), args) 54 | 55 | case result do 56 | true -> flunk message_for_matcher(unquote(:"#{method}"), args, :refutation) 57 | _ -> refute result 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/mocks.ex: -------------------------------------------------------------------------------- 1 | defmodule Pavlov.Mocks do 2 | @moduledoc """ 3 | Use this module to mock methods on a given module. 4 | 5 | ## Example 6 | defmodule MySpec do 7 | use Pavlov.Mocks 8 | 9 | describe "My Tests" do 10 | ... 11 | end 12 | end 13 | """ 14 | 15 | alias __MODULE__ 16 | 17 | defstruct module: nil 18 | 19 | import ExUnit.Callbacks 20 | 21 | defmacro __using__(_) do 22 | quote do 23 | import Pavlov.Mocks 24 | import Pavlov.Mocks.Matchers 25 | import Pavlov.Mocks.Matchers.Messages 26 | end 27 | end 28 | 29 | @doc """ 30 | Prepares a module for stubbing. Used in conjunction with 31 | `.to_receive`. 32 | 33 | ## Example 34 | allow MyModule |> to_receive(...) 35 | """ 36 | def allow(module, opts \\ [:no_link]) do 37 | try_unloading module 38 | 39 | :meck.new(module, opts) 40 | 41 | # Unload the module once the test exits 42 | on_exit fn -> 43 | try_unloading module 44 | end 45 | 46 | %Mocks{module: module} 47 | end 48 | 49 | @doc """ 50 | Mocks a function on a module. Used in conjunction with 51 | `allow`. 52 | 53 | ## Example 54 | allow MyModule |> to_receive(get: fn(url) -> "" end) 55 | 56 | For a method that takes no arguments and returns nil, you may use a 57 | simpler syntax: 58 | allow MyModule |> to_receive(:simple_method) 59 | """ 60 | def to_receive(struct = %Mocks{module: module}, mock) when is_list mock do 61 | {mock, value} = hd(mock) 62 | :meck.expect(module, mock, value) 63 | struct 64 | end 65 | def to_receive(struct = %Mocks{module: module}, mock) do 66 | value = fn -> nil end 67 | :meck.expect(module, mock, value) 68 | struct 69 | end 70 | 71 | @doc false 72 | defp try_unloading(module) do 73 | try do 74 | :meck.unload module 75 | rescue 76 | _ -> :ok 77 | end 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /lib/callbacks.ex: -------------------------------------------------------------------------------- 1 | defmodule Pavlov.Callbacks do 2 | @moduledoc """ 3 | Allows running tasks in-between test executions. 4 | Currently only supports running tasks before tests are 5 | executed. 6 | 7 | ## Context 8 | If you return `{:ok, }` from `before :all`, the dictionary will be merged 9 | into the current context and be available in all subsequent setup_all, 10 | setup and the test itself. 11 | 12 | Similarly, returning `{:ok, }` from `before :each`, the dict returned 13 | will be merged into the current context and be available in all subsequent 14 | setup and the test itself. 15 | 16 | Returning `:ok` leaves the context unchanged in both cases. 17 | """ 18 | 19 | import ExUnit.Callbacks 20 | 21 | @doc false 22 | defmacro __using__(opts \\ []) do 23 | quote do 24 | Agent.start(fn -> %{} end, name: :pavlov_callback_defs) 25 | 26 | import Pavlov.Callbacks 27 | end 28 | end 29 | 30 | @doc false 31 | defmacro before(periodicity \\ :each, context \\ quote(do: _), contents) 32 | 33 | @doc """ 34 | Runs before each **test** in the current context is executed or before 35 | **all** tests in the context are executed. 36 | 37 | Example: 38 | before :all do 39 | IO.puts "Test batch started!" 40 | :ok 41 | end 42 | 43 | before :each do 44 | IO.puts "Here comes a new test!" 45 | :ok 46 | end 47 | """ 48 | defmacro before(:each, context, contents) do 49 | quote do 50 | setup unquote(context), do: unquote(contents)[:do] 51 | 52 | Agent.update :pavlov_callback_defs, fn(map) -> 53 | Dict.put_new map, __MODULE__, {:each, unquote(Macro.escape context), unquote(Macro.escape contents[:do])} 54 | end 55 | end 56 | end 57 | defmacro before(:all, context, contents) do 58 | quote do 59 | setup_all unquote(context), do: unquote(contents)[:do] 60 | 61 | Agent.update :pavlov_callback_defs, fn(map) -> 62 | Dict.put_new map, __MODULE__, {:all, unquote(Macro.escape context), unquote(Macro.escape contents[:do])} 63 | end 64 | end 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /lib/matchers/messages.ex: -------------------------------------------------------------------------------- 1 | defmodule Pavlov.Matchers.Messages do 2 | @moduledoc false 3 | 4 | @doc false 5 | def message_for_matcher(matcher_name, [actual, expected], :assertion) do 6 | actual = inspect actual 7 | expected = inspect expected 8 | 9 | case matcher_name do 10 | :eq -> "Expected #{actual} to equal #{expected}" 11 | :have_key -> "Expected #{actual} to have key #{expected}" 12 | :include -> "Expected #{actual} to include #{expected}" 13 | :have_raised -> "Expected function to have raised #{expected}" 14 | :have_thrown -> "Expected function to have thrown #{expected}" 15 | _ -> "Assertion with #{matcher_name} failed: #{actual}, #{expected}" 16 | end 17 | end 18 | def message_for_matcher(matcher_name, [actual], :assertion) do 19 | actual = inspect actual 20 | 21 | case matcher_name do 22 | :be_true -> "Expected #{actual} to be true" 23 | :be_truthy -> "Expected #{actual} to be truthy" 24 | :be_falsey -> "Expected #{actual} to be falsey" 25 | :be_nil -> "Expected #{actual} to be nil" 26 | :be_empty -> "Expected #{actual} to be empty" 27 | :have_exited -> "Expected function to have exited" 28 | _ -> "Assertion with #{matcher_name} failed: #{actual}" 29 | end 30 | end 31 | def message_for_matcher(matcher_name, [actual, expected], :refutation) do 32 | actual = inspect actual 33 | expected = inspect expected 34 | 35 | case matcher_name do 36 | :eq -> "Expected #{actual} not to equal #{expected}" 37 | :have_key -> "Expected #{actual} not to have key #{expected}" 38 | :include -> "Expected #{actual} not to include #{expected}" 39 | :have_raised -> "Expected function not to have raised #{expected}" 40 | :have_thrown -> "Expected function not to have thrown #{expected}" 41 | _ -> "Refutation with #{matcher_name} failed: #{actual}, #{expected}" 42 | end 43 | end 44 | def message_for_matcher(matcher_name, [actual], :refutation) do 45 | actual = inspect actual 46 | 47 | case matcher_name do 48 | :be_true -> "Expected #{actual} not to be true" 49 | :be_truthy -> "Expected #{actual} not to be truthy" 50 | :be_falsey -> "Expected #{actual} not to be falsey" 51 | :be_nil -> "Expected #{actual} not to be nil" 52 | :be_empty -> "Expected #{actual} not to be empty" 53 | :have_exited -> "Expected function not to have exited" 54 | _ -> "Refutation with #{matcher_name} failed: #{actual}" 55 | end 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /lib/mocks/matchers.ex: -------------------------------------------------------------------------------- 1 | defmodule Pavlov.Mocks.Matchers do 2 | @moduledoc """ 3 | Provides matchers for Mocked modules. 4 | """ 5 | 6 | import ExUnit.Assertions 7 | import Pavlov.Mocks.Matchers.Messages 8 | 9 | @doc """ 10 | Asserts whether a method was called with on a mocked module. 11 | Use in conjunction with `with` to perform assertions on the arguments 12 | passed in to the method. 13 | 14 | A negative version `not_to_have_received` is also provided. The same usage 15 | instructions apply. 16 | 17 | ## Example 18 | expect HTTPotion |> to_have_received :get 19 | """ 20 | def to_have_received(module, tuple) when is_tuple(tuple) do 21 | {method, args} = tuple 22 | args = List.flatten [args] 23 | result = _called(module, method, args) 24 | 25 | case result do 26 | false -> flunk message_for_matcher(:have_received, [module, method, args], :assertion) 27 | _ -> assert result 28 | end 29 | end 30 | def to_have_received(module, method) do 31 | result = _called(module, method, []) 32 | 33 | case result do 34 | false -> flunk message_for_matcher(:have_received, [module, method], :assertion) 35 | _ -> assert result 36 | end 37 | end 38 | 39 | @doc false 40 | def not_to_have_received(module, tuple) when is_tuple(tuple) do 41 | {method, args} = tuple 42 | args = List.flatten [args] 43 | result = _called(module, method, args) 44 | 45 | case result do 46 | true -> flunk message_for_matcher(:have_received, [module, method, args], :refutation) 47 | _ -> refute result 48 | end 49 | end 50 | def not_to_have_received(module, method) do 51 | result = _called(module, method, []) 52 | 53 | case result do 54 | true -> flunk message_for_matcher(:have_received, [module, method], :refutation) 55 | _ -> refute result 56 | end 57 | end 58 | 59 | @doc """ 60 | Use in conjunction with `to_have_received` to perform assertions on the 61 | arguments passed in to the given method. 62 | 63 | ## Example 64 | expect HTTPotion |> to_have_received :get |> with "http://example.com" 65 | """ 66 | def with(method, args) do 67 | {method, args} 68 | end 69 | 70 | @doc """ 71 | Asserts whether a method was called when using "Asserts" syntax: 72 | 73 | ## Example 74 | assert called HTTPotion.get("http://example.com") 75 | """ 76 | defmacro called({ {:., _, [ module , f ]} , _, args }) do 77 | quote do 78 | :meck.called unquote(module), unquote(f), unquote(args) 79 | end 80 | end 81 | 82 | defp _called(module, f, args) do 83 | :meck.called module, f, args 84 | end 85 | 86 | end 87 | -------------------------------------------------------------------------------- /test/case/case_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PavlovCaseTest do 2 | use Pavlov.Case, async: true, trace: true 3 | 4 | it "is the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | 8 | xit "doesn't run this test" do 9 | assert false 10 | end 11 | 12 | describe "Context" do 13 | it "allows nesting" do 14 | assert 1 + 1 == 2 15 | end 16 | 17 | context "With multiple levels" do 18 | it "also allows nesting" do 19 | assert 1 + 1 == 2 20 | end 21 | end 22 | end 23 | 24 | describe "Another Context" do 25 | it "allows nesting" do 26 | assert 1 + 1 == 2 27 | end 28 | end 29 | 30 | xdescribe "A skipped describe" do 31 | it "is never run" do 32 | assert 1 + 3 == 2 33 | end 34 | 35 | describe "A normal Context inside a skipped describe" do 36 | it "is never run" do 37 | assert 1 + 3 == 2 38 | end 39 | end 40 | end 41 | 42 | xcontext "A skipped Context" do 43 | it "is never run" do 44 | assert 1 + 3 == 2 45 | end 46 | 47 | context "A normal Context inside a skipped Context" do 48 | it "is never run" do 49 | assert 1 + 3 == 2 50 | end 51 | end 52 | end 53 | 54 | describe ".let" do 55 | setup_all do 56 | Agent.start_link(fn -> 0 end, name: :memoized_let) 57 | :ok 58 | end 59 | 60 | let :something do 61 | Agent.update(:memoized_let, fn acc -> acc + 1 end) 62 | "I am a string" 63 | end 64 | 65 | it "allows lazy definitions" do 66 | fns = __MODULE__.__info__(:functions) 67 | assert fns[:something] != nil 68 | assert something == "I am a string" 69 | end 70 | 71 | it "only invokes the letted block once" do 72 | Agent.update(:memoized_let, fn acc -> 0 end) 73 | something 74 | something 75 | 76 | assert Agent.get(:memoized_let, fn acc -> acc end) == 1 77 | end 78 | 79 | context "Scoping" do 80 | let :outer_something do 81 | "I come from an enclosing context" 82 | end 83 | 84 | let :outer_something_else do 85 | "I also come from an enclosing context" 86 | end 87 | 88 | context "Inner .let" do 89 | let :inner_something do 90 | "I come from a deeper context" 91 | end 92 | 93 | it "allows accessing letted functions from enclosing contexts" do 94 | assert outer_something == "I come from an enclosing context" 95 | assert outer_something_else == "I also come from an enclosing context" 96 | end 97 | 98 | context "An even deeper context" do 99 | let :inner_something do 100 | "I come from this context" 101 | end 102 | 103 | it "allows accessing letted functions from depply enclosing contexts" do 104 | assert outer_something == "I come from an enclosing context" 105 | end 106 | 107 | it "allows redefining a letted function" do 108 | assert inner_something == "I come from this context" 109 | end 110 | end 111 | end 112 | 113 | it "does not leak letted functions from deeper contexts" do 114 | fns = __MODULE__.__info__(:functions) 115 | assert fns[:inner_something] == nil 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/matchers.ex: -------------------------------------------------------------------------------- 1 | defmodule Pavlov.Matchers do 2 | @moduledoc """ 3 | Provides several matcher functions. 4 | Matchers accept up to two values, `actual` and `expected`, 5 | and return a Boolean. 6 | 7 | Using "Expects" syntax, all matchers have positive and negative 8 | forms. For a matcher `eq`, there is a positive `to_eq` and a negative 9 | `not_to_eq` method. 10 | """ 11 | 12 | import ExUnit.Assertions, only: [flunk: 1] 13 | 14 | @type t :: list | map 15 | 16 | @doc """ 17 | Performs an equality test between two values using ==. 18 | 19 | Example: 20 | eq(1, 2) # => false 21 | eq("a", "a") # => true 22 | """ 23 | @spec eq(any, any) :: boolean 24 | def eq(actual, expected) do 25 | actual == expected 26 | end 27 | 28 | @doc """ 29 | Performs an equality test between a given expression and 'true'. 30 | 31 | Example: 32 | be_true(1==1) # => true 33 | be_true("a"=="b") # => false 34 | """ 35 | @spec be_true(any) :: boolean 36 | def be_true(exp) do 37 | exp == true 38 | end 39 | 40 | @doc """ 41 | Performs a truthy check with a given expression. 42 | 43 | Example: 44 | be_truthy(1) # => true 45 | be_truthy("a") # => true 46 | be_truthy(nil) # => false 47 | be_truthy(false) # => false 48 | """ 49 | @spec be_truthy(any) :: boolean 50 | def be_truthy(exp) do 51 | exp 52 | end 53 | 54 | @doc """ 55 | Performs a falsey check with a given expression. 56 | 57 | Example: 58 | be_falsey(1) # => false 59 | be_falsey("a") # => false 60 | be_falsey(nil) # => true 61 | be_falsey(false) # => true 62 | """ 63 | @spec be_falsey(any) :: boolean 64 | def be_falsey(exp) do 65 | !exp 66 | end 67 | 68 | @doc """ 69 | Performs a nil check with a given expression. 70 | 71 | Example: 72 | be_nil(nil) # => true 73 | be_nil("a") # => false 74 | """ 75 | @spec be_nil(any) :: boolean 76 | def be_nil(exp) do 77 | is_nil exp 78 | end 79 | 80 | @doc """ 81 | Performs has_key? operation on a Dict. 82 | 83 | Example: 84 | have_key(%{:a => 1}, :a) # => true 85 | have_key(%{:a => 1}, :b) # => false 86 | """ 87 | @spec have_key(node, t) :: boolean 88 | def have_key(key, dict) do 89 | Dict.has_key? dict, key 90 | end 91 | 92 | @doc """ 93 | Checks if a Dict is empty. 94 | 95 | Example: 96 | be_empty(%{}) # => true 97 | be_empty(%{:a => 1}) # => false 98 | """ 99 | @spec be_empty(t|char_list) :: boolean 100 | def be_empty(list) do 101 | cond do 102 | is_bitstring(list) -> String.length(list) == 0 103 | is_list(list) || is_map(list) -> Enum.empty? list 104 | true -> false 105 | end 106 | end 107 | 108 | @doc """ 109 | Tests whether a given value is part of an array. 110 | 111 | Example: 112 | include([1, 2, 3], 1) # => true 113 | include([1], 2) # => false 114 | """ 115 | @spec include(any, list|char_list) :: boolean 116 | def include(member, list) do 117 | cond do 118 | is_bitstring(list) -> String.contains? list, member 119 | is_list(list) || is_map(list) -> Enum.member? list, member 120 | true -> false 121 | end 122 | end 123 | 124 | @doc """ 125 | Tests whether a given exception was raised. 126 | 127 | Example: 128 | have_raised(fn -> 1 + "test") end, ArithmeticError) # => true 129 | have_raised(fn -> 1 + 2) end, ArithmeticError) # => false 130 | """ 131 | @spec have_raised(any, function) :: boolean 132 | def have_raised(exception, fun) do 133 | raised = try do 134 | fun.() 135 | rescue error -> 136 | stacktrace = System.stacktrace 137 | name = error.__struct__ 138 | 139 | cond do 140 | name == exception -> 141 | error 142 | name == ExUnit.AssertionError -> 143 | reraise(error, stacktrace) 144 | true -> 145 | flunk "Expected exception #{inspect exception} but got #{inspect name} (#{Exception.message(error)})" 146 | end 147 | else 148 | _ -> false 149 | end 150 | end 151 | 152 | @doc """ 153 | Tests whether a given value was thrown. 154 | 155 | Example: 156 | have_thrown(fn -> throw "x" end, "x") # => true 157 | have_thrown(fn -> throw "x" end, "y") # => false 158 | """ 159 | @spec have_thrown(any, function) :: boolean 160 | def have_thrown(expected, fun) do 161 | value = try do 162 | fun.() 163 | catch 164 | x -> x 165 | end 166 | 167 | value == expected 168 | end 169 | 170 | @doc """ 171 | Tests whether the process has exited. 172 | 173 | Example: 174 | have_exited(fn -> exit "x" end) # => true 175 | have_thrown(fn -> :ok end) # => false 176 | """ 177 | @spec have_exited(function) :: boolean 178 | def have_exited(fun) do 179 | exited = try do 180 | fun.() 181 | catch 182 | :exit, _ -> true 183 | end 184 | 185 | case exited do 186 | true -> true 187 | _ -> false 188 | end 189 | end 190 | 191 | end 192 | -------------------------------------------------------------------------------- /test/mocks_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PavlovMocksTest do 2 | use Pavlov.Case, async: true 3 | use Pavlov.Mocks 4 | import Pavlov.Syntax.Expect 5 | import Fixtures.Mockable 6 | 7 | describe "Mocks" do 8 | describe ".to_have_received" do 9 | it "asserts that a simple mock was called" do 10 | allow(Fixtures.Mockable) |> to_receive(do_something: fn -> :error end) 11 | 12 | result = Fixtures.Mockable.do_something() 13 | 14 | expect Fixtures.Mockable |> to_have_received :do_something 15 | expect result |> to_eq(:error) 16 | end 17 | 18 | it "provides a flunk message" do 19 | message = message_for_matcher(:have_received, [Fixtures.Mockable, :do_something], :assertion) 20 | 21 | expect message 22 | |> to_eq "Expected Elixir.Fixtures.Mockable to have received :do_something" 23 | end 24 | end 25 | 26 | describe ".not_to_have_received" do 27 | it "refutes that a mock was called" do 28 | allow(Fixtures.Mockable) |> to_receive(do_something: fn -> :error end) 29 | 30 | expect Fixtures.Mockable |> not_to_have_received :do_something 31 | end 32 | 33 | it "provides a flunk message" do 34 | message = message_for_matcher(:have_received, [Fixtures.Mockable, :do_something], :refutation) 35 | 36 | expect message 37 | |> to_eq "Expected Elixir.Fixtures.Mockable not to have received :do_something" 38 | end 39 | 40 | context "when used with argument expectations" do 41 | it "provides a flunk message" do 42 | message = message_for_matcher(:have_received, [Fixtures.Mockable, :do_something, [1]], :refutation) 43 | 44 | expect message 45 | |> to_eq "Expected Elixir.Fixtures.Mockable not to have received :do_something with [1]" 46 | end 47 | end 48 | end 49 | 50 | it "resets mocks" do 51 | expect Fixtures.Mockable.do_something |> to_eq({:ok, "did something"}) 52 | end 53 | 54 | it "permits chaining to_receive" do 55 | allow(Fixtures.Mockable) 56 | |> to_receive(do_something: fn -> :error end) 57 | |> to_receive(do_something_else: fn -> :success end) 58 | 59 | result = Fixtures.Mockable.do_something() 60 | other_result = Fixtures.Mockable.do_something_else() 61 | 62 | expect Fixtures.Mockable |> to_have_received :do_something 63 | expect result |> to_eq(:error) 64 | 65 | expect Fixtures.Mockable |> to_have_received :do_something_else 66 | expect other_result |> to_eq(:success) 67 | end 68 | 69 | it "doesn't permit the mock to retain other functions in module" do 70 | allow(Fixtures.Mockable) 71 | |> to_receive(do_something: fn -> :success end) 72 | 73 | expect fn -> Fixtures.Mockable.do_something_else end |> to_have_raised UndefinedFunctionError 74 | end 75 | 76 | context "when the :passthrough option is used" do 77 | it "permits the mock to retain other functions in the module" do 78 | allow(Fixtures.Mockable, [:no_link, :passthrough]) 79 | |> to_receive(do_something: fn -> :success end) 80 | 81 | expect fn -> Fixtures.Mockable.do_something_else end |> not_to_have_raised UndefinedFunctionError 82 | end 83 | end 84 | end 85 | 86 | context "Stubbing" do 87 | it "Can return nil when stubbing" do 88 | allow(Fixtures.Mockable) |> to_receive(:do_something) 89 | 90 | result = Fixtures.Mockable.do_something() 91 | 92 | expect Fixtures.Mockable |> to_have_received :do_something 93 | expect result |> to_be_nil 94 | end 95 | 96 | it "allows setting expectations matching method and arguments" do 97 | allow(Fixtures.Mockable) |> to_receive(do_with_args: fn(_) -> :ok end ) 98 | 99 | Fixtures.Mockable.do_with_args("a string") 100 | 101 | expect Fixtures.Mockable |> to_have_received :do_with_args |> with "a string" 102 | end 103 | 104 | it "allows mocking with arguments to return something" do 105 | allow(Fixtures.Mockable) |> to_receive(do_with_args: fn(_) -> :error end ) 106 | 107 | result = Fixtures.Mockable.do_with_args("a string") 108 | 109 | expect Fixtures.Mockable |> to_have_received :do_with_args |> with "a string" 110 | expect result |> to_eq :error 111 | end 112 | 113 | it "provides a flunk message" do 114 | message = message_for_matcher(:have_received, [Fixtures.Mockable, :do_with_args, [1]], :assertion) 115 | 116 | expect message 117 | |> to_eq "Expected Elixir.Fixtures.Mockable to have received :do_with_args with [1]" 118 | end 119 | end 120 | 121 | context "Callbacks" do 122 | before :each do 123 | allow(Fixtures.Mockable) |> to_receive(do_something: fn -> :error end) 124 | :ok 125 | end 126 | 127 | it "supports mocking at setup time" do 128 | result = Fixtures.Mockable.do_something() 129 | 130 | expect Fixtures.Mockable |> to_have_received :do_something 131 | expect result |> to_eq(:error) 132 | end 133 | 134 | context "Across contexts" do 135 | it "supports mocking across contexts" do 136 | result = Fixtures.Mockable.do_something() 137 | 138 | expect Fixtures.Mockable |> to_have_received :do_something 139 | expect result |> to_eq(:error) 140 | end 141 | 142 | context "When an existing mock is re-mocked" do 143 | before :each do 144 | allow(Fixtures.Mockable) |> to_receive(do_something: fn -> :ok end) 145 | :ok 146 | end 147 | 148 | it "uses the re-mock" do 149 | result = Fixtures.Mockable.do_something() 150 | 151 | expect Fixtures.Mockable |> to_have_received :do_something 152 | expect result |> to_eq(:ok) 153 | end 154 | end 155 | end 156 | end 157 | 158 | context "Using asserts syntax" do 159 | describe ".called" do 160 | it "works for simple mocks" do 161 | allow(Fixtures.Mockable) |> to_receive(do_something: fn -> :error end) 162 | 163 | Fixtures.Mockable.do_something() 164 | 165 | assert called Fixtures.Mockable.do_something 166 | end 167 | 168 | it "works for mocks with arguments" do 169 | allow(Fixtures.Mockable) |> to_receive(do_with_args: fn(_) -> :ok end) 170 | 171 | Fixtures.Mockable.do_with_args("a string") 172 | 173 | assert called Fixtures.Mockable.do_with_args("a string") 174 | end 175 | end 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /lib/case.ex: -------------------------------------------------------------------------------- 1 | defmodule Pavlov.Case do 2 | @moduledoc """ 3 | Use this module to prepare other modules for testing. 4 | 5 | ## Example 6 | defmodule MySpec do 7 | use Pavlov.Case 8 | it "always passes" do 9 | assert true 10 | end 11 | end 12 | """ 13 | 14 | @doc false 15 | defmacro __using__(opts \\ []) do 16 | async = Keyword.get(opts, :async, false) 17 | 18 | quote do 19 | use ExUnit.Case, async: unquote(async) 20 | use Pavlov.Callbacks 21 | use Pavlov.Mocks 22 | 23 | @stack [] 24 | @pending false 25 | 26 | Agent.start(fn -> %{} end, name: :pavlov_let_defs) 27 | 28 | import Pavlov.Case 29 | import Pavlov.Syntax.Sugar 30 | end 31 | end 32 | 33 | @doc """ 34 | The cornerstone BDD macro, "it" allows your test to be defined 35 | via a string. 36 | 37 | ## Example 38 | it "is the truth" do 39 | assert true == true 40 | end 41 | """ 42 | defmacro it(desc, var \\ quote(do: _), contents) do 43 | quote do 44 | defit Enum.join(@stack, "") <> unquote(desc), unquote(var), @pending do 45 | unquote(contents) 46 | end 47 | end 48 | end 49 | 50 | @doc """ 51 | Allows you specify a pending test, meaning that it is never run. 52 | 53 | ## Example 54 | xit "is the truth" do 55 | # This will never run 56 | assert true == true 57 | end 58 | """ 59 | defmacro xit(description, var \\ quote(do: _), contents) do 60 | quote do 61 | defit Enum.join(@stack, "") <> unquote(description), unquote(var), true do 62 | unquote(contents) 63 | end 64 | end 65 | end 66 | 67 | @doc """ 68 | You can nest your tests under a descriptive name. 69 | Tests can be infinitely nested. 70 | """ 71 | defmacro describe(desc, _ \\ quote(do: _), pending \\ false, contents) do 72 | quote do 73 | @stack Enum.concat(@stack, [unquote(desc) <> ", "]) 74 | # Closure the old stack so we can use it in defmodule 75 | old_stack = Enum.concat @stack, [] 76 | pending = @pending || unquote(pending) 77 | 78 | # Defines a new module per describe, thus scoping .let 79 | defmodule Module.concat(__MODULE__, unquote(desc)) do 80 | use ExUnit.Case 81 | 82 | @stack old_stack 83 | @pending pending 84 | 85 | # Redefine enclosing let definitions in this module 86 | Agent.get(:pavlov_callback_defs, fn dict -> 87 | Stream.filter dict, fn {module, _name} -> 88 | String.starts_with?("#{__MODULE__}", "#{module}") && "#{__MODULE__}" != "#{module}" 89 | end 90 | end) 91 | |> Stream.map(fn {_module, {periodicity, context, fun}} -> 92 | quote do 93 | use Pavlov.Mocks 94 | before(unquote(periodicity), unquote(context), do: unquote(fun)) 95 | end 96 | end) 97 | |> Enum.each(&Module.eval_quoted(__MODULE__, &1)) 98 | 99 | unquote(contents) 100 | 101 | # Redefine enclosing let definitions in this module 102 | Agent.get(:pavlov_let_defs, fn dict -> 103 | Stream.filter dict, fn {module, lets} -> 104 | String.starts_with? "#{__MODULE__}", "#{module}" 105 | end 106 | end) 107 | |> Stream.flat_map(fn {_module, lets} -> 108 | Stream.map lets, fn ({name, fun}) -> 109 | quote do: let(unquote(name), do: unquote(fun)) 110 | end 111 | end) 112 | |> Enum.each(&Module.eval_quoted(__MODULE__, &1)) 113 | end 114 | 115 | # Cleans context stack 116 | if Enum.count(@stack) > 0 do 117 | @stack Enum.take(@stack, Enum.count(@stack) - 1) 118 | end 119 | end 120 | end 121 | 122 | @doc """ 123 | Defines a group of tests as pending. 124 | Any other contexts nested within an xdescribe will not run 125 | as well. 126 | """ 127 | defmacro xdescribe(desc, _ \\ quote(do: _), contents) do 128 | quote do 129 | describe unquote(desc), _, true, unquote(contents) 130 | end 131 | end 132 | 133 | @doc """ 134 | Allows lazy initialization of subjects for your tests. 135 | Subjects created via "let" will never leak into other 136 | contexts (defined via "describe" or "context"), not even 137 | those who are children of the context where the lazy subject 138 | is defined. 139 | 140 | Example: 141 | let :lazy do 142 | "oh so lazy" 143 | end 144 | 145 | it "lazy initializes" do 146 | assert lazy == "oh so lazy" 147 | end 148 | """ 149 | 150 | defmacro let(name, contents) do 151 | quote do 152 | require Pavlov.Utils.Memoize, as: Memoize 153 | Memoize.defmem unquote(name)(), do: unquote(contents[:do]) 154 | 155 | Agent.update(:pavlov_let_defs, fn(map) -> 156 | new_let = {unquote(Macro.escape name), unquote(Macro.escape contents[:do])} 157 | 158 | Dict.put map, __MODULE__, (map[__MODULE__] || []) ++ [new_let] 159 | end) 160 | end 161 | end 162 | 163 | @doc false 164 | defmacro defit(message, var \\ quote(do: _), pending \\ false, contents) do 165 | contents = 166 | case contents do 167 | [do: _] -> 168 | quote do 169 | unquote(contents) 170 | :ok 171 | end 172 | _ -> 173 | quote do 174 | try(unquote(contents)) 175 | :ok 176 | end 177 | end 178 | 179 | var = Macro.escape(var) 180 | contents = Macro.escape(contents, unquote: true) 181 | 182 | quote bind_quoted: binding do 183 | message = :"#{message}" 184 | Pavlov.Case.__on_definition__(__ENV__, message, pending) 185 | 186 | def unquote(message)(unquote(var)) do 187 | Pavlov.Utils.Memoize.flush 188 | unquote(contents) 189 | end 190 | end 191 | end 192 | 193 | @doc false 194 | def __on_definition__(env, name, pending \\ false) do 195 | mod = env.module 196 | tags = Module.get_attribute(mod, :tag) ++ Module.get_attribute(mod, :moduletag) 197 | if pending do tags = [tags|[:pending]] end 198 | tags = tags |> normalize_tags |> Map.merge(%{line: env.line, file: env.file}) 199 | 200 | Module.put_attribute(mod, :ex_unit_tests, 201 | %ExUnit.Test{name: name, case: mod, tags: tags}) 202 | 203 | Module.delete_attribute(mod, :tag) 204 | end 205 | 206 | defp normalize_tags(tags) do 207 | Enum.reduce Enum.reverse(tags), %{}, fn 208 | tag, acc when is_atom(tag) -> Map.put(acc, tag, true) 209 | tag, acc when is_list(tag) -> Dict.merge(acc, tag) 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pavlov 2 | [![Build Status](https://travis-ci.org/sproutapp/pavlov.svg?branch=master)](https://travis-ci.org/sproutapp/pavlov) 3 | [![Inline docs](http://inch-ci.org/github/sproutapp/pavlov.svg?branch=master&style=flat)](http://inch-ci.org/github/sproutapp/pavlov) 4 | 5 | A BDD framework for your Elixir projects. It's main goal is to provide a rich, expressive syntax for you to develop your unit tests against. Think of it as RSpec's little Elixir-loving brother. 6 | 7 | Pavlov is an abstraction built on top of the excellent ExUnit, Elixir's standard testing library, so all of its standard features are still supported. 8 | 9 | Here's a short and sweet example of Pavlov in action: 10 | 11 | ```elixir 12 | defmodule OrderSpec do 13 | use Pavlov.Case, async: true 14 | import Pavlov.Syntax.Expect 15 | 16 | describe ".sum" do 17 | context "When the Order has items" do 18 | let :order do 19 | %Order{items: [ 20 | {"burger", 10.0} 21 | {"fries", 5.2} 22 | ]} 23 | end 24 | 25 | it "sums the prices of its items" do 26 | expect Order.sum(order) |> to_eq 15.2 27 | end 28 | end 29 | end 30 | end 31 | ``` 32 | 33 | ## Table Of Contents 34 | 35 | - [Usage](#usage) 36 | - [Describe and Context](#describe-and-context) 37 | - [Let](#let) 38 | - [Expects syntax](#expects-syntax) 39 | - [Included Matchers](#included-matchers) 40 | - [Callbacks](#callbacks) 41 | - [before(:each)](#beforeeach) 42 | - [before(:all)](#beforeall) 43 | - [Mocking](#mocking) 44 | - [Mocks with arguments](#mocks-with-arguments) 45 | - [Skipping tests](#skipping-tests) 46 | - [xit](#xit) 47 | - [xdescribe/xcontext](#xdescribexcontext) 48 | - [Development](#development) 49 | - [Running the tests](#running-the-tests) 50 | - [Building the docs](#building-the-docs) 51 | - [Contributing](#contributing) 52 | 53 | ## Usage 54 | Add Pavlov as a dependency in your `mix.exs` file: 55 | 56 | ```elixir 57 | defp deps do 58 | [{:pavlov, ">= 0.1.0", only: :test}] 59 | end 60 | ``` 61 | 62 | After you are done, run `mix deps.get` in your shell to fetch the dependencies. 63 | To start execution of your Pavlov tests, add the following to your 'test/test_helper.exs': 64 | 65 | ```elixir 66 | Pavlov.start 67 | ``` 68 | 69 | Afterwards, running `mix test` in your shell will run all test suites. 70 | 71 | ## Describe and Context 72 | You may use the `describe` and `context` constructs to group tests together in a logical way. Although `context` is just an alias for `describe`, you may use it to add some extra meaning to your tests, ie. you can use `contexts` within a `described` module function to simulate different conditions under which your function should work. 73 | 74 | ## Let 75 | You can use `let` to define memoized helper methods for your tests. The returning value is cached across all invocations. 'let' is lazily-evaluated, meaning that its body is not evaluated until the first time the method is invoked. 76 | 77 | ```elixir 78 | let :order do 79 | %Order{items: [ 80 | {"burger", 10.0} 81 | {"fries", 5.2} 82 | ]} 83 | end 84 | ``` 85 | 86 | ## Expects syntax 87 | 88 | You may use the regular ExUnit `assert` syntax if you wish, but Pavlov includes 89 | an `expect` syntax that makes your tests more readable. 90 | 91 | If you wish to use this syntax, simply import the `Pavlov.Syntax.Expect` at the 92 | beginning of your Test module: 93 | 94 | ```elixir 95 | defmodule MyTest do 96 | use Pavlov.Case, async: true 97 | import Pavlov.Syntax.Expect 98 | #... 99 | end 100 | ``` 101 | 102 | All core matchers are supported under both syntaxes. 103 | 104 | ## Included Matchers 105 | 106 | When using the `expects` syntax, all matchers have negative counterparts, ie: 107 | ```elixir 108 | expect 1 |> not_to_eq 2 109 | expect(1 > 5) |> not_to_be_true 110 | ``` 111 | 112 | Visit the [Pavlov Wiki](https://github.com/sproutapp/pavlov/wiki/Included-Matchers) 113 | to learn more about all of the core matchers available for your tests. 114 | 115 | ## Callbacks 116 | For now, Pavlov only supports callbacks that run before test cases. [ExUnit's 117 | `on_exit` callback](http://elixir-lang.org/docs/stable/ex_unit/ExUnit.Callbacks.html#on_exit/2) is still fully supported though, and may be used normally inside your `before` callbacks. 118 | 119 | ### before(:each) 120 | Runs the specified code before every test case. 121 | 122 | ```elixir 123 | describe "before :each" do 124 | before :each do 125 | IO.puts "A test is about to start" 126 | :ok 127 | end 128 | 129 | it "does something" do 130 | #... 131 | end 132 | 133 | it "does something else" do 134 | #... 135 | end 136 | end 137 | ``` 138 | 139 | In this case, `"A test is about to start"` is printed twice to the console. 140 | 141 | ### before(:all) 142 | Runs the specified code once before any tests run. 143 | 144 | ```elixir 145 | describe "before :all" do 146 | before :all do 147 | IO.puts "This suite is about to run" 148 | :ok 149 | end 150 | 151 | it "does something" do 152 | #... 153 | end 154 | 155 | it "does something else" do 156 | #... 157 | end 158 | end 159 | ``` 160 | In this case, `"This suite is about to run"` is printed once to the console. 161 | 162 | ## Mocking 163 | Pavlov provides facilities to mock functions in your Elixir modules. This is 164 | achieved using [Meck](https://github.com/eproxus/meck), an erlang mocking tool. 165 | 166 | Here's a simple example using [HTTPotion](https://github.com/myfreeweb/httpotion): 167 | 168 | ```elixir 169 | before :each do 170 | allow HTTPotion |> to_receive(get: fn(url) -> "" end) 171 | end 172 | 173 | it "gets a page" do 174 | result = HTTPotion.get("http://example.com") 175 | 176 | expect HTTPotion |> to_have_received :get 177 | expect result |> to_eq "" 178 | end 179 | ``` 180 | 181 | If you want the mock to retain all other functions in the original module, 182 | then you will need to pass the `opts` `List` argument to the `allow` function 183 | and include the `:passthrough` value. The `allow` function specifies a default 184 | `opts` `List` that includes the `:no_link` value. This value should be included 185 | in the `List` as it ensures that the mock (which is linked to the creating 186 | process) will unload automatically when a crash occurs. 187 | 188 | ```elixir 189 | before :each do 190 | allow(HTTPotion, [:no_link, :passthrough]) |> to_receive(get: fn(url) -> "" end) 191 | end 192 | ``` 193 | 194 | Expectations on mocks also work using `asserts` syntax via the `called` matcher: 195 | 196 | ```elixir 197 | before :each do 198 | allow HTTPotion |> to_receive(get: fn(url) -> "" end) 199 | end 200 | 201 | it "gets a page" do 202 | HTTPotion.get("http://example.com") 203 | 204 | assert called HTTPotion.get 205 | end 206 | ``` 207 | 208 | ### Mocks with arguments 209 | You can also perform assertions on what arguments were passed to a mocked 210 | method: 211 | 212 | ```elixir 213 | before :each do 214 | allow HTTPotion |> to_receive(get: fn(url) -> "" end) 215 | end 216 | 217 | it "gets a page" do 218 | HTTPotion.get("http://example.com") 219 | 220 | expect HTTPotion |> to_have_received :get |> with "http://example.com" 221 | end 222 | ``` 223 | 224 | In `asserts` syntax: 225 | 226 | ```elixir 227 | before :each do 228 | allow HTTPotion |> to_receive (get: fn(url) -> url end ) 229 | end 230 | 231 | it "gets a page" do 232 | HTTPotion.get("http://example.com") 233 | 234 | assert called HTTPotion.get("http://example.com") 235 | end 236 | ``` 237 | 238 | ## Skipping tests 239 | Pavlov runs with the `--exclude pending:true` configuration by default, which 240 | means that tests tagged with `:pending` will not be run. 241 | 242 | Pavlov offers several convenience methods to skip your tests, BDD style: 243 | 244 | ### xit 245 | Marks a specific test as pending and will not run it. 246 | 247 | ```elixir 248 | xit "does not run" do 249 | # This will never run 250 | end 251 | ``` 252 | 253 | ### xdescribe/xcontext 254 | Marks a group of tests as pending and will not run them. Just as `describe` 255 | and `context`, `xdescribe` and `xcontext` are analogous. 256 | 257 | ```elixir 258 | xdescribe "A pending group" do 259 | it "does not run" do 260 | # This will never run 261 | end 262 | 263 | it "does not run either" do 264 | # This will never run either 265 | end 266 | end 267 | ``` 268 | 269 | ## Development 270 | 271 | After cloning the repo, make sure to download all dependencies using `mix deps.get`. 272 | Pavlov is tested using Pavlov itself, so the general philosophy is to simply write a test using a given feature until it passes. 273 | 274 | ### Running the tests 275 | Simply run `mix test` 276 | 277 | ### Building the docs 278 | Run `MIX_ENV=docs mix docs`. The resulting HTML files will be output to the `docs` folder. 279 | 280 | ## Contributing 281 | 282 | 1. Fork it ( https://github.com/sproutapp/pavlov/fork ) 283 | 2. Create your feature branch (`git checkout -b my-new-feature`) 284 | 3. Commit your changes (`git commit -am 'Add some feature'`) 285 | 4. Push to the branch (`git push origin my-new-feature`) 286 | 5. Create a new Pull Request 287 | -------------------------------------------------------------------------------- /test/syntax/expect_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PavlovExpectTest do 2 | use Pavlov.Case, async: true 3 | import Pavlov.Syntax.Expect 4 | import Pavlov.Matchers.Messages 5 | 6 | describe "Matchers" do 7 | describe ".eq" do 8 | it "compares based on equality" do 9 | expect 1 |> to_eq 1 10 | end 11 | 12 | it "supports a negative expression" do 13 | expect 1 |> not_to_eq 2 14 | end 15 | 16 | it "provides a flunk message" do 17 | message = Pavlov.Matchers.Messages.message_for_matcher(:eq, [2, 1], :assertion) 18 | 19 | expect message 20 | |> to_eq "Expected 2 to equal 1" 21 | end 22 | 23 | it "provides a refutation flunk message" do 24 | message = Pavlov.Matchers.Messages.message_for_matcher(:eq, [2, 2], :refutation) 25 | 26 | expect message 27 | |> to_eq "Expected 2 not to equal 2" 28 | end 29 | end 30 | 31 | describe ".be_true" do 32 | it "compares against 'true'" do 33 | expect (1==1) |> to_be_true 34 | end 35 | 36 | it "supports a negative expression" do 37 | expect (1==2) |> not_to_be_true 38 | end 39 | 40 | it "provides a flunk message" do 41 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_true, [false], :assertion) 42 | 43 | expect message 44 | |> to_eq "Expected false to be true" 45 | end 46 | 47 | it "provides a refutation flunk message" do 48 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_true, [true], :refutation) 49 | 50 | expect message 51 | |> to_eq "Expected true not to be true" 52 | end 53 | end 54 | 55 | describe ".be_truthy" do 56 | it "compares based on truthiness" do 57 | expect 1 |> to_be_truthy 58 | expect true |> to_be_truthy 59 | expect "pavlov" |> to_be_truthy 60 | end 61 | 62 | it "supports a negative expression" do 63 | expect false |> not_to_be_truthy 64 | expect nil |> not_to_be_truthy 65 | end 66 | 67 | it "provides a flunk message" do 68 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_truthy, [nil], :assertion) 69 | 70 | expect message 71 | |> to_eq "Expected nil to be truthy" 72 | end 73 | 74 | it "provides a refutation flunk message" do 75 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_truthy, [nil], :refutation) 76 | 77 | expect message 78 | |> to_eq "Expected nil not to be truthy" 79 | end 80 | end 81 | 82 | describe ".be_falsey" do 83 | it "compares based on falseyness" do 84 | expect false |> to_be_falsey 85 | expect nil |> to_be_falsey 86 | end 87 | 88 | it "supports a negative expression" do 89 | expect 1 |> not_to_be_falsey 90 | expect true |> not_to_be_falsey 91 | expect "pavlov" |> not_to_be_falsey 92 | end 93 | 94 | it "provides a flunk message" do 95 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_falsey, [false], :assertion) 96 | 97 | expect message 98 | |> to_eq "Expected false to be falsey" 99 | end 100 | 101 | it "provides a refutation flunk message" do 102 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_falsey, [false], :refutation) 103 | 104 | expect message 105 | |> to_eq "Expected false not to be falsey" 106 | end 107 | end 108 | 109 | describe ".be_nil" do 110 | it "compares against nil" do 111 | expect nil |> to_be_nil 112 | end 113 | 114 | it "supports a negative expression" do 115 | expect "nil" |> not_to_be_nil 116 | end 117 | 118 | it "provides a flunk message" do 119 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_nil, [nil], :assertion) 120 | 121 | expect message 122 | |> to_eq "Expected nil to be nil" 123 | end 124 | 125 | it "provides a refutation flunk message" do 126 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_nil, [nil], :refutation) 127 | 128 | expect message 129 | |> to_eq "Expected nil not to be nil" 130 | end 131 | end 132 | 133 | describe ".have_key" do 134 | it "returns true if a dict has a key" do 135 | expect %{:a => 1} |> to_have_key :a 136 | expect [a: 1] |> to_have_key :a 137 | end 138 | 139 | it "supports a negative expression" do 140 | expect %{:a => 1} |> not_to_have_key :b 141 | expect [a: 1] |> not_to_have_key :b 142 | end 143 | 144 | it "provides a flunk message" do 145 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_key, [%{:a => 1}, :b], :assertion) 146 | 147 | expect message 148 | |> to_eq "Expected %{a: 1} to have key :b" 149 | end 150 | 151 | it "provides a refutation flunk message" do 152 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_key, [%{:a => 1}, :b], :refutation) 153 | 154 | expect message 155 | |> to_eq "Expected %{a: 1} not to have key :b" 156 | end 157 | end 158 | 159 | describe ".be_empty" do 160 | it "returns true if a dict is empty" do 161 | expect %{} |> to_be_empty 162 | expect [] |> to_be_empty 163 | end 164 | 165 | it "returns true if a string is empty" do 166 | expect "" |> to_be_empty 167 | end 168 | 169 | it "supports a negative expression" do 170 | expect %{:a => 1} |> not_to_be_empty 171 | expect [a: 1] |> not_to_be_empty 172 | expect "asd" |> not_to_be_empty 173 | end 174 | 175 | it "provides a flunk message" do 176 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_empty, [%{:a => 1}], :assertion) 177 | 178 | expect message 179 | |> to_eq "Expected %{a: 1} to be empty" 180 | end 181 | 182 | it "provides a refutation flunk message" do 183 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_empty, [%{:a => 1}], :refutation) 184 | 185 | expect message 186 | |> to_eq "Expected %{a: 1} not to be empty" 187 | end 188 | end 189 | 190 | describe ".include" do 191 | it "returns true if a member is in the List" do 192 | expect [1, 2, 3] |> to_include 2 193 | end 194 | 195 | it "works with maps using tuple notation" do 196 | expect %{:a => 1} |> to_include {:a, 1} 197 | end 198 | 199 | it "works with strings and partial strings" do 200 | expect "a string" |> to_include "a stri" 201 | end 202 | 203 | it "supports a negative expression" do 204 | expect [1, 2, 3] |> not_to_include 5 205 | expect %{:a => 1} |> not_to_include {:a, 5} 206 | expect "a string" |> not_to_include "a strict" 207 | end 208 | 209 | it "provides a flunk message" do 210 | message = Pavlov.Matchers.Messages.message_for_matcher(:include, [%{:a => 1}, %{:a => 5}], :assertion) 211 | 212 | expect message 213 | |> to_eq "Expected %{a: 1} to include %{a: 5}" 214 | end 215 | 216 | it "provides a refutation flunk message" do 217 | message = Pavlov.Matchers.Messages.message_for_matcher(:include, [%{:a => 1}, %{:a => 5}], :refutation) 218 | 219 | expect message 220 | |> to_eq "Expected %{a: 1} not to include %{a: 5}" 221 | end 222 | end 223 | 224 | describe ".have_raised" do 225 | it "returns true if a given function raised a given exception" do 226 | expect fn -> 1 + "test" end |> to_have_raised ArithmeticError 227 | end 228 | 229 | it "supports a negative expression" do 230 | expect fn -> 1 + 2 end |> not_to_have_raised ArithmeticError 231 | end 232 | 233 | it "provides a flunk message" do 234 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_raised, [fn -> 1 + "test" end, ArithmeticError], :assertion) 235 | 236 | expect message 237 | |> to_eq "Expected function to have raised ArithmeticError" 238 | end 239 | 240 | it "provides a refutation flunk message" do 241 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_raised, [fn -> 1 + "test" end, ArithmeticError], :refutation) 242 | 243 | expect message 244 | |> to_eq "Expected function not to have raised ArithmeticError" 245 | end 246 | end 247 | 248 | describe ".have_thrown" do 249 | it "returns true if a given function threw a given value" do 250 | expect fn -> throw("x") end |> to_have_thrown "x" 251 | end 252 | 253 | it "supports a negative expression" do 254 | expect fn -> throw("x") end |> not_to_have_thrown "y" 255 | end 256 | 257 | it "provides a flunk message" do 258 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_thrown, [fn -> 1 + throw("x") end, "x"], :assertion) 259 | 260 | expect message 261 | |> to_eq "Expected function to have thrown \"x\"" 262 | end 263 | 264 | it "provides a refutation flunk message" do 265 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_thrown, [fn -> 1 + throw("x") end, "x"], :refutation) 266 | 267 | expect message 268 | |> to_eq "Expected function not to have thrown \"x\"" 269 | end 270 | end 271 | 272 | describe ".have_exited" do 273 | it "returns true if a given function exited" do 274 | expect fn -> exit "bye!" end |> to_have_exited 275 | end 276 | 277 | it "supports a negative expression" do 278 | expect fn -> :ok end |> not_to_have_exited 279 | end 280 | 281 | it "provides a flunk message" do 282 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_exited, [fn -> exit "bye!" end], :assertion) 283 | 284 | expect message 285 | |> to_eq "Expected function to have exited" 286 | end 287 | 288 | it "provides a refutation flunk message" do 289 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_exited, [fn -> exit "bye!" end], :refutation) 290 | 291 | expect message 292 | |> to_eq "Expected function not to have exited" 293 | end 294 | end 295 | end 296 | end 297 | --------------------------------------------------------------------------------