GithubHelp home page GithubHelp logo

rspec_book_in_exunit's Introduction

RspecBook

Chapter 1

Install ExUnit

mix new rspec_book
cd rspec_book

Your First Spec

# 01-getting-started/01/spec/sandwich_spec.rb
defmodule RspecBook.SandwichTest do
  use ExUnit.Case
  doctest RspecBook.Sandwich
  alias RspecBook.Sandwich

  describe "an ideal sandwich" do
    test "is delicious" do
    end
  end
end

# 01-getting-started/02/spec/sandwich_spec.rb
defmodule RspecBook.SandwichTest do
  use ExUnit.Case
  doctest RspecBook.Sandwich
  alias RspecBook.Sandwich

  describe "an ideal sandwich" do
    test "is delicious" do
      sandwich = %Sandwich{taste: "delicious"}
      assert sandwich.taste == "delicious"
    end
  end
end

Groups, Examples, and Expectations

  • groups -> describe
  • examble -> test
  • expectation -> assert

Getting the Most Out of RSpec

  • sandwichのあるまじき姿の文書化
  • sandwichの行動の確認
# 01-getting-started/02/sandwich_test.rb
assert sandwich.taste == "delicious", "Sandwith is not delicious."

Understanding Failure

# 01-getting-started/03/spec/sandwich_spec.rb
defmodule RspecBook.Sandwich do
  defstruct taste: nil, toppings: []
end

Sharing Setup (But Not Sandwiches)

# 01-getting-started/04/spec/sandwich_spec.rb
test "lets me add toppings", %{sandwich: sandwich} do
  sandwich = %Sandwich{taste: "delicious"}
  sandwich = Map.put(sandwich, :toppings, ["cheese" | sandwich.toppings])
  refute Enum.empty?(sandwich.toppings)
end

Hooks

# 01-getting-started/05/spec/sandwich_spec.rb
describe "an ideal sandwich" do
  setup do
    {:ok, sandwich: build(:sandwich) }
  end

# 01-getting-started/05/spec/sandwich_spec.rb
test "is delicious", %{sandwich: sandwich} do
  assert sandwich.taste == "delicious"
end

test "lets me add toppings", %{sandwich: sandwich} do
  sandwich = Map.put(sandwich, :toppings, ["cheese" | sandwich.toppings])
  refute Enum.empty?(sandwich.toppings)
end

letはexunitには無いのでこの以後は無視

Your turn

Exercises

  1. setupしか無いので比較しようがない
  2. https://hexdocs.pm/mix/Mix.Tasks.Test.htmlで読める

Chapter 2

Customizing Your Specs' Output

The Progress Formatter

defmodule RspecBook.CoffeeTest do
  use ExUnit.Case
  doctest RspecBook.Coffee
  alias RspecBook.Coffee

  setup do
    {:ok, coffee: %Coffee{}}
  end

  test "it costs $1", %{coffee: coffee} do
    assert Coffee.price(coffee) == 1.00
  end
  describe "with milk" do
    setup %{coffee: coffee} do
      {:ok, coffee: Coffee.add(coffee, :milk)}
    end

    test "it costs $1.25", %{coffee: coffee} do
      assert Coffee.price(coffee) == 1.25
    end
  end
end
defmodule RspecBook.Coffee do
  defstruct ingredients: []

  def add(coffee, ingredient) do
    Map.put(coffee, :ingredients, [ingredient | coffee.ingredients])
  end

  def price(coffee) do
    1.00
  end
end

The Documentation Formatter

mix test --trace

or make your own formatter and use it.

mix test --formatter=XXX.YYY

Syntax highlighting

Identifying Slow Examples

defmodule RspecBook.SlowTest do
  use ExUnit.Case


  test "it can sleep for 0.1 second", do: Process.sleep(100)
  test "it can sleep for 0.2 second", do: Process.sleep(200)
  test "it can sleep for 0.3 second", do: Process.sleep(300)
  test "it can sleep for 0.4 second", do: Process.sleep(400)
  test "it can sleep for 0.5 second", do: Process.sleep(500)
end
mix test --slowest 2

Running Just What You Need

mix test test/unit
mix test test/unit/specific_test.exs
mix test test/unit test/smoke
mix test test/unit test/foo_test.exs

Running Examples by Name

Running Specific Failures

mix test
mix test test/rspec_book/sandwich_test.exs:17

Running Everything That Failed

mix test
mix test --failed

:failures_manifest_file - specifies a path to the file used to store failures between runs;

