Slide 1

Slide 1 text

Montreal Elixir Meetup — March 13, 2019 Dispatch. A quick overview of neat Elixir features.

Slide 2

Slide 2 text

MIREGO 1 Quick overview of Dispatch 2 Neat Elixir features in Dispatch 3 Questions 2 Summary. Where I’m going with this

Slide 3

Slide 3 text

MIREGO 3 Rémi Prévost Director, Web Development @ Mirego

Slide 4

Slide 4 text

MIREGO Quick overview of Dispatch Neat Elixir features Questions Dispatch. Introduction “Dispatch is a (formely internal, now open-source!) tool we built to make sure our pull requests gets reviewed and looked at by the right people within a GitHub organization.” 4

Slide 5

Slide 5 text

MIREGO Quick overview of Dispatch Neat Elixir features Questions Dispatch. Introduction 5 https://github.com/mirego/dispatch

Slide 6

Slide 6 text

MIREGO Neat Elixir features. How is this going to work 6 Quick overview of Dispatch Neat Elixir features Questions Mox How we use Mox in Dispatch Agent How we use Agent in Dispatch 1 2

Slide 7

Slide 7 text

MIREGO Neat Elixir features. Mox 7 Quick overview of Dispatch Neat Elixir features Questions Mox is a third-party library (not really an Elixir feature) that allows developers to create behaviour-based mocks to use in ExUnit tests. ● It makes sure we can’t mock anything we want ● It makes sure we can’t mock something only when we want it ● It forces us to clearly separate what is the behaviour we need from a module and how this behaviour should be implemented Let’s see how Mox works with a simple example.

Slide 8

Slide 8 text

MIREGO Neat Elixir features. Mox 8 Quick overview of Dispatch Neat Elixir features Questions # lib/my_app/calculator.ex defmodule MyApp.Calculator do def add(first_number, second_number) do first_number + second_number end end

Slide 9

Slide 9 text

MIREGO Neat Elixir features. Mox 9 Quick overview of Dispatch Neat Elixir features Questions # lib/my_app/calculator_behaviour.ex defmodule MyApp.CalculatorBehaviour do @callback add(integer(), integer()) :: integer() end

Slide 10

Slide 10 text

MIREGO Neat Elixir features. Mox 1 0 Quick overview of Dispatch Neat Elixir features Questions # lib/my_app/calculator.ex defmodule MyApp.Calculator do @behaviour MyApp.CalculatorBehaviour def add(first_number, second_number) do first_number + second_number end end

Slide 11

Slide 11 text

MIREGO Neat Elixir features. Mox 1 1 Quick overview of Dispatch Neat Elixir features Questions # lib/my_app/my_app.ex defmodule MyApp do def add_numbers(first_number, second_number) do result = MyApp.Calculator.add(first_number, second_number) "The result is #{result}" end end

Slide 12

Slide 12 text

MIREGO Neat Elixir features. Mox 1 2 Quick overview of Dispatch Neat Elixir features Questions # lib/my_app/my_app.ex defmodule MyApp do def add_numbers(first_number, second_number) do result = calculator().add(first_number, second_number) "The result is #{result}" end def calculator do Application.get_env(:my_app, MyApp)[:calculator] end end

Slide 13

Slide 13 text

MIREGO # test/support/mocks.exs Mox.defmock(MyApp.CalculatorMock, for: MyApp.Calculator) # config/config.exs config :my_app, MyApp, calculator: MyApp.Calculator # config/config.test.exs config :my_app, MyApp, calculator: MyApp.CalculatorMock Neat Elixir features. Mox 1 3 Quick overview of Dispatch Neat Elixir features Questions

Slide 14

Slide 14 text

MIREGO Neat Elixir features. Mox 1 4 Quick overview of Dispatch Neat Elixir features Questions # test/my_app/calculator_test.exs defmodule MyApp.CalculatorTest do use ExUnit.Case async: true setup :verify_on_exit! test "add/2 adds " do expect(MyApp.CalculatorMock, :add, fn(_, _) -> 999 end) assert MyApp.add_numbers(2, 3) == "The result is 999" end end

Slide 15

Slide 15 text

