Share
Embed
Dispatch, a quick overview of neat Elixir features
Dispatch is a (formely internal, now open-source!) tool we built to make sure pull requests gets reviewed and looked at by the right people within a GitHub organization.
In this presentation, we’ll take a look at how we use a few neat Elixir features to make Dispatch work.
Rémi Prévost
March 13, 2019
Transcript
34 slides
-
Montreal Elixir Meetup — March 13, 2019 Dispatch. A quick
overview of neat Elixir features. -
MIREGO 1 Quick overview of Dispatch 2 Neat Elixir features
in Dispatch 3 Questions 2 Summary. Where I’m going with this -
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 -
MIREGO Quick overview of Dispatch Neat Elixir features Questions Dispatch.
Introduction 5 https://github.com/mirego/dispatch -
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 -
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. -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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. -
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 -
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 -
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 -
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 -
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 -
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 -
MIREGO Neat Elixir features. Agent 2 2 “Agents are a
simple abstraction around state.” — Elixir documentation Quick overview of Dispatch Neat Elixir features Questions -
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 -
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. -
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 -
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 -
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. -
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 -
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 -
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 -
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 -
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 -
MIREGO Questions? 3 3 Quick overview of Dispatch Neat Elixir
features Questions https://github.com/mirego/dispatch