defmodule RspecBook.Coffee do
  defstruct ingredients: []

  def add(coffee, ingredient) do
    Map.put(coffee, :ingredients, [ingredient | coffee.ingredients])
  end

  def price(coffee) do
    1.00 + length(coffee.ingredients) * 0.25
  end
end

Focusing Specific Examples

@tag :wip

:include - specifies which tests are run by skipping tests that do not match the filter. Keep in mind that all tests are included by default, so unless they are excluded first, the :include option has no effect. To only run the tests that match the :include filter, exclude the :test tag first (see the documentation for ExUnit.Case for more information on tags);

Tag Filtering

mix test --only wip

Marking Work in Progress

Starting With the Description

test 'it is light in color'
test 'it cooler than 200 degrees Fahrenheit'

Marking Incomplete Work

@tag :skip
test 'it is light in color', %{coffee: coffee} do
  assert Coffee.color(coffee) == :light
end

@tag :skip
test 'it cooler than 200 degrees Fahrenheit', %{coffee: coffee} do
  assert Coffee.temperature(coffee) < 200.0
end

Completing Work In Progress

def color(coffee) do
  case :milk in coffee.ingredients do
    true -> :light
    false -> :dark
  end
end

def temperature(coffee) do
  case :milk in coffee.ingredients do
    true -> 190.0
    false -> 205.0
  end
end

Your Turn

defmodule RspecBook.TeaTest do
  use ExUnit.Case
  doctest RspecBook.Tea
  alias RspecBook.Tea

  setup do
    {:ok, tea: %Tea{}}
  end

  test "it tastes like Earl Grey", %{tea: tea} do
    assert Tea.flavor(tea) == :earl_grey
  end

  test "it is hot", %{tea: tea} do
    assert Tea.temperature(tea) > 200.0
  end
end

Chapter 3

no code

japanese text

note: It is not published without permission of the copyright holder.

Chapter 4

First Step

The Project: An Expense Tracker

no code

Getting Started

mix archive.install hex phx_new 1.5.4
mix phx.new expense_tracker --no-html --no-ecto --no-webpack --no-gettext --no-dashboard
cd expense_tracker
cat mix.exs
...
defp deps do
  [
    {:phoenix, "~> 1.5.4"},
    {:telemetry_metrics, "~> 0.4"},
    {:telemetry_poller, "~> 0.4"},
    {:jason, "~> 1.0"},
    {:plug_cowboy, "~> 2.0"}
  ]
end
mix deps.get

Deciding What to Test First

# test/expense_tracker_web/acceptance/expenses_test.exs
defmodule ExpenseTracker.ExpensesTest do
  use ExpenseTrackerWeb.ConnCase

  @coffee %{
    payee: "Staarbucks",
    amount: 5.75,
    date: "2020-07-28"
  }

  test "it records submitted expenses", %{conn: conn} do
    post conn, "/expenses", @coffee
  end
end

Checking Response

# test/expense_tracker_web/acceptance/expenses_test.exs
# ...
conn = post conn, "/expenses", @coffee
assert json_response(conn, 200)
# lib/expense_tracker_web/router.ex
# ...
scope "/", ExpenseTrackerWeb do
  pipe_through(:api)
  post("/expenses", ExpensesController, :create)
end
# lib/expense_tracker_web/controllers/expenses_controller.ex
defmodule ExpenseTrackerWeb.ExpensesController do
  use ExpenseTrackerWeb, :controller

  def create(conn, _params) do
    json(conn, %{})
  end
end

Filing In the Response Body

# test/expense_tracker_web/acceptance/expenses_test.exs
# ...
assert json = json_response(conn, 200)
assert is_integer(json["expense_id"])
# lib/expense_tracker_web/controllers/expenses_controller.ex
defmodule ExpenseTrackerWeb.ExpensesController do
  use ExpenseTrackerWeb, :controller

  def create(conn, _params) do
    json(conn, %{expense_id: 42})
  end
end

Quering the Data

# test/expense_tracker_web/acceptance/expenses_test.exs
# ...
defp post_expense(conn, params) do
  conn = post conn, "/expenses", params
  assert json = json_response(conn, 200)
  assert is_integer(json["expense_id"])
  Map.put(params, "expense_id", json["expense_id"])
