Slide 1

Slide 1 text

Building Bulletproof Real-Time Applications with Phoenix, LiveView, and StreamData

Slide 2

Slide 2 text

Introductions 2

Slide 3

Slide 3 text

Hello! I’m Sophie I’m an engineer and sometimes teacher at The Flatiron School where I build education and business tools in Elixir and Ruby. @sm_debenedetto 3 *and this is my dog

Slide 4

Slide 4 text

And... I’m Michael I’m a lead engineer at RentPath, where we build websites to help people find a place to live. @michaelstalker 4 *and this is my family

Slide 5

Slide 5 text

Elixir School A free, open-source Elixir curriculum Through the hard work of many volunteers around the world, we’ve developed and translated content covering everything from intro topics to deep dives. 5

Slide 6

Slide 6 text

Get Involved! Help us grow the Elixir School community Write a lesson, contribute a TIL (or longer!) blog post or provide a translation. Open an issue here describing your idea or simply open a PR with your work to get started. 6

Slide 7

Slide 7 text

Goals 7

Slide 8

Slide 8 text

What We’ll Learn ○ Phoenix LiveView ○ Phoenix PubSub ○ Phoenix Presence ○ Property-based Testing with StreamData 8

Slide 9

Slide 9 text

What We’ll Build A real-time ticket estimation tool ○ Users can see other users in the “estimation room” in real-time ○ Users can vote on tickets ○ Users can see the winning vote tallied in real-time and move on to the next ticket 9

Slide 10

Slide 10 text

Pointing Party A collaborative ticket estimation tool for your team 10

Slide 11

Slide 11 text

11

Slide 12

Slide 12 text

User Starts Estimation Round When I click “start pointing!” Then the first card for estimation appears for all users in the room Features Real-Time User Presence When a new user joins/leaves Then I see the list of present users update in the UI 12

Slide 13

Slide 13 text

Ticket is Estimated When another user submits a vote Then I see the UI update to indicate they have voted Features Winner is Calculated When the last user submits a vote Then all users see the winning vote And a user can click “next card” 13

Slide 14

Slide 14 text

Application Starting State Log In with a username (We built some dummy authorization flows) See the static ticket estimation page Click the start button and... Oh no you can’t estimate a ticket! 14

Slide 15

Slide 15 text

We Need Real-Time! In the current state of our app, users cannot collaborate on ticket estimation. 15

Slide 16

Slide 16 text

What is the Real-Time Web? Users receive new information from the server as soon as it is available—no request required This represents a significant departure from traditional HTTP communication Made possible by WebSockets 16

Slide 17

Slide 17 text

17 WebSockets

Slide 18

Slide 18 text

What are WebSockets? A Protocol built on top of TCP that allows for bi-directional, "full-duplex" communication between the client and the server by creating a persistent connection between the two. 18

Slide 19

Slide 19 text

19 Server Client Request GET https://elixirschool.com Response

Weclome to Elixir School!

HTTP Protocol

Slide 20

Slide 20 text

20 Server Client Step 1. Request to initiate WS connection Client Client GET / Host: elixirschool.com Upgrade: websocket Connection: upgrade

Slide 21

Slide 21 text

21 Server Client Step 2. Open and maintain WS connections Client Client

Slide 22

Slide 22 text

22 We’ll use WebSockets to maintain a persistent, bi-directional and stateful connection between our ticket estimation clients and the server with the help of Phoenix LiveView

Slide 23

Slide 23 text

23 Phoenix LiveView

Slide 24

Slide 24 text

What is LiveView? Phoenix LiveView leverages server-rendered HTML and Phoenix’s native WebSocket tooling so you can build fancy real-time features without all that complicated JavaScript. 24

Slide 25

Slide 25 text

25 So...I don’t need to write JavaScript ever again?

Slide 26

Slide 26 text

26 Not exactly.

Slide 27

Slide 27 text

27 You just don’t need it as often.

Slide 28

Slide 28 text

28 How It Works

Slide 29

Slide 29 text

Connecting ● Client connects to Phoenix server over WebSockets, at the LiveView socket endpoint ● Phoenix server starts a LiveView process for that user’s socket 29

Slide 30

Slide 30 text

Rendering the Template ● The LiveView leex (Live EEX) template is rendered, using the data from the LiveView process’ state 30

Slide 31