MIREGO Neat Elixir features. Mock in Dispatch 1 5 Quick overview of Dispatch Neat Elixir features Questions Dispatch interacts with several APIs in order to fetch and post data. Of course, we don’t want to depend on these integrations when running our test suite. We use Mox to decouple our external service implementations from the code that actually uses them.

Slide 16

Slide 16 text

MIREGO Neat Elixir features. 1 6 Quick overview of Dispatch Neat Elixir features Questions # lib/dispatch/repositories/client_behaviour.ex defmodule Dispatch.Repositories.ClientBehaviour do @callback fetch_contributors(binary()) :: list(Map.t()) @callback request_reviewers(binary(), integer(), list(Map.t()) :: :ok end Mock in Dispatch

Slide 17

Slide 17 text

MIREGO Neat Elixir features. 1 7 Quick overview of Dispatch Neat Elixir features Questions # lib/dispatch/repositories/github_client.ex defmodule Dispatch.Repositories.GitHubClient do @behaviour Dispatch.Repositories.ClientBehaviour def fetch_contributors(repo) do url = "https://api.github.com/repos/#{repo}/stats/contributors" {:ok, %{body: %{"users" => users}} = HTTPoison.get(url) Enum.map(users, & &1.username) end def request_reviewers(repo, number, reviewers) do url = "https://api.github.com/repos/#{repo}/prs/#{number}/reviews" {:ok, _} = HTTPoison.post(url, reviewers) :ok end end Mock in Dispatch

Slide 18

Slide 18 text

MIREGO Neat Elixir features. 1 8 Quick overview of Dispatch Neat Elixir features Questions # config/config.exs config :dispatch, Dispatch, repositories_client: Dispatch.Repositories.GitHubClient # config/config.test.exs config :dispatch, Dispatch, repositories_client: Dispatch.Repositories.MockClient Mock in Dispatch

Slide 19

Slide 19 text

MIREGO Neat Elixir features. 1 9 Quick overview of Dispatch Neat Elixir features Questions # lib/dispatch/dispatch.ex defmodule Dispatch do def request_reviewers(repo, number) do client = repositories_client() contributors = client.fetch_contributors(repo) response = client.request_reviewers(repo, number, contributors) [response: response, reviewers: contributors] end defp repositories_client do Application.get_env(:dispatch, Dispatch)[:repositories_client] end end Mock in Dispatch

Slide 20

Slide 20 text

MIREGO # test/support/mocks.exs Mox.defmock(Dispatch.Repositories.MockClient, for: Dispatch.Repositories.ClientBehaviour ) # test/dispatch/dispatch_test.exs defmodule DispatchTest do setup :verify_on_exit! test "request_reviewers/2 should fetch contributors and request them" do # Tests! end end Neat Elixir features. 2 0 Quick overview of Dispatch Neat Elixir features Questions Mock in Dispatch

Slide 21

Slide 21 text

MIREGO Neat Elixir features. 2 1 Quick overview of Dispatch Neat Elixir features Questions # test/dispatch/dispatch_test.exs defmodule DispatchTest do test "request_reviewers/2 should fetch contributors and request them" do expect(MockClient, :fetch_contributors, fn "mirego/foo" -> ["bar", "baz", "omg"] end) expect(MockClient, :request_reviewers, fn "mirego/foo", 45, [] -> :ok end) [response: response, reviewers: reviewers] = Dispatch.request_reviewers("mirego/foo", 45, []) assert response == :ok assert reviewers == ["bar", "baz", "omg"] end end Mock in Dispatch

Slide 22

Slide 22 text

MIREGO Neat Elixir features. Agent 2 2 “Agents are a simple abstraction around state.” — Elixir documentation Quick overview of Dispatch Neat Elixir features Questions

Slide 23

Slide 23 text

MIREGO Neat Elixir features. Agent 2 3 “But I thought Elixir was a stateless language!?” — Anyone starting with Elixir Quick overview of Dispatch Neat Elixir features Questions

Slide 24

Slide 24 text