end
# test/expense_tracker_web/acceptance/expenses_test.exs
# ...
@coffee %{"payee" => "Starbucks", "amount" => 5.75, "date" => "2020-07-28"}
# ...
coffee = post_expense(conn, @coffee)
# test/expense_tracker_web/acceptance/expenses_test.exs
# ...
@zoo %{"payee" => "Zoo", "amount" => 15.25, "date" => "2020-07-28"}
@groceries %{"payee" => "Whole Foods", "amount" => 95.2, "date" => "2020-07-29"}
# ...
zoo = post_expense(conn, @zoo)
groceries = post_expense(conn, @groceries)
# test/expense_tracker_web/acceptance/expenses_test.exs
# ...
test "it records submitted expenses", %{conn: conn} do
  coffee = post_expense(conn, @coffee)
  zoo = post_expense(conn, @zoo)
  _groceries = post_expense(conn, @groceries)
  conn = get conn, "/expenses/2020-07-28"
  assert json = json_response(conn, 200)
  assert json == [coffee, zoo]
end
# lib/expense_tracker_web/router.ex
# ...
scope "/", ExpenseTrackerWeb do
  pipe_through(:api)
  post("/expenses", ExpensesController, :create)
  get("/expenses/:date", ExpensesController, :index)
end
# lib/expense_tracker_web/controllers/expenses_controller.ex
# ...
def index(conn, _params) do
  json(conn, [])
end

Saving Your Progress: Pending Spec

# test/expense_tracker_web/acceptance/expenses_test.exs
# ...
@tag skip: true
test "it records submitted expenses", %{conn: conn} do
iex -S mix phx.server
curl localhost:4000/expenses/2020-07-28 -w "\n"

Your Turn

no code

Chapter 5

From Acceptance spec to unit spec

A Better Testing Experience

no trans

Sketching the Behavior

# test/expense_tracker_web/controllers/expenses_controller_test.exs
defmodule ExpenseTrackerWeb.ExpensesControllerTest do
  use ExpenseTrackerWeb.ConnCase

  describe ".create/2" do
    test "it returns id when successfully recorded"
    test "it responds with a 200 (OK) when successfully recorded"

    # ... next content go here ...
  end
end
# test/expense_tracker_web/controllers/expenses_controller_test.exs
# ...
test "it returns an error message when the expense fails validation"
test "it responds with a 422 (Unprocessable entity) when the expense fails validation"

Filling In the First Spec

Connecting to Storage

see diff

case ExpenseTracker.Repo.insert(%Ledger{some: "data"}) do
  {:ok, struct} -> struct.id
  {:error, changeset} -> changeset.errors
end

Test Doubles: Mocks, Stubs, and Others

# mix.exs
defp deps do
  [
    ...
    {:hammox, "~> 0.2", only: :test}
  ]
end
# lib/expense_tracker/recording/behaviour.ex
defmodule ExpenseTracker.Recording.Behaviour do
  @callback record(map) :: {:ok, map} | {:error, any}
end
# config/test.ex
# ...
config :expense_tracker, :behaviour,
  recording: RecordingMock
# test/expense_tracker_web/controllers/expenses_controller_test.exs
defmodule ExpenseTrackerWeb.ExpensesControllerTest do
  use ExpenseTrackerWeb.ConnCase
  import Mox
  alias ExpenseTracker.Recording

  describe ".create/2" do
    @expense %{"some" => "data"}
    defmock(RecordingMock, for: Recording.Behaviour)
    setup do
      expect(RecordingMock, :record, fn @expense ->
        {:ok, %{id: 417}}
      end)
      :ok
    end

    test "it returns id and responds with a 200 (OK) when successfully recorded", %{conn: conn} do
      conn = post conn, "/expenses", @expense
      assert json = json_response(conn, 200)
      assert 417 == json["expense_id"]
    end

    # ...
  end
end

Handling Success

# lib/expense_tracker_web/controllers/expenses_controller.ex
# ...
def create(conn, params) do
  case recording().record(params) do
    {:ok, %{id: id}} -> json(conn, %{expense_id: id})
    _else -> raise "fail pass not implemented yet"
  end
end

defp recording, do: Application.get_env(:expense_tracker, :behaviour)[:recording]
  alias Plug.Conn

  Conn.put_status(conn, :unprocessable_entity)
  |> json(%{expense_id: id})

Refactoring

@expense %{"some" => "data"}
defmock(RecordingMock, for: Recording.Behaviour)
setup do
  expect(RecordingMock, :record, fn @expense ->
    {:ok, %{id: 417}}
  end)
  :ok
end

test "it returns id and responds with a 200 (OK) when successfully recorded", %{conn: conn} do
  conn = post conn, "/expenses", @expense
  assert json = json_response(conn, 200)
  assert 417 == json["expense_id"]