Slide 31 text

Handling Changes ● Certain user interactions on the page will send a message over the socket to the LiveView process ● The LiveView process will update its state and push a message back down the socket ● The leex template will re-render with the new state 31

Slide 32

Slide 32 text

32 A Closer Look

Slide 33

Slide 33 text

Roadmap 3. Handling Changes 2. Connecting to LiveView 33 1. Configuring LiveVIew

Slide 34

Slide 34 text

34 1. Configuring LiveView

Slide 35

Slide 35 text

35 Install the dependency

Slide 36

Slide 36 text

36 # mix.exs def deps do [ {:phoenix_live_view, github: "phoenixframework/phoenix_live_view"} ] end

Slide 37

Slide 37 text

37 Update our app’s config with the signing salt for our LiveView socket connection

Slide 38

Slide 38 text

38 config :my_app, PointingParty.Endpoint, ... live_view: [ signing_salt: "YOUR_SECRET" ]

Slide 39

Slide 39 text

39 Configure our app to support “Live EEX” templates

Slide 40

Slide 40 text

40 config :phoenix, template_engines: [ leex: Phoenix.LiveView.Engine ]

Slide 41

Slide 41 text

41 Import the LiveView render functions

Slide 42

Slide 42 text

42 # lib/pointing_party_web.ex def view do quote do ... import Phoenix.LiveView, only: [live_render: 2, live_render: 3] end end

Slide 43

Slide 43 text

43 Add the LiveView JS library to our NPM dependencies

Slide 44

Slide 44 text

44 # assets/package.json { "dependencies": { ... "phoenix_live_view": "file:../deps/phoenix_live_view" } }

Slide 45

Slide 45 text

45 Add the LiveView path to our dev live reload config

Slide 46

Slide 46 text

46 # config/dev.exs config :pointing_party, PointingPartyWeb.Endpoint, live_reload: [ patterns: [ ..., ~r{lib/my_app_web/live/.*(ex)$} ] ]

Slide 47

Slide 47 text

Roadmap 3. Handling Changes 2. Connecting to LiveView 47 1. Configuring LiveVIew

Slide 48

Slide 48 text

48 2. Connecting to LiveView

Slide 49

Slide 49 text

Connection Process ● User visits ”/cards” in the browser 49

Slide 50

Slide 50 text

Connection Process ● User visits ”/cards” in the browser ● CardController mounts the LiveView 50

Slide 51

Slide 51 text

Connection Process ● User visits ”/cards” in the browser ● CardController mounts the LiveView ● The LiveView renders the LEEX template as static HTML 51

Slide 52

Slide 52 text

Connection Process ● User visits ”/cards” in the browser ● CardController mounts the LiveView ● The LiveView renders the LEEX template as static HTML ● A JS snippet on that static HTML page sends the “WebSocket connect” request 52

Slide 53

Slide 53 text

Connection Process ● User visits ”/cards” in the browser ● CardController mounts the LiveView ● The LiveView renders the LEEX template as static HTML ● A JS snippet on that static HTML page sends the “WebSocket connect” request ● The LiveView process re-renders the template over WebSockets, is now listening for events from the client 53

Slide 54

Slide 54 text

54 LiveView Client Request GET /cards Render LEEX Template

Start pointing!

Step 1. Mount the LiveView + render static HTML CardController Mount LiveView

Slide 55

Slide 55 text

55 Client Connect to LiveView socket Step 2. Establish WS connection LiveView LV Socket

Slide 56

Slide 56 text

56 Client Connect to LiveView socket Step 3. Re-render the template LiveView Re-render template over WS connection LV Socket

Slide 57

Slide 57 text

We Will ● Mount the LiveView socket endpoint ● Define a LiveView and LEEX template ● Teach our controller to render the LiveView 57

Slide 58

Slide 58 text

58 Mount the LiveView socket at the “/live” endpoint

Slide 59

Slide 59 text

59 defmodule PointingPartyWeb.Endpoint do use Phoenix.Endpoint, otp_app: :pointing_party socket "/live", Phoenix.LiveView.Socket end

Slide 60

Slide 60 text

60 Define The LiveView

Slide 61

Slide 61 text

61 # app/pointing_party_web/live/card_live.ex defmodule PointingPartyWeb.CardLive do use Phoenix.LiveView alias PointingPartyWeb.CardView def render(assigns) do Phoenix.View.render(CardView, "index.html", assigns) end def mount(_session, socket) do {:ok, assign(socket, is_pointing: false)} end end

