Skip to content

Property based testing with StreamData #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[
import_deps: [:ecto, :phoenix],
import_deps: [:ecto, :phoenix, :stream_data],
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
subdirectories: ["priv/*/migrations"]
]
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,40 @@ Welcome to Elixir School's workshop on Real-Time Phoenix with Channels, PubSub,

Collaborators can sign in with a username and click a button to start the "pointing party". Tickets are displayed for estimation and each user can cast their store point estimate vote. Once all the votes are cast, a winner is declared and the participants can move on to estimate the next ticket.

The master branch of this app represents the starting state of the code for this workshop. Clone down and make sure you're on the master branch in order to follow along.
The master branch of this app represents the starting state of the code for this workshop. Clone down and make sure you're on the master branch in order to follow along.

## Resources

### Phoenix Channels

* [Official guide](https://hexdocs.pm/phoenix/channels.html)
* [API documentation](https://hexdocs.pm/phoenix/Phoenix.Channel.html#content)

### Phoenix PubSub

* [API documentation](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html)

### Phoenix LiveView

* [LiveView announcement](https://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript) by Chris McCord
* [API documentation](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html)
* ["Walk-Through of Phoenix LiveView"](https://elixirschool.com/blog/phoenix-live-view/) by Sophie DeBenedetto
* ["Building Real-Time Features with Phoenix Live View and PubSub"](https://elixirschool.com/blog/live-view-with-pub-sub/) by Sophie DeBenedetto
* ["Using Channels with LiveView for Better UX"](https://elixirschool.com/blog/live-view-with-channels/) by Sophie DeBenedetto
* ["Tracking Users in a Chat App with LiveView, PubSub Presence"](https://elixirschool.com/blog/live-view-with-presence/) by Sophie DeBenedetto

### Property-based Testing and StreamData

* [StreamData on GitHub](https://github.com/whatyouhide/stream_data)
* [StreamData documentation](https://hexdocs.pm/stream_data/StreamData.html)
* [Elixir School article on StreamData](https://elixirschool.com/en/lessons/libraries/stream-data/)
* [_Property-Based Testing with PropEr, Erlang, and Elixir_ and _PropEr Testing_](https://propertesting.com/) by Fred Hebert
* ["An introduction to property-based testing"](https://fsharpforfunandprofit.com/posts/property-based-testing/) by Scott Wlaschin
* ["Choosing properties for property-based testing"](https://fsharpforfunandprofit.com/posts/property-based-testing-2/) by Scott Wlaschin

### Estimation

* [_Agile Estimating and Planning_](https://www.mountaingoatsoftware.com/books/agile-estimating-and-planning) by Mike Cohn of Mountain Goat Software
* [planningpoker.com](https://www.planningpoker.com/) is a full-featured estimation tool that may work well for your team.

Thanks to James Grenning and Mike Cohn for their inspiration and their work in software estimation!
3 changes: 1 addition & 2 deletions config/cards.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ config :pointing_party,
Update the application to save an individual's vote and the final card points to the database.
"""
},

%{
title: "Add Guardian dependency",
description: """
Expand All @@ -34,5 +33,5 @@ config :pointing_party,
description: """
Update our existing authentication flow to use the newly created Auth module.
"""
},
}
]
1 change: 1 addition & 0 deletions lib/pointing_party/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule PointingParty.Account do

def create(attrs) do
changeset = changeset(%Account{}, attrs)

if changeset.valid? do
account = apply_changes(changeset)
{:ok, account}
Expand Down
11 changes: 4 additions & 7 deletions lib/pointing_party/vote_calculator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,7 @@ defmodule PointingParty.VoteCalculator do
end

defp get_points(score_card, users) do
votes =
users
|> Enum.map(fn {_username, %{metas: [%{points: points}]}} ->
points
end)
votes = Enum.map(users, fn {_username, %{metas: [%{points: points}]}} -> points end)

update_score_card(score_card, :votes, votes)
end
Expand Down Expand Up @@ -57,9 +53,10 @@ defmodule PointingParty.VoteCalculator do

defp handle_tie(%{majority: nil, calculated_votes: calculated_votes}) do
calculated_votes
|> Enum.sort_by(&elem(&1, 1))
|> Enum.take(2)
|> Enum.sort_by(&elem(&1, 1), &>=/2)
|> Enum.map(&elem(&1, 0))
|> Enum.sort()
|> Enum.take(2)
end

defp handle_tie(%{majority: majority}), do: majority
Expand Down
5 changes: 3 additions & 2 deletions lib/pointing_party_web/channels/presence.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
defmodule PointingPartyWeb.Presence do
use Phoenix.Presence, otp_app: :pointing_party,
pubsub_server: PointingParty.PubSub
use Phoenix.Presence,
otp_app: :pointing_party,
pubsub_server: PointingParty.PubSub
end
1 change: 1 addition & 0 deletions lib/pointing_party_web/channels/room_channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ defmodule PointingPartyWeb.RoomChannel do
end

defp initialize_state(%{assigns: %{cards: _cards}} = socket), do: socket

defp initialize_state(socket) do
[first | cards] = Card.cards()

Expand Down
4 changes: 2 additions & 2 deletions lib/pointing_party_web/controllers/session_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ defmodule PointingPartyWeb.SessionController do
|> put_session(:username, username)
|> redirect(to: "/cards")
|> halt()

{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end

def delete(conn, _params) do
clear_session(conn)
|> redirect(to: "/login") |> halt()
clear_session(conn) |> redirect(to: "/login") |> halt()
end
end
2 changes: 1 addition & 1 deletion lib/pointing_party_web/plugs/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ defmodule PointingPartyWeb.Plugs.Auth do
end

defp authenticate(conn) do
get_session(conn, :username)
get_session(conn, :username)
end
end
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ defmodule PointingParty.MixProject do
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
{:ex_machina, "~> 2.3", only: :test}
{:ex_machina, "~> 2.3", only: :test},
{:mix_test_watch, "~> 0.8", only: :dev, runtime: false},
{:stream_data, "~> 0.1", only: :test}
]
end
end
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
"mix_test_watch": {:hex, :mix_test_watch, "0.9.0", "c72132a6071261893518fa08e121e911c9358713f62794a90c95db59042af375", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix": {:hex, :phoenix, "1.4.9", "746d098e10741c334d88143d3c94cab1756435f94387a63441792e66ec0ee974", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
Expand All @@ -21,5 +22,6 @@
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
"stream_data": {:hex, :stream_data, "0.4.3", "62aafd870caff0849a5057a7ec270fad0eb86889f4d433b937d996de99e3db25", [:mix], [], "hexpm"},
"telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
}
5 changes: 2 additions & 3 deletions priv/repo/migrations/20190708014847_create_cards.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ defmodule PointingParty.Repo.Migrations.CreateCards do

def change do
create table(:cards) do
add :title, :string
add :description, :string
add(:title, :string)
add(:description, :string)

timestamps()
end

end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule PointingParty.Repo.Migrations.AddPointsToCards do

def change do
alter table("cards") do
add :points, :integer, default: 0
add(:points, :integer, default: 0)
end
end
end
3 changes: 1 addition & 2 deletions priv/repo/seeds.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.


alias PointingParty.{Card, Repo}

cards = [
Expand All @@ -21,7 +20,7 @@ cards = [
%{title: "Fifth card", description: "This is a description of the fifth card."},
%{title: "Sixth card", description: "This is a description of the sixth card."},
%{title: "Seventh card", description: "This is a description of the seventh card."},
%{title: "Eighth card", description: "This is a description of the eighth card."},
%{title: "Eighth card", description: "This is a description of the eighth card."}
]

Enum.each(cards, fn card ->
Expand Down
103 changes: 88 additions & 15 deletions test/pointing_party/vote_calculator_test.exs
Original file line number Diff line number Diff line change
@@ -1,23 +1,96 @@
defmodule PointingParty.VoteCalculatorTest do
use ExUnit.Case, async: true
use ExUnitProperties

@users_with_winner %{
"sean" => %{metas: [%{points: 1}]},
"michael" => %{metas: [%{points: 3}]},
"sophie" => %{metas: [%{points: 3}]}
}
alias PointingParty.{Card, VoteCalculator}

@users_with_tie %{
"sean" => %{metas: [%{points: 1}]},
"michael" => %{metas: [%{points: 2}]},
"sophie" => %{metas: [%{points: 3}]}
}
# Properties
# --------------
# When there is a tie, the result will be a list
# The tie list will be sorted in increasing order
# The tie list will contain unique elements
# The tie list will have exactly two elements
# The tie list contains only integers
# The tie list integers must be valid voting options
# The greatest element in the tie list will not be greater than the highest vote
#
# When there is a winner, the result will be an integer
# When there is a winner, the result will be one of the valid voting options
# When there is a winner, it will not be greater than the highest vote
#
# Notes
# --------------
# Ties can happen when there are even or odd numbers of players
# Winning votes can also have an even or odd number of players
# Single-player games will always have a winner

test "calculate_votes/1 calculates when there is a winner" do
{"winner", 3} = PointingParty.VoteCalculator.calculate_votes(@users_with_winner)
end

test "calculate_votes/1 calculates when there is a tie" do
{"tie", [1,2]} = PointingParty.VoteCalculator.calculate_votes(@users_with_tie)
describe "calculate_votes/1" do
setup do
points_map = fixed_map(%{
points: member_of(Card.points_range())
})
metas_map = fixed_map(%{
metas: list_of(points_map, length: 1)
})
user_generator = nonempty(map_of(string(:alphanumeric), metas_map))

[user_generator: user_generator]
end

property "calculated vote is a list or an integer", %{user_generator: user_generator} do
check all users <- user_generator,
{_event, winner} = VoteCalculator.calculate_votes(users),
max_runs: 20 do
assert is_list(winner) || is_integer(winner)
end
end

property "the winning value is not more than the highest vote", %{user_generator: user_generator} do
check all users <- user_generator,
max_runs: 20 do
max_vote =
users
|> Enum.map(fn {_username, %{metas: [%{points: points}]}} -> points end)
|> Enum.max()

case PointingParty.VoteCalculator.calculate_votes(users) do
{"winner", winner} -> assert winner <= max_vote
{"tie", [_lesser, greater]} -> assert greater <= max_vote
end
end
end

property "when there is a winner, calculated vote is a valid integer", %{user_generator: user_generator} do
check all users <- user_generator,
{event, winner} = PointingParty.VoteCalculator.calculate_votes(users),
max_runs: 20 do
if event == "winner" do
assert winner in Card.points_range()
end
end
end

property "when there is a tie, calculated vote is a list with two sorted values", %{user_generator: user_generator} do
check all users <- user_generator,
{event, votes} = PointingParty.VoteCalculator.calculate_votes(users),
max_runs: 20 do
if event == "tie" do
[lesser, greater] = votes

assert lesser < greater
end
end
end

property "when there is a tie, calculated vote is a list of valid integers", %{user_generator: user_generator} do
check all users <- user_generator,
{event, votes} = PointingParty.VoteCalculator.calculate_votes(users),
max_runs: 20 do
if event == "tie" do
assert Enum.all?(votes, fn vote -> vote in Card.points_range() end)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule PointingPartyWeb.CardControllerTest do
@username "test_user"

describe "authenticated user" do
setup %{conn: conn} do
setup %{conn: conn} do
auth_conn = Plug.Test.init_test_session(conn, username: @username)
{:ok, %{conn: auth_conn}}
end
Expand Down
6 changes: 3 additions & 3 deletions test/pointing_party_web/controllers/page_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ defmodule PointingPartyWeb.PageControllerTest do
@username "test_user"

describe "authenticated user" do
setup %{conn: conn} do
conn = conn
|> Plug.Conn.assign(:username, @username)
setup %{conn: conn} do
conn = Plug.Conn.assign(conn, :username, @username)

{:ok, %{conn: conn}}
end

Expand Down