end

Handling Failure

@expense %{"some" => "data"}
@invalid_expense %{"some" => "invalid"}
defmock(RecordingMock, for: Recording.Behaviour)
setup do
  expect(RecordingMock, :record, fn
    @expense -> {:ok, %{id: 417}}
    @invalid_expense -> {:error, %{error: "expense incomplete"}}
  end)
  :ok
end
test "it returns an error message and responds with a 422 (Unprocessable entity) when the expense fails validation", %{conn: conn} do
  conn = post conn, "/expenses", @invalid_expense
  assert json = json_response(conn, 422)
  assert "expense incomplete" == json["error"]
end
{:error, error} ->
  Conn.put_status(conn, :unprocessable_entity)
  |> json(error)

Defining the Ledger

defmodule ExpenseTracker.Recording do
  @behaviour ExpenseTracker.Recording.Behaviour
  def record(_), do: {:ok, %{}}
end

Your Turn

Exercises

Reducing duplication
assert json = json_response(conn, 200)
# assert json do something
Implementing the GET Routes
describe ".index/2" do
  test "it returns expense records as JSON and responds with a 200 (OK) when expenses exist on the given date"
  test "it returns empty array as JSON and responds with a 200 (OK) when no expenses on the given date"
end

Chapter 6

Hooking Up the database

Getting to Know Sequel

Creating a Database

mix ecto.gen.migrate create_expenses
defmodule ExpenseTracker.Repo.Migrations.CreateExpenses do
  use Ecto.Migration

  def change do
    create table(:expenses) do
      add :payee, :string
      add :amount, :float
      add :date, :date
    end
  end
end
$ mix ecto.create
The database for ExpenseTracker.Repo has been created
$ mix ecto.migrate

08:14:36.965 [info]  == Running 20200810231027 ExpenseTracker.Repo.Migrations.CreateExp

08:14:36.967 [info]  create table posts

08:14:36.991 [info]  == Migrated 20200810231027 in 0.0s

Testing Ledger Behaviour

see test/support/data_case.ex

defmodule ExpenseTracker.RecordingTest do
  use ExpenseTrackerWeb.DataCase
  alias ExpenseTracker.Recording
  describe ".record/1" do
    # ... contexts go here ...
  end
end
@expense %{
  "payee" => "Starbucks",
  "amount" => 5.75,
  "date" => "2020-08-11"
}
test "successfully saves the expense in the DB with valid expense" do
  assert {:ok, result} = Recording.record(@expense)
  assert [%Expense{payee: "Starbucks", amount: 5.75, date: ~D[2020-08-11]} = result] == Repo.all(Expense)
end

Testing the Invalid Case

@expense %{
  "amount" => 5.75,
  "date" => "2020-08-11"
}
test "rejects the expecse as invalid when the expense lacks a payee" do
  assert {:error, changeset} = Recording.record(@expense)
  assert %{payee: ["can't be blank"]} == errors_on(changeset)
end

Isolating Your Specs Using Database Translations

see test/support/data_case.ex

Filling in the Behavior

defmodule ExpenseTracker.Recording do
  @behaviour ExpenseTracker.Recording.Behaviour
  alias ExpenseTracker.Recording.Expense
  alias ExpenseTracker.Repo

  def record(expense) do
    expense
    |> Expense.changeset()
    |> Repo.insert()
  end
end
defmodule ExpenseTracker.Recording.Expense do
  use Ecto.Schema
  alias Ecto.Changeset

  @type t :: %__MODULE__{}

  schema "expenses" do
    field(:payee)
    field(:amount, :float)
    field(:date, :date)
  end

  @spec changeset(map) :: Changeset.t()
  def changeset(params) do
    Changeset.cast(%__MODULE__{}, params, [:payee, :amount, :date])
    |> Changeset.validate_required([:payee, :amount, :date])
  end
end

Quering Expenses

describe ".expenses_on/1" do
  @expense %{
    "payee" => "Starbucks",
    "amount" => 5.75,
    "date" => "2020-08-11"
  }
  test "returns all expenses for the provided date" do
    assert {:ok, result_1} = Recording.record(@expense)
    assert {:ok, result_2} = Recording.record(@expense)
    assert {:ok, result_3} = Recording.record(Map.put(@expense, "date", "2020-08-10"))
    assert {:ok, [result_1, result_2]} == Recording.expenses_on("2020-08-11")
  end

  test "returns a blank list when there are no matching expenses" do
    assert {:ok, []} == Recording.expenses_on("2020-08-11")
  end