Slide 62

Slide 62 text

62 Define The LEEX Template

Slide 63

Slide 63 text

63
<%= if !@is_pointing do %>
Start the Party!
<% else %> <% end %>

Slide 64

Slide 64 text

64 Include the LiveView socket connect JS

Slide 65

Slide 65 text

65 // assets/js/app.js import LiveSocket from "phoenix_live_view" let liveSocket = new LiveSocket("/live") liveSocket.connect()

Slide 66

Slide 66 text

66 Render the LiveView from the controller

Slide 67

Slide 67 text

67 defmodule PointingPartyWeb.CardController do use PointingPartyWeb, :controller import Phoenix.LiveView.Controller def index(conn, _params) do %{assigns: %{username: username}} = conn live_render(conn, PointingPartyWeb.CardLive, session: %{username: username}) end end

Slide 68

Slide 68 text

68 Now, when the user visits “/cards”, the LiveView process will start

Slide 69

Slide 69 text

69 And render the static LEEX template

Slide 70

Slide 70 text

70
...

Slide 71

Slide 71 text

71 This will send the LiveView socket connect request

Slide 72

Slide 72 text

72 Opening a bi-directional, stateful connection

Slide 73

Slide 73 text

73 Over which the LEEX template will re-render

Slide 74

Slide 74 text

74 3. Handling Changes

Slide 75

Slide 75 text

Roadmap 3. Handling Changes 2. Connecting to LiveView 75 1. Configuring LiveVIew

Slide 76

Slide 76 text

76
<%= if !@is_pointing do %>
Start the Party!
<% else %> <% end %>

Slide 77

Slide 77 text

We Will ● Add a DOM element binding that will send an event from the client to LiveView 77

Slide 78

Slide 78 text

We Will ● Add a DOM element binding that will send an event from the client to LiveView ● Teach our LiveView how to handle this event 78

Slide 79

Slide 79 text

79 Add the DOM element binding

Slide 80

Slide 80 text

80 Start the Party!

Slide 81

Slide 81 text

81 The phx-click event will be sent over the WS to our LiveView

Slide 82

Slide 82 text

82 Which will invoke the matching handle_event/3 function

Slide 83

Slide 83 text

83 def handle_event("start_party", _value, socket) do # coming soon! end

Slide 84

Slide 84 text

84 def handle_event("start_party", _value, socket) do [current_card | remaining_cards] = Card.cards() socket = assign(socket, is_pointing: true, current_card: current_card, remaining_cards: remaining_cards) {:noreply, socket} end

Slide 85

Slide 85 text

85 Re-rendering the template

Slide 86

Slide 86 text

86
<%= if !@is_pointing do %>
Start the Party!
<% else %> <% end %>

Slide 87

Slide 87 text

87

<%= @current_card.title %>

<%= @current_card.description %>

Story Points 0 1 3 5

Slide 88

Slide 88 text

Your Turn!

Slide 89

Slide 89 text

Fork this repo

Slide 90

Slide 90 text

Feature Roadmap 2. Users see list of present users 3. All votes are tallied, displayed 1. User initiates pointing round 90

Slide 91

Slide 91 text

91

Slide 92

Slide 92 text

User initiates pointing round 92 Define the LiveView ● Define a LiveView and LEEX template - We did this for you :) ● Render the LiveView from the controller - We did this for you :) Handle Events ● Add a phx-click event to the “start pointing” button ● Define a handle_event/3 function to match this event and update socket.assigns accordingly Configure LiveView ● We did this for you :)

Slide 93

Slide 93 text

Building Bulletproof Real-Time Applications Part 2: Phoenix PubSub

Slide 94

Slide 94 text

94 We Have a Problem

Slide 95

Slide 95 text

95 The pointing round is only started for the single user who clicked “start pointing”

Slide 96

Slide 96 text

96 How can we broadcast shared changes to all of our users?

Slide 97

Slide 97 text

97 We need PubSub!

Slide 98

Slide 98 text

98 Phoenix PubSub

Slide 99

Slide 99 text

What is PubSub? PubSub (“publish/subscribe”) is a pattern in which we publish messages to a “topic”, such that those messages can be consumed by any number of subscribers. 99