MIREGO Neat Elixir features. Agent 2 4 Quick overview of Dispatch Neat Elixir features Questions Processes (from Erlang) actually have a state. GenServer is an abstraction on top of processes, that provides a simple and standard way to create, supervise and interact with processes. Agent is an abstraction on top of GenServer, that provides a simple way to create processes exposing a simple API to store and retrieve state data. Let’s see how Agent works with a simple example.

Slide 25

Slide 25 text

MIREGO defmodule Counter do use Agent def start_link(initial_value) do Agent.start_link(fn -> initial_value end, name: __MODULE__) end def value do Agent.get(__MODULE__, fn state -> state end) end def increment do Agent.update(__MODULE__, fn state -> state + 1 end) end end Neat Elixir features. Agent 2 5 Quick overview of Dispatch Neat Elixir features Questions

Slide 26

Slide 26 text

MIREGO # Start the process Counter.start_link(0) # Retrieve the value from the state Counter.value => 0 # Increment the value twice Counter.increment => :ok Counter.increment => :ok # Retrieve the value from the state Counter.value => 2 Neat Elixir features. Agent 2 6 Quick overview of Dispatch Neat Elixir features Questions

Slide 27

Slide 27 text

MIREGO Neat Elixir features. Agent in Dispatch 2 7 Quick overview of Dispatch Neat Elixir features Questions Dispatch uses an external JSON file to store its configuration: ● a list of experts by stack (eg. elixir, ruby, react, etc.) ● a list of learners by stack ● a list of blacklisted users We store this state in an Agent and make sure it’s up-to-date when we need it.

Slide 28

Slide 28 text

MIREGO Neat Elixir features. # lib/dispatch/settings/json_static_file_client.ex defmodule Dispatch.Settings.JSONStaticFileClient do use Agent def start_link do state = build_state() Agent.start_link(fn -> state end, name: __MODULE__) end end 2 8 Quick overview of Dispatch Neat Elixir features Questions Agent in Dispatch

Slide 29

Slide 29 text

MIREGO # lib/dispatch/settings/json_static_file_client.ex defmodule Dispatch.Settings.JSONStaticFileClient do defp build_state do url = Application.get_env(:dispatch, __MODULE__)[:configuration_url] {:ok, configuration} = fetch_configuration(url) %{ experts: Map.get(configuration, "experts"), learners: Map.get(configuration, "learners"), blacklist: Map.get(configuration, "blacklist"), } end defp fetch_configuration(url) do # Fetch JSON file from URL and parse it end end Neat Elixir features. 2 9 Quick overview of Dispatch Neat Elixir features Questions Agent in Dispatch

Slide 30

Slide 30 text

MIREGO # lib/dispatch/settings/json_static_file_client.ex defmodule Dispatch.Settings.JSONStaticFileClient do @behaviour Dispatch.Settings.ClientBehaviour def expert_users(stack) do Agent.get(__MODULE__, & &1.experts) |> Map.get(stack) end def learner_users(stack) do Agent.get(__MODULE__, & &1.learners) |> Map.get(stack) end def blacklisted_users do Agent.get(__MODULE__, & &1.blacklist) end end Neat Elixir features. 3 0 Quick overview of Dispatch Neat Elixir features Questions Agent in Dispatch

Slide 31

Slide 31 text

MIREGO # lib/dispatch/settings/json_static_file_client.ex defmodule Dispatch.Settings.JSONStaticFileClient do def refresh do state = build_state() Agent.update(__MODULE__, fn _ -> state end) end end Neat Elixir features. 3 1 Quick overview of Dispatch Neat Elixir features Questions Agent in Dispatch

Slide 32

Slide 32 text

MIREGO # lib/dispatch/dispatch.ex defmodule Dispatch do def fetch_reviewers(repo, number) do client = settings_client(). # Refresh settings client().refresh() # Draw the rest of the owl # client().expert_users("elixir") => […] end defp settings_client do Application.get_env(:dispatch, __MODULE__)[:settings_client] end end Neat Elixir features. 3 2 Quick overview of Dispatch Neat Elixir features Questions Agent in Dispatch

Slide 33

Slide 33 text

MIREGO Questions? 3 3 Quick overview of Dispatch Neat Elixir features Questions https://github.com/mirego/dispatch

Slide 34

Slide 34 text

Thank you.