end
def expenses_on(date) do
  Expense
  |> where(date: ^date)
  |> Repo.all()
  |> (fn data -> {:ok, data} end).()
end

Ensuring the Application Works for Real

iex -S mix phx.server
curl localhost:4000/expenses/2020-08-11 -w "\n"
mix ecto.create
mix ecto.migrate
curl localhost:4000/expenses --data '{"payee": "Zoo", "amount": 10, "date": "2020-08-11"}'
curl localhost:4000/expenses --data '{"payee": "Starbucks", "amount": 7.5, "date": "2020-08-11"}'

Your Turn

Chapter 7

Getting Words Right

The basic

describe
# by module name
defmodule RspecBook.GardenTest do
end

defmodule RspecBookTest do
end

# by module name and use case
defmodule RspecBook.GardenInWinterTest do
end

Other Ways to Get the Words Right

context Instead of describe

see: https://elixirforum.com/t/how-to-describe-many-contexts-in-exunit-without-a-hierarchy/1551

example Instead of it

skip

specify Instead of it

skip

Defining Your Own Names

see: https://elixirforum.com/t/tagging-all-tests-that-use-a-particular-case/4351/2

Sharing Common Logic

you can use hook and helper function

defmodule RspecBook.GardenTest do

  # hook
  def setup(_) do
    {:ok, user: build(:user)}
  end

  # helper method
  def build(:user) do
    %User{id: 1, username: "admin", password: "some-very-secure-password"}
  end
end

Hooks

Type

see: https://hexdocs.pm/ex_unit/ExUnit.Callbacks.html

  • before -> setup/1, setup_all/1
  • after -> on_exit/1
  • around -> x
before and after
around
Config hooks

see: https://hexdocs.pm/ex_unit/ExUnit.Case.html#module-tags

Scope
When to Use Hooks

Helper Methods

Putting Your Helper in a Module
Include Modules Automatically

Not Recommended! http://blog.lucidsimple.com/2016/01/31/exunit-cheat-sheet.html#load

Sharing Example Groups

see: https://blog.codeminer42.com/how-to-test-shared-behavior-in-elixir-3ea3ebb92b64/

Sharing Contexts

Sharing Examples

Nesting
Customizing Sharing Groups With Blocks

Your Turn

Chapter 8

Defining Metadata

Metadata Defined By RSpec

defmodule RspecBook.MetadataTest do
  use ExUnit.Case

  test "is used by ExUnit for context", context do
    IO.inspect(context)
  end
end

see: https://hexdocs.pm/ex_unit/ExUnit.Case.html#module-known-tags

Custom Metadata

see: https://hexdocs.pm/ex_unit/ExUnit.Case.html#module-tags

Derived Metadata

mix test --only xxxx

Default Metadata

see: https://hexdocs.pm/ex_unit/ExUnit.Case.html#module-filters

Reading Metadata

Selecting Which Specs to Run

Filtering

mix test --only xxxx

Excluding Examples

see: https://hexdocs.pm/ex_unit/ExUnit.Case.html#module-filters

Including Examples

see: https://hexdocs.pm/ex_unit/ExUnit.Case.html#module-filters

The Command Line

Sharing Code Conditionally

Changing How Your Specs Run

Your Turn

Chapter 9

Command-Line Configuration

Environment Options

https://hexdocs.pm/mix/Mix.Tasks.Test.html#module-configuration

  • def deps

Filtering Options

https://hexdocs.pm/mix/Mix.Tasks.Test.html#module-filters

Output Options

https://hexdocs.pm/mix/Mix.Tasks.Test.html#module-command-line-options

Setting Command-Line Defaults

https://hexdocs.pm/ex_unit/1.10.4/ExUnit.html#configure/1-options

Using a Custom Formatter

https://elixirforum.com/t/registering-a-custom-formatter-for-exunit/1472

Setting Up the Formatter

How Formatter Work

Getting RSpec to Use the Formatter
Cleaning Up the Output

RSpec.configure

https://hexdocs.pm/ex_unit/1.10.4/ExUnit.html#configure/1-options

Hooks

Sharing Code Using Modules

Filtering

Metadata

Output Options

Library Configuration

Mocks
Expectations

Other Useful Options

Zero Monkey-Patching Mode
Random Order
Adding Your Own Settings

Your Turn

rspec_book_in_exunit's People

Contributors

marocchino avatar

Watchers

 avatar  avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.