Slide 100

Slide 100 text

What is Phoenix PubSub? The Phoenix PubSub library allows us subscribe Elixir processes to a shared topic and publish messages to those processes. Phoenix Channels abstract away the interactions with Phoenix PubSub, but you can also use the PubSub library directly, which is exactly what we’ll do from within our LiveView. 100

Slide 101

Slide 101 text

Phoenix PubSub and Distributed Elixir Phoenix PubSub is configured by default to be distribution-friendly with Phoenix.PubSub.PG2. With this adapter, messages are broadcast to processes sharing a topic across nodes. 101

Slide 102

Slide 102 text

102 How It Works

Slide 103

Slide 103 text

103 Subscribe users to a shared topic

Slide 104

Slide 104 text

104 A user “joins” a pointing room when they mount the LiveView

Slide 105

Slide 105 text

105 This is when we will subscribe that LiveView process to a shared PubSub topic

Slide 106

Slide 106 text

106 Broadcasting Messages

Slide 107

Slide 107 text

107 When a user sends an event to LiveView, we will broadcast it over PubSub to all the other users’ LiveViews

Slide 108

Slide 108 text

108 Our LiveView code will implement a handle_info/3 function that receives the broadcast, updates state, and causes the template to re-render

Slide 109

Slide 109 text

109 Client Submit “Start_pointing” event LiveView Client Client LiveView LiveView PubSub Broadcast to subscribers handle_info and update socket Re-render template

Slide 110

Slide 110 text

Building Bulletproof Real-Time Applications Part 3: Phoenix Presence

Slide 111

Slide 111 text

111 We Have A Problem

Slide 112

Slide 112 text

112 We don’t know which users are participating, who has voted or what their vote is!

Slide 113

Slide 113 text

113 We don’t know which users are participating, who has voted or what their vote is!

Slide 114

Slide 114 text

114 How can we sync and share stateful info, like who is present, across LiveView processes?

Slide 115

Slide 115 text

115 Phoenix Presence

Slide 116

Slide 116 text

What is Phoenix Presence? The Phoenix.Presence module allows us to: ● Register a process under a given topic ● Store that info in a decentralized and resilient data store. ● Broadcast presence-related events and sync presence data with ease. 116

Slide 117

Slide 117 text

117 Phoenix Presence + Distributed Elixir =

Slide 118

Slide 118 text

Phoenix Presence Uses a CRDT A CRDT (Conflict-free Replicated Data Type) backs Phoenix Presence. What’s so special about a CFRDT? Unlike centralized data stores like Redis, Phoenix Presence… 118

Slide 119

Slide 119 text

“ …gives you high availability and performance because we don't have to send all data through a central server. It also gives you resilience to failure because the system automatically recovers from failures. - Chris McCord 119

Slide 120

Slide 120 text

120 How It Works

Slide 121

Slide 121 text

Configure Presence ● Define a module that uses Phoenix.Presence and shares a PubSub server with the rest of our application. 121

Slide 122

Slide 122 text

Register a Process ● Use the Presence.Track behaviour to register a given user’s LiveView process under a shared topic 122

Slide 123

Slide 123 text

Broadcast Presence to Existing Users ● Teach LiveView to handle the presence broadcast by fetching the list of present users from the presence data store 123

Slide 124

Slide 124 text

Building Bulletproof Real-Time Applications Part 4: Property-based testing with Stream Data

Slide 125

Slide 125 text

Testing Achieving 100% confidence there are no bugs (Just kidding.) 125

Slide 126

Slide 126 text

Pascal’s Triangle

Slide 127

Slide 127 text

127 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 128

Slide 128 text

128 1

Slide 129

Slide 129 text

129 1 1 1

Slide 130

Slide 130 text

130 1 1 1 1 2 1

Slide 131

Slide 131 text

131 1 1 1 1 2 1 1 3 3 1

Slide 132

Slide 132 text

132 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1

Slide 133

Slide 133 text

133 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 134

Slide 134 text

“ 134 Given an integer n >= 0, write a function that returns the nth row of Pascal’s Triangle.

Slide 135

Slide 135 text

Possible Code Implementation defmodule Pascal do def row(n) do # Amazing calculations end end 135

Slide 136

Slide 136 text

Example-Based Unit Testing Stuff you’re probably used to. 136

