├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── callbacks.ex ├── case.ex ├── matchers.ex ├── matchers │ └── messages.ex ├── mocks.ex ├── mocks │ ├── matchers.ex │ └── matchers │ │ └── messages.ex ├── pavlov.ex ├── syntax │ ├── expect.ex │ └── sugar.ex └── utils │ └── memoize.ex ├── mix.exs ├── mix.lock ├── script └── ci │ ├── docs.sh │ ├── prepare.sh │ └── test.sh └── test ├── callbacks_test.exs ├── case └── case_test.exs ├── fixtures └── mockable.ex ├── mocks_test.exs ├── syntax └── expect_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | /docs 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | otp_release: 3 | - 17.4 4 | after_script: 5 | - MIX_ENV=docs mix deps.get 6 | - MIX_ENV=docs mix inch.report 7 | sudo: false # aka: use container for testing 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | - [Subject](#subject) 39 | - [Expects syntax](#expects-syntax) 40 | - [Included Matchers](#included-matchers) 41 | - [Callbacks](#callbacks) 42 | - [before(:each)](#beforeeach) 43 | - [before(:all)](#beforeall) 44 | - [Mocking](#mocking) 45 | - [Mocks with arguments](#mocks-with-arguments) 46 | - [Skipping tests](#skipping-tests) 47 | - [xit](#xit) 48 | - [xdescribe/xcontext](#xdescribexcontext) 49 | - [Development](#development) 50 | - [Running the tests](#running-the-tests) 51 | - [Building the docs](#building-the-docs) 52 | - [Contributing](#contributing) 53 | 54 | ## Usage 55 | Add Pavlov as a dependency in your `mix.exs` file: 56 | 57 | ```elixir 58 | defp deps do 59 | [{:pavlov, ">= 0.1.0", only: :test}] 60 | end 61 | ``` 62 | 63 | After you are done, run `mix deps.get` in your shell to fetch the dependencies. 64 | To start execution of your Pavlov tests, add the following to your 'test/test_helper.exs': 65 | 66 | ```elixir 67 | Pavlov.start 68 | ``` 69 | 70 | Afterwards, running `mix test` in your shell will run all test suites. 71 | 72 | ## Describe and Context 73 | 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. 74 | 75 | ## Let 76 | 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. 77 | 78 | ```elixir 79 | let :order do 80 | %Order{items: [ 81 | {"burger", 10.0} 82 | {"fries", 5.2} 83 | ]} 84 | end 85 | ``` 86 | 87 | ## Subject 88 | You can use `subject` to explicitly define the value that is returned by the subject method in the example scope. A subject declared in a context will be available in child contexts as well. 89 | 90 | ```elixir 91 | describe "Array" do 92 | subject do 93 | [1, 2, 3] 94 | end 95 | 96 | it "has the prescribed elements" do 97 | assert subject == [1, 2, 3] 98 | end 99 | 100 | context "Inner context" do 101 | it "can use an outer-scope subject" do 102 | assert subject == [1, 2, 3] 103 | end 104 | end 105 | end 106 | ``` 107 | 108 | If you are using [Expects syntax](#expects-syntax) together with `subject`, you can use the `is_expected` helper: 109 | 110 | ```elixir 111 | describe "Numbers" do 112 | subject do: 5 113 | 114 | context "straight up, no message" do 115 | it is_expected |> to_eq 5 116 | end 117 | 118 | context "you can also use a custom message" do 119 | it "is equal to 5" do 120 | is_expected |> to_eq 5 121 | end 122 | end 123 | end 124 | ``` 125 | 126 | By default, every created context via `describe` or `context` contains a `subject` that returns `nil`. 127 | 128 | ## Expects syntax 129 | 130 | You may use the regular ExUnit `assert` syntax if you wish, but Pavlov includes 131 | an `expect` syntax that makes your tests more readable. 132 | 133 | If you wish to use this syntax, simply import the `Pavlov.Syntax.Expect` at the 134 | beginning of your Test module: 135 | 136 | ```elixir 137 | defmodule MyTest do 138 | use Pavlov.Case, async: true 139 | import Pavlov.Syntax.Expect 140 | #... 141 | end 142 | ``` 143 | 144 | All core matchers are supported under both syntaxes. 145 | 146 | ## Included Matchers 147 | 148 | When using the `expects` syntax, all matchers have negative counterparts, ie: 149 | ```elixir 150 | expect 1 |> not_to_eq 2 151 | expect(1 > 5) |> not_to_be_true 152 | ``` 153 | 154 | Visit the [Pavlov Wiki](https://github.com/sproutapp/pavlov/wiki/Included-Matchers) 155 | to learn more about all of the core matchers available for your tests. 156 | 157 | ## Callbacks 158 | For now, Pavlov only supports callbacks that run before test cases. [ExUnit's 159 | `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. 160 | 161 | ### before(:each) 162 | Runs the specified code before every test case. 163 | 164 | ```elixir 165 | describe "before :each" do 166 | before :each do 167 | IO.puts "A test is about to start" 168 | :ok 169 | end 170 | 171 | it "does something" do 172 | #... 173 | end 174 | 175 | it "does something else" do 176 | #... 177 | end 178 | end 179 | ``` 180 | 181 | In this case, `"A test is about to start"` is printed twice to the console. 182 | 183 | ### before(:all) 184 | Runs the specified code once before any tests run. 185 | 186 | ```elixir 187 | describe "before :all" do 188 | before :all do 189 | IO.puts "This suite is about to run" 190 | :ok 191 | end 192 | 193 | it "does something" do 194 | #... 195 | end 196 | 197 | it "does something else" do 198 | #... 199 | end 200 | end 201 | ``` 202 | In this case, `"This suite is about to run"` is printed once to the console. 203 | 204 | ## Mocking 205 | Pavlov provides facilities to mock functions in your Elixir modules. This is 206 | achieved using [Meck](https://github.com/eproxus/meck), an erlang mocking tool. 207 | 208 | Here's a simple example using [HTTPotion](https://github.com/myfreeweb/httpotion): 209 | 210 | ```elixir 211 | before :each do 212 | allow HTTPotion |> to_receive(get: fn(url) -> "" end) 213 | end 214 | 215 | it "gets a page" do 216 | result = HTTPotion.get("http://example.com") 217 | 218 | expect HTTPotion |> to_have_received :get 219 | expect result |> to_eq "" 220 | end 221 | ``` 222 | 223 | If you want the mock to retain all other functions in the original module, 224 | then you will need to pass the `opts` `List` argument to the `allow` function 225 | and include the `:passthrough` value. The `allow` function specifies a default 226 | `opts` `List` that includes the `:no_link` value. This value should be included 227 | in the `List` as it ensures that the mock (which is linked to the creating 228 | process) will unload automatically when a crash occurs. 229 | 230 | ```elixir 231 | before :each do 232 | allow(HTTPotion, [:no_link, :passthrough]) |> to_receive(get: fn(url) -> "" end) 233 | end 234 | ``` 235 | 236 | Expectations on mocks also work using `asserts` syntax via the `called` matcher: 237 | 238 | ```elixir 239 | before :each do 240 | allow HTTPotion |> to_receive(get: fn(url) -> "" end) 241 | end 242 | 243 | it "gets a page" do 244 | HTTPotion.get("http://example.com") 245 | 246 | assert called HTTPotion.get 247 | end 248 | ``` 249 | 250 | ### Mocks with arguments 251 | You can also perform assertions on what arguments were passed to a mocked 252 | method: 253 | 254 | ```elixir 255 | before :each do 256 | allow HTTPotion |> to_receive(get: fn(url) -> "" end) 257 | end 258 | 259 | it "gets a page" do 260 | HTTPotion.get("http://example.com") 261 | 262 | expect HTTPotion |> to_have_received :get |> with_args "http://example.com" 263 | end 264 | ``` 265 | 266 | In `asserts` syntax: 267 | 268 | ```elixir 269 | before :each do 270 | allow HTTPotion |> to_receive (get: fn(url) -> url end ) 271 | end 272 | 273 | it "gets a page" do 274 | HTTPotion.get("http://example.com") 275 | 276 | assert called HTTPotion.get("http://example.com") 277 | end 278 | ``` 279 | 280 | ## Skipping tests 281 | Pavlov runs with the `--exclude pending:true` configuration by default, which 282 | means that tests tagged with `:pending` will not be run. 283 | 284 | Pavlov offers several convenience methods to skip your tests, BDD style: 285 | 286 | ### xit 287 | Marks a specific test as pending and will not run it. 288 | 289 | ```elixir 290 | xit "does not run" do 291 | # This will never run 292 | end 293 | ``` 294 | 295 | ### xdescribe/xcontext 296 | Marks a group of tests as pending and will not run them. Just as `describe` 297 | and `context`, `xdescribe` and `xcontext` are analogous. 298 | 299 | ```elixir 300 | xdescribe "A pending group" do 301 | it "does not run" do 302 | # This will never run 303 | end 304 | 305 | it "does not run either" do 306 | # This will never run either 307 | end 308 | end 309 | ``` 310 | 311 | ## Development 312 | 313 | After cloning the repo, make sure to download all dependencies using `mix deps.get`. 314 | Pavlov is tested using Pavlov itself, so the general philosophy is to simply write a test using a given feature until it passes. 315 | 316 | ### Running the tests 317 | Simply run `mix test` 318 | 319 | ### Building the docs 320 | Run `MIX_ENV=docs mix docs`. The resulting HTML files will be output to the `docs` folder. 321 | 322 | ## Contributing 323 | 324 | 1. Fork it ( https://github.com/sproutapp/pavlov/fork ) 325 | 2. Create your feature branch (`git checkout -b my-new-feature`) 326 | 3. Commit your changes (`git commit -am 'Add some feature'`) 327 | 4. Push to the branch (`git push origin my-new-feature`) 328 | 5. Create a new Pull Request 329 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | block = contents[:do] 50 | quote do 51 | setup unquote(context), do: unquote(do_block(block)) 52 | 53 | Agent.update :pavlov_callback_defs, fn(map) -> 54 | Dict.put_new map, __MODULE__, {:each, unquote(Macro.escape context), unquote(Macro.escape contents[:do])} 55 | end 56 | end 57 | end 58 | defmacro before(:all, context, contents) do 59 | block = contents[:do] 60 | quote do 61 | setup_all unquote(context), do: unquote(do_block(block)) 62 | 63 | Agent.update :pavlov_callback_defs, fn(map) -> 64 | Dict.put_new map, __MODULE__, {:all, unquote(Macro.escape context), unquote(Macro.escape contents[:do])} 65 | end 66 | end 67 | end 68 | 69 | defp do_block({:__block__, _, statements} = block) do 70 | [last | _] = Enum.reverse(statements) 71 | if match?(:ok, last) || match?({:ok, _}, last) do 72 | block 73 | else 74 | {:__block__, [], [statements, :ok]} 75 | end 76 | end 77 | defp do_block({:ok, _} = block), do: block 78 | defp do_block(:ok), do: :ok 79 | defp do_block(block) do 80 | {:__block__, [], [block, :ok]} 81 | end 82 | 83 | end 84 | -------------------------------------------------------------------------------- /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 | Agent.start(fn -> %{} end, name: :pavlov_subject_defs) 28 | 29 | import Pavlov.Case 30 | import Pavlov.Syntax.Sugar 31 | end 32 | end 33 | 34 | @doc """ 35 | The cornerstone BDD macro, "it" allows your test to be defined 36 | via a string. 37 | 38 | ## Example 39 | it "is the truth" do 40 | assert true == true 41 | end 42 | """ 43 | defmacro it(contents) do 44 | quote do 45 | it "is expected", do: unquote(contents) 46 | end 47 | end 48 | defmacro it(desc, var \\ quote(do: _), contents) do 49 | quote do 50 | message = Enum.join(@stack, "") <> unquote(desc) 51 | 52 | defit message, unquote(var), @pending do 53 | unquote(contents) 54 | end 55 | end 56 | end 57 | 58 | @doc """ 59 | Allows you specify a pending test, meaning that it is never run. 60 | 61 | ## Example 62 | xit "is the truth" do 63 | # This will never run 64 | assert true == true 65 | end 66 | """ 67 | defmacro xit(description, var \\ quote(do: _), contents) do 68 | quote do 69 | defit Enum.join(@stack, "") <> unquote(description), unquote(var), true do 70 | unquote(contents) 71 | end 72 | end 73 | end 74 | 75 | @doc """ 76 | You can nest your tests under a descriptive name. 77 | Tests can be infinitely nested. 78 | """ 79 | defmacro describe(desc, _ \\ quote(do: _), pending \\ false, contents) do 80 | quote do 81 | @stack Enum.concat(@stack, [unquote(desc) <> ", "]) 82 | # Closure the old stack so we can use it in defmodule 83 | old_stack = Enum.concat @stack, [] 84 | pending = @pending || unquote(pending) 85 | 86 | # Defines a new module per describe, thus scoping .let 87 | defmodule Module.concat(__MODULE__, unquote(desc)) do 88 | use ExUnit.Case 89 | 90 | @stack old_stack 91 | @pending pending 92 | 93 | def subject, do: nil 94 | defoverridable [subject: 0] 95 | 96 | # Redefine enclosing let definitions in this module 97 | Agent.get(:pavlov_callback_defs, fn dict -> 98 | Stream.filter dict, fn {module, _name} -> 99 | sub_module? module, __MODULE__ 100 | end 101 | end) 102 | |> Stream.map(fn {_module, {periodicity, context, fun}} -> 103 | quote do 104 | use Pavlov.Mocks 105 | before(unquote(periodicity), unquote(context), do: unquote(fun)) 106 | end 107 | end) 108 | |> Enum.each(&Module.eval_quoted(__MODULE__, &1)) 109 | 110 | unquote(contents) 111 | 112 | # Redefine enclosing let definitions in this module 113 | Agent.get(:pavlov_let_defs, fn dict -> 114 | Stream.filter dict, fn {module, lets} -> 115 | sub_module? module, __MODULE__ 116 | end 117 | end) 118 | |> Stream.flat_map(fn {_module, lets} -> 119 | Stream.map lets, fn ({name, fun}) -> 120 | quote do: let(unquote(name), do: unquote(fun)) 121 | end 122 | end) 123 | |> Enum.each(&Module.eval_quoted(__MODULE__, &1)) 124 | 125 | # Redefine enclosing subject definitions in this module 126 | Agent.get(:pavlov_subject_defs, fn dict -> 127 | Stream.filter dict, fn {module, _subjects} -> 128 | sub_module? module, __MODULE__ 129 | end 130 | end) 131 | |> Stream.flat_map(fn {_module, subjects} -> 132 | Stream.map subjects, fn (fun) -> 133 | quote do: subject(unquote(fun)) 134 | end 135 | end) 136 | |> Enum.each(&Module.eval_quoted(__MODULE__, &1)) 137 | end 138 | 139 | # Cleans context stack 140 | if Enum.count(@stack) > 0 do 141 | @stack Enum.take(@stack, Enum.count(@stack) - 1) 142 | end 143 | end 144 | end 145 | 146 | @doc """ 147 | Defines a group of tests as pending. 148 | Any other contexts nested within an xdescribe will not run 149 | as well. 150 | """ 151 | defmacro xdescribe(desc, _ \\ quote(do: _), contents) do 152 | quote do 153 | describe unquote(desc), _, true, unquote(contents) 154 | end 155 | end 156 | 157 | @doc """ 158 | Allows lazy initialization of subjects for your tests. 159 | Subjects created via "let" will never leak into other 160 | contexts (defined via "describe" or "context"), not even 161 | those who are children of the context where the lazy subject 162 | is defined. 163 | 164 | Example: 165 | let :lazy do 166 | "oh so lazy" 167 | end 168 | 169 | it "lazy initializes" do 170 | assert lazy == "oh so lazy" 171 | end 172 | """ 173 | 174 | defmacro let(name, contents) do 175 | quote do 176 | require Pavlov.Utils.Memoize, as: Memoize 177 | Memoize.defmem unquote(name)(), do: unquote(contents[:do]) 178 | 179 | defoverridable [{unquote(name), 0}] 180 | 181 | Agent.update(:pavlov_let_defs, fn(map) -> 182 | new_let = {unquote(Macro.escape name), unquote(Macro.escape contents[:do])} 183 | 184 | Dict.put map, __MODULE__, (map[__MODULE__] || []) ++ [new_let] 185 | end) 186 | end 187 | end 188 | 189 | @doc """ 190 | You can use `subject` to explicitly define the value 191 | that is returned by the subject method in the example 192 | scope. A subject declared in a context will be available 193 | in child contexts as well. 194 | 195 | Example: 196 | describe "Array" do 197 | subject do 198 | [1, 2, 3] 199 | end 200 | 201 | it "has the prescribed elements" do 202 | assert subject == [1, 2, 3] 203 | end 204 | 205 | context "Inner context" do 206 | it "can use an outer-scope subject" do 207 | assert subject == [1, 2, 3] 208 | end 209 | end 210 | end 211 | """ 212 | 213 | defmacro subject(contents) do 214 | contents = Macro.escape(contents) 215 | 216 | quote bind_quoted: binding do 217 | def subject do 218 | Macro.expand(unquote(contents), __MODULE__)[:do] 219 | end 220 | 221 | defoverridable [subject: 0] 222 | 223 | Agent.update(:pavlov_subject_defs, fn(map) -> 224 | Dict.put map, __MODULE__, (map[__MODULE__] || []) ++ [contents] 225 | end) 226 | end 227 | end 228 | 229 | @doc false 230 | defmacro defit(message, var \\ quote(do: _), pending \\ false, contents) do 231 | contents = 232 | case contents do 233 | [do: _] -> 234 | quote do 235 | unquote(contents) 236 | :ok 237 | end 238 | _ -> 239 | quote do 240 | try(unquote(contents)) 241 | :ok 242 | end 243 | end 244 | 245 | var = Macro.escape(var) 246 | contents = Macro.escape(contents, unquote: true) 247 | 248 | quote bind_quoted: binding do 249 | message = :"#{message}" 250 | Pavlov.Case.__on_definition__(__ENV__, message, pending) 251 | 252 | def unquote(message)(unquote(var)) do 253 | Pavlov.Utils.Memoize.flush 254 | unquote(contents) 255 | end 256 | end 257 | end 258 | 259 | @doc false 260 | def __on_definition__(env, name, pending \\ false) do 261 | mod = env.module 262 | tags = Module.get_attribute(mod, :tag) ++ Module.get_attribute(mod, :moduletag) 263 | if pending do tags = tags ++ [:pending] end 264 | tags = tags |> normalize_tags |> Map.merge(%{line: env.line, file: env.file}) 265 | 266 | Module.put_attribute(mod, :ex_unit_tests, 267 | %ExUnit.Test{name: name, case: mod, tags: tags}) 268 | 269 | Module.delete_attribute(mod, :tag) 270 | end 271 | 272 | @doc false 273 | def sub_module?(child, parent) do 274 | String.starts_with? "#{parent}", "#{child}" 275 | end 276 | 277 | defp normalize_tags(tags) do 278 | Enum.reduce Enum.reverse(tags), %{}, fn 279 | tag, acc when is_atom(tag) -> Map.put(acc, tag, true) 280 | tag, acc when is_list(tag) -> Dict.merge(acc, tag) 281 | end 282 | end 283 | end 284 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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/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_args "http://example.com" 65 | """ 66 | def with_args(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 | -------------------------------------------------------------------------------- /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 #{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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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({fun, context, args}, module) do 17 | contents = {fun, context, args} 18 | 19 | quote do 20 | Module.eval_quoted(unquote(module), unquote(contents)) 21 | end 22 | end 23 | def expect(subject) do 24 | subject 25 | end 26 | 27 | # Dynamically defines methods for included matchers, such that: 28 | # For a given matcher `eq`, defines a method `to_eq`. 29 | Enum.each Pavlov.Matchers.__info__(:functions), fn({method, _}) -> 30 | method_name = :"to_#{method}" 31 | 32 | @doc false 33 | def unquote(method_name)(expected, actual \\ nil) do 34 | args = case actual do 35 | nil -> [expected] 36 | _ -> [actual, expected] 37 | end 38 | 39 | result = apply(Pavlov.Matchers, unquote(method), args) 40 | 41 | case result do 42 | false -> flunk message_for_matcher(unquote(:"#{method}"), args, :assertion) 43 | _ -> assert result 44 | end 45 | end 46 | end 47 | 48 | # Dynamically defines methods for included matchers, such that: 49 | # For a given matcher `eq`, defines a method `not_to_eq`. 50 | Enum.each Pavlov.Matchers.__info__(:functions), fn({method, _}) -> 51 | method_name = :"not_to_#{method}" 52 | 53 | @doc false 54 | def unquote(method_name)(expected, actual \\ nil) do 55 | args = case actual do 56 | nil -> [expected] 57 | _ -> [actual, expected] 58 | end 59 | 60 | result = apply(Pavlov.Matchers, unquote(method), args) 61 | 62 | case result do 63 | true -> flunk message_for_matcher(unquote(:"#{method}"), args, :refutation) 64 | _ -> refute result 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /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 | defmacro is_expected do 29 | quote do 30 | subject 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /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.3" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 28 | context "without :ok, {:ok, dict}" do 29 | before :each do 30 | :something_other_than_ok 31 | end 32 | it "does not error", _context do 33 | :ok 34 | end 35 | end 36 | end 37 | 38 | describe "before :all" do 39 | before :all do 40 | {:ok, [context: :setup_all]} 41 | end 42 | 43 | it "runs the callback before all tests", context do 44 | expect context[:context] |> to_eq :setup_all 45 | end 46 | 47 | context "With a Nested context" do 48 | it "runs the callback before the test", context do 49 | expect context[:context] |> to_eq :setup_all 50 | end 51 | end 52 | 53 | context "without :ok, {:ok, dict}" do 54 | before :all do 55 | :something_other_than_ok 56 | end 57 | it "does not error", _context do 58 | :ok 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /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 | 120 | describe ".subject" do 121 | subject do: response 122 | let :response, do: 5 123 | 124 | it "allows the usage of a subject method" do 125 | assert subject == response 126 | end 127 | 128 | context "Inner .subject" do 129 | it "uses the outer scope's subject method" do 130 | assert subject == response 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /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 | 17 | @doc false 18 | def do_with_several_args(arg1, arg2) do 19 | [arg1, arg2] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /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_args "a string" 102 | end 103 | 104 | it "allows setting expectations matching method and several arguments" do 105 | allow(Fixtures.Mockable) |> to_receive(do_with_several_args: fn(_, _) -> :ok end ) 106 | 107 | Fixtures.Mockable.do_with_several_args(1, 2) 108 | 109 | expect Fixtures.Mockable |> to_have_received :do_with_several_args |> with_args [:_, 2] 110 | end 111 | 112 | it "allows mocking with arguments to return something" do 113 | allow(Fixtures.Mockable) |> to_receive(do_with_args: fn(_) -> :error end ) 114 | 115 | result = Fixtures.Mockable.do_with_args("a string") 116 | 117 | expect Fixtures.Mockable |> to_have_received :do_with_args |> with_args "a string" 118 | expect result |> to_eq :error 119 | end 120 | 121 | it "provides a flunk message" do 122 | message = message_for_matcher(:have_received, [Fixtures.Mockable, :do_with_args, [1]], :assertion) 123 | 124 | expect message 125 | |> to_eq "Expected Elixir.Fixtures.Mockable to have received :do_with_args with_args [1]" 126 | end 127 | end 128 | 129 | context "Callbacks" do 130 | before :each do 131 | allow(Fixtures.Mockable) |> to_receive(do_something: fn -> :error end) 132 | :ok 133 | end 134 | 135 | it "supports mocking at setup time" 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 "Across contexts" do 143 | it "supports mocking across contexts" do 144 | result = Fixtures.Mockable.do_something() 145 | 146 | expect Fixtures.Mockable |> to_have_received :do_something 147 | expect result |> to_eq(:error) 148 | end 149 | 150 | context "When an existing mock is re-mocked" do 151 | before :each do 152 | allow(Fixtures.Mockable) |> to_receive(do_something: fn -> :ok end) 153 | :ok 154 | end 155 | 156 | it "uses the re-mock" do 157 | result = Fixtures.Mockable.do_something() 158 | 159 | expect Fixtures.Mockable |> to_have_received :do_something 160 | expect result |> to_eq(:ok) 161 | end 162 | end 163 | end 164 | end 165 | 166 | context "Using asserts syntax" do 167 | describe ".called" do 168 | it "works for simple mocks" do 169 | allow(Fixtures.Mockable) |> to_receive(do_something: fn -> :error end) 170 | 171 | Fixtures.Mockable.do_something() 172 | 173 | assert called Fixtures.Mockable.do_something 174 | end 175 | 176 | it "works for mocks with arguments" do 177 | allow(Fixtures.Mockable) |> to_receive(do_with_args: fn(_) -> :ok end) 178 | 179 | Fixtures.Mockable.do_with_args("a string") 180 | 181 | assert called Fixtures.Mockable.do_with_args("a string") 182 | end 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /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 ".is_expected" do 7 | subject do: 5 8 | 9 | context "without a message" do 10 | it is_expected |> to_eq 5 11 | end 12 | 13 | context "with a message" do 14 | it "is equal to 5" do 15 | is_expected |> to_eq 5 16 | end 17 | end 18 | end 19 | 20 | describe "Matchers" do 21 | describe ".eq" do 22 | it "compares based on equality" do 23 | expect 1 |> to_eq 1 24 | end 25 | 26 | it "supports a negative expression" do 27 | expect 1 |> not_to_eq 2 28 | end 29 | 30 | it "provides a flunk message" do 31 | message = Pavlov.Matchers.Messages.message_for_matcher(:eq, [2, 1], :assertion) 32 | 33 | expect message 34 | |> to_eq "Expected 2 to equal 1" 35 | end 36 | 37 | it "provides a refutation flunk message" do 38 | message = Pavlov.Matchers.Messages.message_for_matcher(:eq, [2, 2], :refutation) 39 | 40 | expect message 41 | |> to_eq "Expected 2 not to equal 2" 42 | end 43 | end 44 | 45 | describe ".be_true" do 46 | it "compares against 'true'" do 47 | expect (1==1) |> to_be_true 48 | end 49 | 50 | it "supports a negative expression" do 51 | expect (1==2) |> not_to_be_true 52 | end 53 | 54 | it "provides a flunk message" do 55 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_true, [false], :assertion) 56 | 57 | expect message 58 | |> to_eq "Expected false to be true" 59 | end 60 | 61 | it "provides a refutation flunk message" do 62 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_true, [true], :refutation) 63 | 64 | expect message 65 | |> to_eq "Expected true not to be true" 66 | end 67 | end 68 | 69 | describe ".be_truthy" do 70 | it "compares based on truthiness" do 71 | expect 1 |> to_be_truthy 72 | expect true |> to_be_truthy 73 | expect "pavlov" |> to_be_truthy 74 | end 75 | 76 | it "supports a negative expression" do 77 | expect false |> not_to_be_truthy 78 | expect nil |> not_to_be_truthy 79 | end 80 | 81 | it "provides a flunk message" do 82 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_truthy, [nil], :assertion) 83 | 84 | expect message 85 | |> to_eq "Expected nil to be truthy" 86 | end 87 | 88 | it "provides a refutation flunk message" do 89 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_truthy, [nil], :refutation) 90 | 91 | expect message 92 | |> to_eq "Expected nil not to be truthy" 93 | end 94 | end 95 | 96 | describe ".be_falsey" do 97 | it "compares based on falseyness" do 98 | expect false |> to_be_falsey 99 | expect nil |> to_be_falsey 100 | end 101 | 102 | it "supports a negative expression" do 103 | expect 1 |> not_to_be_falsey 104 | expect true |> not_to_be_falsey 105 | expect "pavlov" |> not_to_be_falsey 106 | end 107 | 108 | it "provides a flunk message" do 109 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_falsey, [false], :assertion) 110 | 111 | expect message 112 | |> to_eq "Expected false to be falsey" 113 | end 114 | 115 | it "provides a refutation flunk message" do 116 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_falsey, [false], :refutation) 117 | 118 | expect message 119 | |> to_eq "Expected false not to be falsey" 120 | end 121 | end 122 | 123 | describe ".be_nil" do 124 | it "compares against nil" do 125 | expect nil |> to_be_nil 126 | end 127 | 128 | it "supports a negative expression" do 129 | expect "nil" |> not_to_be_nil 130 | end 131 | 132 | it "provides a flunk message" do 133 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_nil, [nil], :assertion) 134 | 135 | expect message 136 | |> to_eq "Expected nil to be nil" 137 | end 138 | 139 | it "provides a refutation flunk message" do 140 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_nil, [nil], :refutation) 141 | 142 | expect message 143 | |> to_eq "Expected nil not to be nil" 144 | end 145 | end 146 | 147 | describe ".have_key" do 148 | it "returns true if a dict has a key" do 149 | expect %{:a => 1} |> to_have_key :a 150 | expect [a: 1] |> to_have_key :a 151 | end 152 | 153 | it "supports a negative expression" do 154 | expect %{:a => 1} |> not_to_have_key :b 155 | expect [a: 1] |> not_to_have_key :b 156 | end 157 | 158 | it "provides a flunk message" do 159 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_key, [%{:a => 1}, :b], :assertion) 160 | 161 | expect message 162 | |> to_eq "Expected %{a: 1} to have key :b" 163 | end 164 | 165 | it "provides a refutation flunk message" do 166 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_key, [%{:a => 1}, :b], :refutation) 167 | 168 | expect message 169 | |> to_eq "Expected %{a: 1} not to have key :b" 170 | end 171 | end 172 | 173 | describe ".be_empty" do 174 | it "returns true if a dict is empty" do 175 | expect %{} |> to_be_empty 176 | expect [] |> to_be_empty 177 | end 178 | 179 | it "returns true if a string is empty" do 180 | expect "" |> to_be_empty 181 | end 182 | 183 | it "supports a negative expression" do 184 | expect %{:a => 1} |> not_to_be_empty 185 | expect [a: 1] |> not_to_be_empty 186 | expect "asd" |> not_to_be_empty 187 | end 188 | 189 | it "provides a flunk message" do 190 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_empty, [%{:a => 1}], :assertion) 191 | 192 | expect message 193 | |> to_eq "Expected %{a: 1} to be empty" 194 | end 195 | 196 | it "provides a refutation flunk message" do 197 | message = Pavlov.Matchers.Messages.message_for_matcher(:be_empty, [%{:a => 1}], :refutation) 198 | 199 | expect message 200 | |> to_eq "Expected %{a: 1} not to be empty" 201 | end 202 | end 203 | 204 | describe ".include" do 205 | it "returns true if a member is in the List" do 206 | expect [1, 2, 3] |> to_include 2 207 | end 208 | 209 | it "works with maps using tuple notation" do 210 | expect %{:a => 1} |> to_include {:a, 1} 211 | end 212 | 213 | it "works with strings and partial strings" do 214 | expect "a string" |> to_include "a stri" 215 | end 216 | 217 | it "supports a negative expression" do 218 | expect [1, 2, 3] |> not_to_include 5 219 | expect %{:a => 1} |> not_to_include {:a, 5} 220 | expect "a string" |> not_to_include "a strict" 221 | end 222 | 223 | it "provides a flunk message" do 224 | message = Pavlov.Matchers.Messages.message_for_matcher(:include, [%{:a => 1}, %{:a => 5}], :assertion) 225 | 226 | expect message 227 | |> to_eq "Expected %{a: 1} to include %{a: 5}" 228 | end 229 | 230 | it "provides a refutation flunk message" do 231 | message = Pavlov.Matchers.Messages.message_for_matcher(:include, [%{:a => 1}, %{:a => 5}], :refutation) 232 | 233 | expect message 234 | |> to_eq "Expected %{a: 1} not to include %{a: 5}" 235 | end 236 | end 237 | 238 | describe ".have_raised" do 239 | it "returns true if a given function raised a given exception" do 240 | expect fn -> 1 + "test" end |> to_have_raised ArithmeticError 241 | end 242 | 243 | it "supports a negative expression" do 244 | expect fn -> 1 + 2 end |> not_to_have_raised ArithmeticError 245 | end 246 | 247 | it "provides a flunk message" do 248 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_raised, [fn -> 1 + "test" end, ArithmeticError], :assertion) 249 | 250 | expect message 251 | |> to_eq "Expected function to have raised ArithmeticError" 252 | end 253 | 254 | it "provides a refutation flunk message" do 255 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_raised, [fn -> 1 + "test" end, ArithmeticError], :refutation) 256 | 257 | expect message 258 | |> to_eq "Expected function not to have raised ArithmeticError" 259 | end 260 | end 261 | 262 | describe ".have_thrown" do 263 | it "returns true if a given function threw a given value" do 264 | expect fn -> throw("x") end |> to_have_thrown "x" 265 | end 266 | 267 | it "supports a negative expression" do 268 | expect fn -> throw("x") end |> not_to_have_thrown "y" 269 | end 270 | 271 | it "provides a flunk message" do 272 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_thrown, [fn -> 1 + throw("x") end, "x"], :assertion) 273 | 274 | expect message 275 | |> to_eq "Expected function to have thrown \"x\"" 276 | end 277 | 278 | it "provides a refutation flunk message" do 279 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_thrown, [fn -> 1 + throw("x") end, "x"], :refutation) 280 | 281 | expect message 282 | |> to_eq "Expected function not to have thrown \"x\"" 283 | end 284 | end 285 | 286 | describe ".have_exited" do 287 | it "returns true if a given function exited" do 288 | expect fn -> exit "bye!" end |> to_have_exited 289 | end 290 | 291 | it "supports a negative expression" do 292 | expect fn -> :ok end |> not_to_have_exited 293 | end 294 | 295 | it "provides a flunk message" do 296 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_exited, [fn -> exit "bye!" end], :assertion) 297 | 298 | expect message 299 | |> to_eq "Expected function to have exited" 300 | end 301 | 302 | it "provides a refutation flunk message" do 303 | message = Pavlov.Matchers.Messages.message_for_matcher(:have_exited, [fn -> exit "bye!" end], :refutation) 304 | 305 | expect message 306 | |> to_eq "Expected function not to have exited" 307 | end 308 | end 309 | end 310 | end 311 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Pavlov.start 2 | --------------------------------------------------------------------------------