Slide 137

Slide 137 text

Row 0 describe "row/1" do test "returns [1] for first row" do assert Pascal.row(0) == [1] end end 137

Slide 138

Slide 138 text

Row 1 describe "row/1" do test "returns [1, 1] for second row" do assert Pascal.row(1) == [1, 1] end end 138

Slide 139

Slide 139 text

Row 4 describe "row/1" do test "returns value for 5th row" do assert Pascal.row(4) == [1, 4, 6, 4, 1] end end 139

Slide 140

Slide 140 text

Advantages

Slide 141

Slide 141 text

Advantages ○ Easy to reason about ○ Target specific test cases

Slide 142

Slide 142 text

Disadvantages

Slide 143

Slide 143 text

Disadvantages ○ Requires you to get edge cases right ○ Doesn’t cover large indexes ○ Doesn’t require a deep understanding of your domain

Slide 144

Slide 144 text

Stuff that’s going to feel hard. 144 Property-based Testing with StreamData

Slide 145

Slide 145 text

Streams of Random Data Using Generators 145

Slide 146

Slide 146 text

Booleans StreamData.boolean() |> Enum.take(5) # [false, true, false, false, false] 146

Slide 147

Slide 147 text

Integers StreamData.integer() |> Enum.take(5) # [0, -2, -2, 4, 3] 147

Slide 148

Slide 148 text

Strings StreamData.string(:alphanumeric) |> Enum.take(5) # ["", "", "vL2", "AbgJ", "XuH7"] 148

Slide 149

Slide 149 text

You Can Combine 149

Slide 150

Slide 150 text

Lists list = StreamData.list_of( StreamData.integer() ) Enum.take(list, 5) # [[], [-2, 1], [-1], [-2, -2, -1], []] 150

Slide 151

Slide 151 text

Maps map = StreamData.map_of( StreamData.atom(:alphanumeric), StreamData.integer() ) Enum.take(map, 5) # [%{}, %{}, %{iB3: -3, inF: -3, k: -1}, %{BSy: 2, z: -1}, %{}] 151

Slide 152

Slide 152 text

Lists of Maps StreamData.list_of( StreamData.map_of( StreamData.atom(:alphanumeric), StreamData.integer() ) ) 152

Slide 153

Slide 153 text

Lists of Maps of Lists of Integers StreamData.list_of( StreamData.map_of( StreamData.atom(:alphanumeric), StreamData.list_of(StreamData.integer()) ) ) 153

Slide 154

Slide 154 text

154 [ [%{gc: []}], [%{O: [-1]}, %{f: [1, -2]}], [%{}], [ %{_: [], kTy: [4, -2, 0], u2h: [4, -2], w: [0, -3, 2, -4]}, %{_CK: [-2, -3, 3]} ], [ %{r: [-3, 2, -1, 2]}, %{_0: [1], _I: [-5, 0, 4, -3], _O: [-3, 3, 0], kz: [1, 4, 5, -1, -5]}, %{fr: [0, -2, 1, 3, 4]}, ] ]

Slide 155

Slide 155 text

Data Starts Small And gets larger 155

Slide 156

Slide 156 text

Integers, Revisited StreamData.integer() |> Enum.take(25) # [0, -2, 1, -3, -1, 1, 4, 7, -1, 9, 0, 8, 9, -12, 12, -5, 0, -7, -16, 8, 21, 1, -16, -7, -19] 156

Slide 157

Slide 157 text

157 Lots of Tests, Data Shrinking

Slide 158

Slide 158 text

158 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 159

Slide 159 text

Possible Test Implementation property "number of row elements is n+1" do check all number <- StreamData.integer(), row_index = abs(number) do element_count = length(Pascal.row(row_index)) assert element_count == row_index + 1 end end 159

Slide 160

Slide 160 text

assert length(Pascal.row(0)) == 1 160

Slide 161

Slide 161 text

assert length(Pascal.row(0)) == 1 assert length(Pascal.row(1)) == 2 161

Slide 162

Slide 162 text

assert length(Pascal.row(0)) == 1 assert length(Pascal.row(1)) == 2 assert length(Pascal.row(0)) == 1 162

Slide 163

Slide 163 text

assert length(Pascal.row(0)) == 1 assert length(Pascal.row(1)) == 2 assert length(Pascal.row(0)) == 1 assert length(Pascal.row(2)) == 3 163

Slide 164

Slide 164 text

assert length(Pascal.row(0)) == 1 assert length(Pascal.row(1)) == 2 assert length(Pascal.row(0)) == 1 assert length(Pascal.row(2)) == 3 # … assert length(Pascal.row(47)) == 48 164

Slide 165

Slide 165 text

assert length(Pascal.row(0)) == 1 assert length(Pascal.row(1)) == 2 assert length(Pascal.row(0)) == 1 assert length(Pascal.row(2)) == 3 # … assert length(Pascal.row(47)) == 48 assert length(Pascal.row(58)) == 117 165

Slide 166

Slide 166 text

assert length(Pascal.row(0)) == 1 assert length(Pascal.row(1)) == 2 assert length(Pascal.row(0)) == 1 assert length(Pascal.row(2)) == 3 # … assert length(Pascal.row(47)) == 48 assert length(Pascal.row(58)) == 117 # ?!?! 166

Slide 167

Slide 167 text

Stuff that’s true of any solution 167 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 168

Slide 168 text

Stuff that’s true of any solution ○ Each row is symmetrical 168 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 169

Slide 169 text

Stuff that’s true of any solution ○ Each row is symmetrical ○ The first and last elements of each row are 1 169 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 170

Slide 170 text

Stuff that’s true of any solution ○ Each row is symmetrical ○ The first and last elements of each row are 1 ○ After row 0, the second element in each list is the row index 170 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 171

Slide 171 text

Stuff that’s true of any solution ○ Each row is symmetrical ○ The first and last elements of each row are 1 ○ After row 0, the second element in each list is the row index ○ So is the second-to-last element 171 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 172

Slide 172 text

Stuff that’s true of any solution ○ We can represent any row as a list 172 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 173

Slide 173 text

Stuff that’s true of any solution ○ We can represent any row as a list ○ Row 0 just has one element: 1 173 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 174

Slide 174 text

Stuff that’s true of any solution ○ We can represent any row as a list ○ Row 0 just has one element: 1 ○ For all rows after 0, 1 appears twice 174 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 175

Slide 175 text

Stuff that’s true of any solution ○ We can represent any row as a list ○ Row 0 just has one element: 1 ○ For all rows after 0, 1 appears twice ○ Each row n has n+1 elements in it 175 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 176

Slide 176 text

Stuff that’s true of any solution ○ We can represent any row as a list ○ Row 0 just has one element: 1 ○ For all rows after 0, 1 appears twice ○ Each row n has n+1 elements in it ○ In each row, numbers go up, then down 176 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 …

Slide 177

Slide 177 text

Potential Properties ○ Data type ○ Data shape ○ Count of specific values ○ Presence/emptiness ○ Size ○ Membership ○ Range ○ Symmetry ○ Relationships

Slide 178

Slide 178 text

Testing Votes

Slide 179

Slide 179 text

Voting Results def calculate_votes(users) do case winning_vote(users) do top_two when is_list(top_two) -> {"tie", top_two} winner -> {"winner", winner} end end

Slide 180

Slide 180 text

A Property Test property "calculated vote is a list or an integer" do check all users <- user_generator, {_event, winner} = calculate_votes(users) # We'll assert something here later end end

Slide 181

Slide 181 text

A Missing Generator setup do # Create generator here [user_generator: user_generator] end

Slide 182

Slide 182 text

Your Turn

Slide 183

Slide 183 text

Properties of Our Code

Slide 184

Slide 184 text

What’s true of any solution? def calculate_votes(users) do case winning_vote(users) do top_two when is_list(top_two) -> {"tie", top_two} winner -> {"winner", winner} end end

Slide 185

Slide 185 text

Your Turn Writing our properties and assertions

Slide 186

Slide 186 text

I :heart: StreamData And you should, too.

Slide 187

Slide 187 text

Resources ○ “An introduction to property-based testing” by Scott Wlaschin ○ “Choosing properties for property-based testing” by Scott Wlaschin ○ Property-Based Testing with PropEr, Erlang, and Elixir and PropEr Testing by Fred Hebert ○ SteamData documentation ○ “Testing the Hard Stuff and Staying Sane” by John Hughes ○ “Picking Properties to Test in Property Based Testing” by Michael Stalker 187

Slide 188

Slide 188 text

Recap