From 6f958953426483294f93577b99e6eee8a1a790ee Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Tue, 7 Apr 2020 18:59:23 -0700 Subject: [PATCH 01/34] Refactor Outbound Slack Connection into a behavior abstraction --- lib/alice/chat_backends/outbound_client.ex | 8 ++++++++ lib/alice/chat_backends/slack_outbound.ex | 7 +++++++ lib/alice/router/helpers.ex | 2 +- mix.exs | 3 ++- mix.lock | 1 + test/support/mocks.exs | 1 + 6 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 lib/alice/chat_backends/outbound_client.ex create mode 100644 lib/alice/chat_backends/slack_outbound.ex create mode 100644 test/support/mocks.exs diff --git a/lib/alice/chat_backends/outbound_client.ex b/lib/alice/chat_backends/outbound_client.ex new file mode 100644 index 0000000..e438d15 --- /dev/null +++ b/lib/alice/chat_backends/outbound_client.ex @@ -0,0 +1,8 @@ +defmodule Alice.ChatBackends.OutboundClient do + @moduledoc """ + Documentation for the OutboundClient behavior. This defines a behavior for modules that serve as an outbound connection to a backend. + """ + + @callback send_message(response :: String.t(), channel :: String.t(), backend :: map()) :: String.t() + @callback indicate_typing(channel :: String.t(), backend :: map()) :: String.t() +end diff --git a/lib/alice/chat_backends/slack_outbound.ex b/lib/alice/chat_backends/slack_outbound.ex new file mode 100644 index 0000000..4d45b49 --- /dev/null +++ b/lib/alice/chat_backends/slack_outbound.ex @@ -0,0 +1,7 @@ +defmodule Alice.ChatBackends.SlackOutbound do + @behaviour Alice.ChatBackends.OutboundClient + + def send_message(response, channel, slack), do: Slack.Sends.send_message(response, channel, slack) + + def indicate_typing(channel, slack), do: Slack.Sends.indicate_typing(channel, slack) +end diff --git a/lib/alice/router/helpers.ex b/lib/alice/router/helpers.ex index ee883b8..302725a 100644 --- a/lib/alice/router/helpers.ex +++ b/lib/alice/router/helpers.ex @@ -32,7 +32,7 @@ defmodule Alice.Router.Helpers do defp slack_api do case Mix.env() do :test -> FakeSlack - _else -> Slack.Sends + _else -> Alice.ChatBackends.SlackOutbound end end diff --git a/mix.exs b/mix.exs index 7aa6943..3eb3bf8 100644 --- a/mix.exs +++ b/mix.exs @@ -28,7 +28,8 @@ defmodule Alice.Mixfile do {:poolboy, "~> 1.5.0"}, {:redix, "~> 0.6.0"}, {:poison, "~> 3.0"}, - {:dialyxir, "~> 1.0.0-rc.6", only: [:dev], runtime: false} + {:dialyxir, "~> 1.0.0-rc.6", only: [:dev], runtime: false}, + {:mox, "~> 0.5", only: :test} ] end diff --git a/mix.lock b/mix.lock index e49c81f..5caa965 100644 --- a/mix.lock +++ b/mix.lock @@ -17,6 +17,7 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm", "7a4c8e1115a2732a67d7624e28cf6c9f30c66711a9e92928e745c255887ba465"}, + "mox": {:hex, :mox, "0.5.2", "55a0a5ba9ccc671518d068c8dddd20eeb436909ea79d1799e2209df7eaa98b6c", [:mix], [], "hexpm", "df4310628cd628ee181df93f50ddfd07be3e5ecc30232d3b6aadf30bdfe6092b"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, diff --git a/test/support/mocks.exs b/test/support/mocks.exs new file mode 100644 index 0000000..d2fb45a --- /dev/null +++ b/test/support/mocks.exs @@ -0,0 +1 @@ +Mox.defmock(Alice.ChatBackends.OutboundMock, for: Alice.ChatBackends.OutboundClient) From db0b8e2f1a3fb59831817a0a1a75a488a0a4cad7 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Tue, 7 Apr 2020 19:59:16 -0700 Subject: [PATCH 02/34] Refactored internal tests to use Mox library and added tests for ping handler --- lib/alice/router/helpers.ex | 2 +- mix.exs | 4 +++ test/alice/handlers/utils_test.exs | 17 ++++++++++ test/alice/router/helpers_test.exs | 47 ++++++++++++++++++---------- test/support/{mocks.exs => mocks.ex} | 0 test/test_helper.exs | 19 +---------- 6 files changed, 54 insertions(+), 35 deletions(-) create mode 100644 test/alice/handlers/utils_test.exs rename test/support/{mocks.exs => mocks.ex} (100%) diff --git a/lib/alice/router/helpers.ex b/lib/alice/router/helpers.ex index 302725a..de757f9 100644 --- a/lib/alice/router/helpers.ex +++ b/lib/alice/router/helpers.ex @@ -31,7 +31,7 @@ defmodule Alice.Router.Helpers do defp slack_api do case Mix.env() do - :test -> FakeSlack + :test -> Alice.ChatBackends.OutboundMock _else -> Alice.ChatBackends.SlackOutbound end end diff --git a/mix.exs b/mix.exs index 3eb3bf8..ffda82a 100644 --- a/mix.exs +++ b/mix.exs @@ -6,6 +6,7 @@ defmodule Alice.Mixfile do app: :alice, version: "0.4.1", elixir: "~> 1.7", + elixirc_paths: elixirc_paths(Mix.env()), build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, description: "A Slack bot", @@ -44,4 +45,7 @@ defmodule Alice.Mixfile do } ] end + + defp elixirc_paths(:test), do: ["test/support", "lib"] + defp elixirc_paths(_), do: ["lib"] end diff --git a/test/alice/handlers/utils_test.exs b/test/alice/handlers/utils_test.exs new file mode 100644 index 0000000..4c0d858 --- /dev/null +++ b/test/alice/handlers/utils_test.exs @@ -0,0 +1,17 @@ +defmodule Alice.Handlers.UtilsTest do + use ExUnit.Case + import Mox + import Alice.Handlers.Utils + + def conn do + %Alice.Conn{message: %{channel: :channel}, slack: :slack} + end + + test "it should respond approriately" do + Alice.ChatBackends.OutboundMock + |> expect(:send_message, fn resp, _, _ when resp in ["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"] -> "" end) + + ping(conn()) + verify!() + end +end diff --git a/test/alice/router/helpers_test.exs b/test/alice/router/helpers_test.exs index 6358d2f..a2ff987 100644 --- a/test/alice/router/helpers_test.exs +++ b/test/alice/router/helpers_test.exs @@ -1,11 +1,6 @@ -defmodule FakeSlack do - def send_message(text, :channel, :slack) do - send(self(), {:msg, text}) - end -end - defmodule Alice.Router.HelpersTest do - use ExUnit.Case, async: true + use ExUnit.Case + import Mox import Alice.Router.Helpers def conn do @@ -13,43 +8,63 @@ defmodule Alice.Router.HelpersTest do end test "reply returns the conn" do + Alice.ChatBackends.OutboundMock + |> stub(:send_message, fn _, _, _ -> "" end) + assert reply("yo", conn()) == conn() end test "reply sends a message with Slack.send_message" do + Alice.ChatBackends.OutboundMock + |> expect(:send_message, fn "yo", _, _ -> "" end) + reply("yo", conn()) - assert_received {:msg, "yo"} + verify!() end test "reply calls random_reply when given a list" do - ["element"] |> reply(conn()) - assert_received {:msg, "element"} + Alice.ChatBackends.OutboundMock + |> expect(:send_message, fn "element", _, _ -> "" end) + + reply(["element"], conn()) + verify!() end test "random_reply sends a message from a given list" do + Alice.ChatBackends.OutboundMock + |> expect(:send_message, fn resp, _, _ when resp in ["rabbit", "hole"] -> "" end) + ~w[rabbit hole] |> random_reply(conn()) - assert_received {:msg, resp} - assert resp in ~w[rabbit hole] + verify!() end test "chance_reply, when chance passes, \ replies with the given message" do + Alice.ChatBackends.OutboundMock + |> expect(:send_message, fn "always", _, _ -> "" end) + chance_reply(conn(), 1, "always") - assert_received {:msg, "always"} + verify!() end test "chance_reply, when chance does not pass, \ when not given negative message, \ does not reply" do + Alice.ChatBackends.OutboundMock + |> expect(:send_message, 0, fn "never", _, _ -> "" end) + chance_reply(conn(), 0, "never") - refute_received {:msg, _} + verify!() end test "chance_reply, when chance does not pass, \ when given negative message, \ replies with negative" do + Alice.ChatBackends.OutboundMock + |> expect(:send_message, 0, fn "positive", _, _ -> "" end) + |> expect(:send_message, 1, fn "negative", _, _ -> "" end) + chance_reply(conn(), 0, "positive", "negative") - refute_received {:msg, "positive"} - assert_received {:msg, "negative"} + verify!() end end diff --git a/test/support/mocks.exs b/test/support/mocks.ex similarity index 100% rename from test/support/mocks.exs rename to test/support/mocks.ex diff --git a/test/test_helper.exs b/test/test_helper.exs index 6279db2..a1b6e56 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,19 +1,2 @@ -defmodule Mock do - def setup(name, params), do: setup(name, params, default_return: nil) - - def setup(name, params, default_return: value) do - send(self(), {name, params}) - - receive do - {:return, {^name, value}} -> value - after - 0 -> value - end - end - - def setup_return(name, value) do - send(self(), {:return, {name, value}}) - end -end - ExUnit.start() +Application.ensure_all_started(:mox) From eea8685fe0d99463557ad1845a295183203a27e2 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Tue, 7 Apr 2020 20:29:53 -0700 Subject: [PATCH 03/34] Extract conn creation into a test helper --- test/alice/handlers/utils_test.exs | 5 +---- test/alice/router/helpers_test.exs | 5 +---- test/test_helper.exs | 6 ++++++ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/alice/handlers/utils_test.exs b/test/alice/handlers/utils_test.exs index 4c0d858..7e26bcd 100644 --- a/test/alice/handlers/utils_test.exs +++ b/test/alice/handlers/utils_test.exs @@ -1,12 +1,9 @@ defmodule Alice.Handlers.UtilsTest do use ExUnit.Case import Mox + import Alice.TestHelpers import Alice.Handlers.Utils - def conn do - %Alice.Conn{message: %{channel: :channel}, slack: :slack} - end - test "it should respond approriately" do Alice.ChatBackends.OutboundMock |> expect(:send_message, fn resp, _, _ when resp in ["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"] -> "" end) diff --git a/test/alice/router/helpers_test.exs b/test/alice/router/helpers_test.exs index a2ff987..83523c4 100644 --- a/test/alice/router/helpers_test.exs +++ b/test/alice/router/helpers_test.exs @@ -1,12 +1,9 @@ defmodule Alice.Router.HelpersTest do use ExUnit.Case import Mox + import Alice.TestHelpers import Alice.Router.Helpers - def conn do - %Alice.Conn{message: %{channel: :channel}, slack: :slack} - end - test "reply returns the conn" do Alice.ChatBackends.OutboundMock |> stub(:send_message, fn _, _, _ -> "" end) diff --git a/test/test_helper.exs b/test/test_helper.exs index a1b6e56..0dc0a8a 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1,8 @@ +defmodule Alice.TestHelpers do + def conn do + %Alice.Conn{message: %{channel: :channel}, slack: :slack} + end +end + ExUnit.start() Application.ensure_all_started(:mox) From 83e7e20c873fd6704243fe6da64040053cedb459 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Tue, 7 Apr 2020 21:00:26 -0700 Subject: [PATCH 04/34] Rename slack_api to more generic outbound_api --- lib/alice/router/helpers.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/alice/router/helpers.ex b/lib/alice/router/helpers.ex index de757f9..51a7fc7 100644 --- a/lib/alice/router/helpers.ex +++ b/lib/alice/router/helpers.ex @@ -24,12 +24,12 @@ defmodule Alice.Router.Helpers do def reply(conn = %Conn{message: %{channel: channel}, slack: slack}, resp) do resp |> Alice.Images.uncache() - |> slack_api().send_message(channel, slack) + |> outbound_api().send_message(channel, slack) conn end - defp slack_api do + defp outbound_api do case Mix.env() do :test -> Alice.ChatBackends.OutboundMock _else -> Alice.ChatBackends.SlackOutbound @@ -103,7 +103,7 @@ defmodule Alice.Router.Helpers do """ @spec indicate_typing(Conn.t()) :: Conn.t() def indicate_typing(conn = %Conn{message: %{channel: chan}, slack: slack}) do - slack_api().indicate_typing(chan, slack) + outbound_api().indicate_typing(chan, slack) conn end end From 1f99111f7f74cfebeb811a8a59c8b51c429e5dbb Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Wed, 8 Apr 2020 09:58:44 -0700 Subject: [PATCH 05/34] Refactored mocking infrastructure into a Case class with associated macros --- lib/alice/handlers/case.ex | 38 ++++++++++++++++++++++++++++ mix.exs | 3 +-- test/alice/handlers/utils_test.exs | 4 +-- test/alice/router/helpers_test.exs | 40 ++++++++++++------------------ test/support/mocks.ex | 1 - test/test_helper.exs | 8 +----- 6 files changed, 58 insertions(+), 36 deletions(-) create mode 100644 lib/alice/handlers/case.ex delete mode 100644 test/support/mocks.ex diff --git a/lib/alice/handlers/case.ex b/lib/alice/handlers/case.ex new file mode 100644 index 0000000..079551f --- /dev/null +++ b/lib/alice/handlers/case.ex @@ -0,0 +1,38 @@ +defmodule Alice.Handlers.Case do + def start() do + Application.ensure_all_started(:mox) + Mox.defmock(Alice.ChatBackends.OutboundMock, for: Alice.ChatBackends.OutboundClient) + end + + def fake_conn do + %Alice.Conn{message: %{channel: :channel}, slack: :slack} + end + + defmacro __using__(_opts) do + quote do + import Mox + import Alice.Handlers.Case + end + end + + defmacro expect_response(response, times_expected \\ 1) + defmacro expect_response(response, times_expected) when is_list(response) do + quote do + Alice.ChatBackends.OutboundMock + |> expect(:send_message, unquote(times_expected), fn resp, _, _ when resp in unquote(response) -> "" end) + end + end + defmacro expect_response(response, times_expected) do + quote do + Alice.ChatBackends.OutboundMock + |> expect(:send_message, unquote(times_expected), fn unquote(response), _, _ -> "" end) + end + end + + defmacro stub_response() do + quote do + Alice.ChatBackends.OutboundMock + |> stub(:send_message, fn _, _, _ -> "" end) + end + end +end diff --git a/mix.exs b/mix.exs index ffda82a..f893686 100644 --- a/mix.exs +++ b/mix.exs @@ -30,7 +30,7 @@ defmodule Alice.Mixfile do {:redix, "~> 0.6.0"}, {:poison, "~> 3.0"}, {:dialyxir, "~> 1.0.0-rc.6", only: [:dev], runtime: false}, - {:mox, "~> 0.5", only: :test} + {:mox, "~> 0.5", only: [:test]} ] end @@ -46,6 +46,5 @@ defmodule Alice.Mixfile do ] end - defp elixirc_paths(:test), do: ["test/support", "lib"] defp elixirc_paths(_), do: ["lib"] end diff --git a/test/alice/handlers/utils_test.exs b/test/alice/handlers/utils_test.exs index 7e26bcd..504d502 100644 --- a/test/alice/handlers/utils_test.exs +++ b/test/alice/handlers/utils_test.exs @@ -1,14 +1,14 @@ defmodule Alice.Handlers.UtilsTest do use ExUnit.Case import Mox - import Alice.TestHelpers + import Alice.Handlers.Case import Alice.Handlers.Utils test "it should respond approriately" do Alice.ChatBackends.OutboundMock |> expect(:send_message, fn resp, _, _ when resp in ["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"] -> "" end) - ping(conn()) + ping(fake_conn()) verify!() end end diff --git a/test/alice/router/helpers_test.exs b/test/alice/router/helpers_test.exs index 83523c4..605b5d0 100644 --- a/test/alice/router/helpers_test.exs +++ b/test/alice/router/helpers_test.exs @@ -1,67 +1,59 @@ defmodule Alice.Router.HelpersTest do use ExUnit.Case - import Mox - import Alice.TestHelpers + use Alice.Handlers.Case import Alice.Router.Helpers test "reply returns the conn" do - Alice.ChatBackends.OutboundMock - |> stub(:send_message, fn _, _, _ -> "" end) + stub_response() - assert reply("yo", conn()) == conn() + assert reply("yo", fake_conn()) == fake_conn() end test "reply sends a message with Slack.send_message" do - Alice.ChatBackends.OutboundMock - |> expect(:send_message, fn "yo", _, _ -> "" end) + expect_response("yo") - reply("yo", conn()) + reply("yo", fake_conn()) verify!() end test "reply calls random_reply when given a list" do - Alice.ChatBackends.OutboundMock - |> expect(:send_message, fn "element", _, _ -> "" end) + expect_response("element") - reply(["element"], conn()) + reply(["element"], fake_conn()) verify!() end test "random_reply sends a message from a given list" do - Alice.ChatBackends.OutboundMock - |> expect(:send_message, fn resp, _, _ when resp in ["rabbit", "hole"] -> "" end) + expect_response(["rabbit", "hole"]) - ~w[rabbit hole] |> random_reply(conn()) + ~w[rabbit hole] |> random_reply(fake_conn()) verify!() end test "chance_reply, when chance passes, \ replies with the given message" do - Alice.ChatBackends.OutboundMock - |> expect(:send_message, fn "always", _, _ -> "" end) + expect_response("always") - chance_reply(conn(), 1, "always") + chance_reply(fake_conn(), 1, "always") verify!() end test "chance_reply, when chance does not pass, \ when not given negative message, \ does not reply" do - Alice.ChatBackends.OutboundMock - |> expect(:send_message, 0, fn "never", _, _ -> "" end) + expect_response("never", 0) - chance_reply(conn(), 0, "never") + chance_reply(fake_conn(), 0, "never") verify!() end test "chance_reply, when chance does not pass, \ when given negative message, \ replies with negative" do - Alice.ChatBackends.OutboundMock - |> expect(:send_message, 0, fn "positive", _, _ -> "" end) - |> expect(:send_message, 1, fn "negative", _, _ -> "" end) + expect_response("positive", 0) + expect_response("negative", 1) - chance_reply(conn(), 0, "positive", "negative") + chance_reply(fake_conn(), 0, "positive", "negative") verify!() end end diff --git a/test/support/mocks.ex b/test/support/mocks.ex deleted file mode 100644 index d2fb45a..0000000 --- a/test/support/mocks.ex +++ /dev/null @@ -1 +0,0 @@ -Mox.defmock(Alice.ChatBackends.OutboundMock, for: Alice.ChatBackends.OutboundClient) diff --git a/test/test_helper.exs b/test/test_helper.exs index 0dc0a8a..ea684c2 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,8 +1,2 @@ -defmodule Alice.TestHelpers do - def conn do - %Alice.Conn{message: %{channel: :channel}, slack: :slack} - end -end - ExUnit.start() -Application.ensure_all_started(:mox) +Alice.Handlers.Case.start() From 84b1d784c543c31c4b31f3cf8cbe17c618b31d16 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Wed, 8 Apr 2020 12:13:47 -0700 Subject: [PATCH 06/34] Extracted more test context into helpers to make testing easier --- lib/alice/handlers/case.ex | 9 +++++++++ test/alice/handlers/help_test.exs | 1 + test/alice/handlers/utils_test.exs | 15 ++++++++++----- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/alice/handlers/case.ex b/lib/alice/handlers/case.ex index 079551f..8ff9084 100644 --- a/lib/alice/handlers/case.ex +++ b/lib/alice/handlers/case.ex @@ -8,6 +8,15 @@ defmodule Alice.Handlers.Case do %Alice.Conn{message: %{channel: :channel}, slack: :slack} end + def fake_conn_with_text(text) do + %Alice.Conn{message: %{text: text, channel: :channel}, slack: :slack} + end + + def fake_conn_with_capture(message, capture_regex) do + fake_conn_with_text(message) + |> Alice.Conn.add_captures(capture_regex) + end + defmacro __using__(_opts) do quote do import Mox diff --git a/test/alice/handlers/help_test.exs b/test/alice/handlers/help_test.exs index f04661c..4a2d4fe 100644 --- a/test/alice/handlers/help_test.exs +++ b/test/alice/handlers/help_test.exs @@ -1,5 +1,6 @@ defmodule Alice.Handlers.HelpTest do use ExUnit.Case, async: true + use Alice.Handlers.Case alias Alice.Handlers.Help alias Alice.Handlers.TestHandler diff --git a/test/alice/handlers/utils_test.exs b/test/alice/handlers/utils_test.exs index 504d502..e150197 100644 --- a/test/alice/handlers/utils_test.exs +++ b/test/alice/handlers/utils_test.exs @@ -1,14 +1,19 @@ defmodule Alice.Handlers.UtilsTest do use ExUnit.Case - import Mox - import Alice.Handlers.Case + use Alice.Handlers.Case import Alice.Handlers.Utils - test "it should respond approriately" do - Alice.ChatBackends.OutboundMock - |> expect(:send_message, fn resp, _, _ when resp in ["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"] -> "" end) + test "it should respond to a ping" do + expect_response(["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"]) ping(fake_conn()) verify!() end + + test "it should respond with info about the running bot" do + expect_response(_, 2) + + info(fake_conn()) + verify!() + end end From 002ec1c08e87151088ec2affd0c1402723dbd708 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Wed, 8 Apr 2020 19:06:05 -0700 Subject: [PATCH 07/34] Added testing instructions to the README --- README.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2078468..a5b7d6f 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,8 @@ end defp deps do [ - {:alice, "~> 0.4.0"} + {:alice, "~> 0.4.0"}, + {:mox, "~> 0.5", only: test} ] end ``` @@ -208,6 +209,42 @@ defmodule Alice.Handlers.GoogleImages do end ``` +### Testing Handlers + +To test a handler, we've exposed a few macros that create a mock Alice's +outbound internals so that you can test your handler's actual +implementation. Do do so, you'll need to add the following to your test +helper to ensure things are started properly. + +In `test/test_helper.exs`: + +``` +Alice.Handlers.Case.start() +``` + +Then you can write the actual test. You set the expectation for what you +want the final message to be sent to the chat to be, then you can create +a fake of a connection to pass into your method. From there, you just +pass it in, then finally verify that all your expectations were met. + +In `test/alice/handlers/google_images_test.exs`: + +```elixir +defmodule Alice.Handlers.GoogleImagesTest do + use ExUnit.Case + use Alice.Handlers.Case + + test "it fetches an image when asked" do + expect_response("http://example.com/image_from_google.jpg") + + fake_conn_with_capture("img me example image", ~r/(image|img)\s+me (?.+)/i) + |> Alice.Handlers.GoogleImages.fetch + + verify!() + end +end +``` + ### Registering Handlers In the `mix.exs` file of your bot, add your handler to the list of handlers to From 38166cfa8289e51501ff45b35c0fca280bb7294c Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Wed, 8 Apr 2020 19:10:20 -0700 Subject: [PATCH 08/34] Properly mark the test_helper section of the README as elixir --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5b7d6f..e0512e8 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ helper to ensure things are started properly. In `test/test_helper.exs`: -``` +```elixir Alice.Handlers.Case.start() ``` From fc76e6ea38244e2d0cae3420043fcd2ccd6efa46 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 13:23:13 -0700 Subject: [PATCH 09/34] Changed from a mock to a spy --- lib/alice/handlers/case.ex | 47 ------------------------------ lib/alice/router/helpers.ex | 2 +- mix.exs | 1 + test/alice/handlers/help_test.exs | 1 - test/alice/handlers/utils_test.exs | 13 ++++----- test/alice/router/helpers_test.exs | 42 +++++++++++++------------- test/support/case.ex | 33 +++++++++++++++++++++ test/support/outbound_fake.ex | 11 +++++++ test/test_helper.exs | 1 - 9 files changed, 72 insertions(+), 79 deletions(-) delete mode 100644 lib/alice/handlers/case.ex create mode 100644 test/support/case.ex create mode 100644 test/support/outbound_fake.ex diff --git a/lib/alice/handlers/case.ex b/lib/alice/handlers/case.ex deleted file mode 100644 index 8ff9084..0000000 --- a/lib/alice/handlers/case.ex +++ /dev/null @@ -1,47 +0,0 @@ -defmodule Alice.Handlers.Case do - def start() do - Application.ensure_all_started(:mox) - Mox.defmock(Alice.ChatBackends.OutboundMock, for: Alice.ChatBackends.OutboundClient) - end - - def fake_conn do - %Alice.Conn{message: %{channel: :channel}, slack: :slack} - end - - def fake_conn_with_text(text) do - %Alice.Conn{message: %{text: text, channel: :channel}, slack: :slack} - end - - def fake_conn_with_capture(message, capture_regex) do - fake_conn_with_text(message) - |> Alice.Conn.add_captures(capture_regex) - end - - defmacro __using__(_opts) do - quote do - import Mox - import Alice.Handlers.Case - end - end - - defmacro expect_response(response, times_expected \\ 1) - defmacro expect_response(response, times_expected) when is_list(response) do - quote do - Alice.ChatBackends.OutboundMock - |> expect(:send_message, unquote(times_expected), fn resp, _, _ when resp in unquote(response) -> "" end) - end - end - defmacro expect_response(response, times_expected) do - quote do - Alice.ChatBackends.OutboundMock - |> expect(:send_message, unquote(times_expected), fn unquote(response), _, _ -> "" end) - end - end - - defmacro stub_response() do - quote do - Alice.ChatBackends.OutboundMock - |> stub(:send_message, fn _, _, _ -> "" end) - end - end -end diff --git a/lib/alice/router/helpers.ex b/lib/alice/router/helpers.ex index 51a7fc7..90fc303 100644 --- a/lib/alice/router/helpers.ex +++ b/lib/alice/router/helpers.ex @@ -31,7 +31,7 @@ defmodule Alice.Router.Helpers do defp outbound_api do case Mix.env() do - :test -> Alice.ChatBackends.OutboundMock + :test -> Alice.ChatBackends.OutboundFake _else -> Alice.ChatBackends.SlackOutbound end end diff --git a/mix.exs b/mix.exs index f893686..8c330dd 100644 --- a/mix.exs +++ b/mix.exs @@ -46,5 +46,6 @@ defmodule Alice.Mixfile do ] end + defp elixirc_paths(:test), do: ["test/support", "lib"] defp elixirc_paths(_), do: ["lib"] end diff --git a/test/alice/handlers/help_test.exs b/test/alice/handlers/help_test.exs index 4a2d4fe..f04661c 100644 --- a/test/alice/handlers/help_test.exs +++ b/test/alice/handlers/help_test.exs @@ -1,6 +1,5 @@ defmodule Alice.Handlers.HelpTest do use ExUnit.Case, async: true - use Alice.Handlers.Case alias Alice.Handlers.Help alias Alice.Handlers.TestHandler diff --git a/test/alice/handlers/utils_test.exs b/test/alice/handlers/utils_test.exs index e150197..31bfba9 100644 --- a/test/alice/handlers/utils_test.exs +++ b/test/alice/handlers/utils_test.exs @@ -1,19 +1,18 @@ defmodule Alice.Handlers.UtilsTest do use ExUnit.Case - use Alice.Handlers.Case + import Alice.Handlers.Case import Alice.Handlers.Utils test "it should respond to a ping" do - expect_response(["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"]) - ping(fake_conn()) - verify!() + + assert Enum.member?(["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"], first_reply()) end test "it should respond with info about the running bot" do - expect_response(_, 2) - info(fake_conn()) - verify!() + + {:ok, version} = :application.get_key(:alice, :vsn) + assert first_reply() == "Alice #{version} - https://github.com/alice-bot" end end diff --git a/test/alice/router/helpers_test.exs b/test/alice/router/helpers_test.exs index 605b5d0..74ad85e 100644 --- a/test/alice/router/helpers_test.exs +++ b/test/alice/router/helpers_test.exs @@ -1,59 +1,57 @@ defmodule Alice.Router.HelpersTest do use ExUnit.Case - use Alice.Handlers.Case + import Alice.Handlers.Case import Alice.Router.Helpers test "reply returns the conn" do - stub_response() - assert reply("yo", fake_conn()) == fake_conn() end test "reply sends a message with Slack.send_message" do - expect_response("yo") - reply("yo", fake_conn()) - verify!() + + assert first_reply() == "yo" end - test "reply calls random_reply when given a list" do - expect_response("element") + test "multiple replies can be sent in the same handler" do + reply("first", fake_conn()) + reply("second", fake_conn()) + assert ["first", "second"] == replies_received() + end + + test "reply calls random_reply when given a list" do reply(["element"], fake_conn()) - verify!() + + assert first_reply() == "element" end test "random_reply sends a message from a given list" do - expect_response(["rabbit", "hole"]) - ~w[rabbit hole] |> random_reply(fake_conn()) - verify!() + + assert Enum.member?(~w[rabbit hole], first_reply()) end test "chance_reply, when chance passes, \ replies with the given message" do - expect_response("always") - chance_reply(fake_conn(), 1, "always") - verify!() + + assert first_reply() == "always" end test "chance_reply, when chance does not pass, \ when not given negative message, \ does not reply" do - expect_response("never", 0) - chance_reply(fake_conn(), 0, "never") - verify!() + + assert first_reply() == nil end test "chance_reply, when chance does not pass, \ when given negative message, \ replies with negative" do - expect_response("positive", 0) - expect_response("negative", 1) - chance_reply(fake_conn(), 0, "positive", "negative") - verify!() + + assert replies_received() == ["negative"] end end diff --git a/test/support/case.ex b/test/support/case.ex new file mode 100644 index 0000000..ccf9367 --- /dev/null +++ b/test/support/case.ex @@ -0,0 +1,33 @@ +defmodule Alice.Handlers.Case do + def replies_received() do + message = receive do + {:send_message, %{response: message}} -> message + after + 0 -> nil + end + case message do + nil -> [] + message -> [message | replies_received()] + end + end + + def first_reply() do + case replies_received do + [first_message | _] -> first_message + _ -> nil + end + end + + def fake_conn do + %Alice.Conn{message: %{channel: :channel}, slack: :slack} + end + + def fake_conn_with_text(text) do + %Alice.Conn{message: %{text: text, channel: :channel}, slack: :slack} + end + + def fake_conn_with_capture(message, capture_regex) do + fake_conn_with_text(message) + |> Alice.Conn.add_captures(capture_regex) + end +end diff --git a/test/support/outbound_fake.ex b/test/support/outbound_fake.ex new file mode 100644 index 0000000..abd6b27 --- /dev/null +++ b/test/support/outbound_fake.ex @@ -0,0 +1,11 @@ +defmodule Alice.ChatBackends.OutboundFake do + @behaviour Alice.ChatBackends.OutboundClient + + def send_message(response, channel, slack) do + send(self(), {:send_message, %{response: response, channel: channel, slack: slack}}) + end + + def indicate_typing(channel, slack) do + send(self(), {:indicate_typing, %{channel: channel, slack: slack}}) + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index ea684c2..869559e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1 @@ ExUnit.start() -Alice.Handlers.Case.start() From 1da74bed1d24e3380df10a6617a419afc77d064d Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 13:24:08 -0700 Subject: [PATCH 10/34] Renamed Spy correctly --- lib/alice/router/helpers.ex | 2 +- test/support/outbound_fake.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/alice/router/helpers.ex b/lib/alice/router/helpers.ex index 90fc303..03af970 100644 --- a/lib/alice/router/helpers.ex +++ b/lib/alice/router/helpers.ex @@ -31,7 +31,7 @@ defmodule Alice.Router.Helpers do defp outbound_api do case Mix.env() do - :test -> Alice.ChatBackends.OutboundFake + :test -> Alice.ChatBackends.OutboundSpy _else -> Alice.ChatBackends.SlackOutbound end end diff --git a/test/support/outbound_fake.ex b/test/support/outbound_fake.ex index abd6b27..597da40 100644 --- a/test/support/outbound_fake.ex +++ b/test/support/outbound_fake.ex @@ -1,4 +1,4 @@ -defmodule Alice.ChatBackends.OutboundFake do +defmodule Alice.ChatBackends.OutboundSpy do @behaviour Alice.ChatBackends.OutboundClient def send_message(response, channel, slack) do From e317d4ada476b25412c5cb989029bb6771c488a7 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 13:30:10 -0700 Subject: [PATCH 11/34] Removed mox dependency --- mix.exs | 3 +-- test/support/case.ex | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/mix.exs b/mix.exs index 8c330dd..27c1101 100644 --- a/mix.exs +++ b/mix.exs @@ -29,8 +29,7 @@ defmodule Alice.Mixfile do {:poolboy, "~> 1.5.0"}, {:redix, "~> 0.6.0"}, {:poison, "~> 3.0"}, - {:dialyxir, "~> 1.0.0-rc.6", only: [:dev], runtime: false}, - {:mox, "~> 0.5", only: [:test]} + {:dialyxir, "~> 1.0.0-rc.6", only: [:dev], runtime: false} ] end diff --git a/test/support/case.ex b/test/support/case.ex index ccf9367..cd31487 100644 --- a/test/support/case.ex +++ b/test/support/case.ex @@ -12,7 +12,7 @@ defmodule Alice.Handlers.Case do end def first_reply() do - case replies_received do + case replies_received() do [first_message | _] -> first_message _ -> nil end From 79730853ea0f4ada1d334ae31aa054d780747d2b Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 13:35:53 -0700 Subject: [PATCH 12/34] Make sure test supports are in the path --- mix.exs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 27c1101..5483e42 100644 --- a/mix.exs +++ b/mix.exs @@ -45,6 +45,5 @@ defmodule Alice.Mixfile do ] end - defp elixirc_paths(:test), do: ["test/support", "lib"] - defp elixirc_paths(_), do: ["lib"] + defp elixirc_paths(_), do: ["test/support", "lib"] end From 371453f14316eeb2d7457c8ef5f69ec8eb54d7aa Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 14:07:21 -0700 Subject: [PATCH 13/34] Update README to match new testing style --- README.md | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index e0512e8..ce372e8 100644 --- a/README.md +++ b/README.md @@ -179,8 +179,7 @@ end defp deps do [ - {:alice, "~> 0.4.0"}, - {:mox, "~> 0.5", only: test} + {:alice, "~> 0.4.0"} ] end ``` @@ -211,36 +210,27 @@ end ### Testing Handlers -To test a handler, we've exposed a few macros that create a mock Alice's -outbound internals so that you can test your handler's actual -implementation. Do do so, you'll need to add the following to your test -helper to ensure things are started properly. - -In `test/test_helper.exs`: - -```elixir -Alice.Handlers.Case.start() -``` - -Then you can write the actual test. You set the expectation for what you -want the final message to be sent to the chat to be, then you can create -a fake of a connection to pass into your method. From there, you just -pass it in, then finally verify that all your expectations were met. +Alice provides several helpers to make it easy to test your handlers. +First you'll need to generate a fake connection which will need to +consist of an example message as well as the regex used to capture it. +Then that will be fed to your actual method. From there you can use +either `first_reply()` to get the first reply sent out or +`replies_received()` which will return a List of replies that have been +received during your test. You can use either to use normal assertions +on to ensure your handler behaves in the manner you expect. In `test/alice/handlers/google_images_test.exs`: ```elixir defmodule Alice.Handlers.GoogleImagesTest do use ExUnit.Case - use Alice.Handlers.Case + import Alice.Handlers.Case test "it fetches an image when asked" do - expect_response("http://example.com/image_from_google.jpg") - fake_conn_with_capture("img me example image", ~r/(image|img)\s+me (?.+)/i) |> Alice.Handlers.GoogleImages.fetch - verify!() + assert first_reply() == "http://example.com/image_from_google.jpg" end end ``` From 6bba99f8623dd3ba3f9b66973372ea711b966f93 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 15:24:50 -0700 Subject: [PATCH 14/34] Implemented new test_message method to actually test the routing --- test/alice/handlers/utils_test.exs | 7 +++---- test/support/case.ex | 26 +++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/test/alice/handlers/utils_test.exs b/test/alice/handlers/utils_test.exs index 31bfba9..5dc0814 100644 --- a/test/alice/handlers/utils_test.exs +++ b/test/alice/handlers/utils_test.exs @@ -1,16 +1,15 @@ defmodule Alice.Handlers.UtilsTest do use ExUnit.Case - import Alice.Handlers.Case - import Alice.Handlers.Utils + use Alice.Handlers.Case, handlers: Alice.Handlers.Utils test "it should respond to a ping" do - ping(fake_conn()) + send_test_message("ping") assert Enum.member?(["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"], first_reply()) end test "it should respond with info about the running bot" do - info(fake_conn()) + send_test_message("<@alice> info") {:ok, version} = :application.get_key(:alice, :vsn) assert first_reply() == "Alice #{version} - https://github.com/alice-bot" diff --git a/test/support/case.ex b/test/support/case.ex index cd31487..dae8a74 100644 --- a/test/support/case.ex +++ b/test/support/case.ex @@ -23,11 +23,35 @@ defmodule Alice.Handlers.Case do end def fake_conn_with_text(text) do - %Alice.Conn{message: %{text: text, channel: :channel}, slack: :slack} + %Alice.Conn{message: %{text: text, channel: :channel, user: 0}, slack: %{users: ["fake_user"], me: %{id: "alice"}}} end def fake_conn_with_capture(message, capture_regex) do fake_conn_with_text(message) |> Alice.Conn.add_captures(capture_regex) end + + def send_test_message(message) do + conn = fake_conn_with_text(message) + case Alice.Conn.command?(conn) do + true -> Alice.Router.match_commands(conn) + false -> Alice.Router.match_routes(conn) + end + end + + defmacro __using__(opts \\ []) do + handlers = opts + |> Keyword.get(:handlers, []) + |> List.wrap() + + quote do + import Alice.Handlers.Case + + setup do + Alice.Router.start_link(unquote(handlers)) + + :ok + end + end + end end From 1e8486bbd4527692200b211d9a0532399ef9cc37 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 16:02:18 -0700 Subject: [PATCH 15/34] Renamed Case helper methods --- test/alice/handlers/utils_test.exs | 4 ++-- test/alice/router/helpers_test.exs | 4 ++-- test/support/case.ex | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/alice/handlers/utils_test.exs b/test/alice/handlers/utils_test.exs index 5dc0814..5f4e16c 100644 --- a/test/alice/handlers/utils_test.exs +++ b/test/alice/handlers/utils_test.exs @@ -3,13 +3,13 @@ defmodule Alice.Handlers.UtilsTest do use Alice.Handlers.Case, handlers: Alice.Handlers.Utils test "it should respond to a ping" do - send_test_message("ping") + receive_message("ping") assert Enum.member?(["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"], first_reply()) end test "it should respond with info about the running bot" do - send_test_message("<@alice> info") + receive_message("<@alice> info") {:ok, version} = :application.get_key(:alice, :vsn) assert first_reply() == "Alice #{version} - https://github.com/alice-bot" diff --git a/test/alice/router/helpers_test.exs b/test/alice/router/helpers_test.exs index 74ad85e..55a243c 100644 --- a/test/alice/router/helpers_test.exs +++ b/test/alice/router/helpers_test.exs @@ -17,7 +17,7 @@ defmodule Alice.Router.HelpersTest do reply("first", fake_conn()) reply("second", fake_conn()) - assert ["first", "second"] == replies_received() + assert ["first", "second"] == all_replies() end test "reply calls random_reply when given a list" do @@ -52,6 +52,6 @@ defmodule Alice.Router.HelpersTest do replies with negative" do chance_reply(fake_conn(), 0, "positive", "negative") - assert replies_received() == ["negative"] + assert all_replies() == ["negative"] end end diff --git a/test/support/case.ex b/test/support/case.ex index dae8a74..27ba83a 100644 --- a/test/support/case.ex +++ b/test/support/case.ex @@ -1,5 +1,5 @@ defmodule Alice.Handlers.Case do - def replies_received() do + def all_replies() do message = receive do {:send_message, %{response: message}} -> message after @@ -7,12 +7,12 @@ defmodule Alice.Handlers.Case do end case message do nil -> [] - message -> [message | replies_received()] + message -> [message | all_replies()] end end def first_reply() do - case replies_received() do + case all_replies() do [first_message | _] -> first_message _ -> nil end @@ -31,7 +31,7 @@ defmodule Alice.Handlers.Case do |> Alice.Conn.add_captures(capture_regex) end - def send_test_message(message) do + def receive_message(message) do conn = fake_conn_with_text(message) case Alice.Conn.command?(conn) do true -> Alice.Router.match_commands(conn) From f79724a8bb83bfc5c9522c20bbe0ed38e2c8879e Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 16:04:03 -0700 Subject: [PATCH 16/34] Use better format for Enum checking --- test/alice/handlers/utils_test.exs | 2 +- test/alice/router/helpers_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/alice/handlers/utils_test.exs b/test/alice/handlers/utils_test.exs index 5f4e16c..f16daad 100644 --- a/test/alice/handlers/utils_test.exs +++ b/test/alice/handlers/utils_test.exs @@ -5,7 +5,7 @@ defmodule Alice.Handlers.UtilsTest do test "it should respond to a ping" do receive_message("ping") - assert Enum.member?(["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"], first_reply()) + assert first_reply() in ["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"] end test "it should respond with info about the running bot" do diff --git a/test/alice/router/helpers_test.exs b/test/alice/router/helpers_test.exs index 55a243c..45f3d99 100644 --- a/test/alice/router/helpers_test.exs +++ b/test/alice/router/helpers_test.exs @@ -29,7 +29,7 @@ defmodule Alice.Router.HelpersTest do test "random_reply sends a message from a given list" do ~w[rabbit hole] |> random_reply(fake_conn()) - assert Enum.member?(~w[rabbit hole], first_reply()) + assert first_reply() in ~w[rabbit hole] end test "chance_reply, when chance passes, \ From 1a7a23dcfb353022adae97f4fa4d13689f08bd6c Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 16:31:34 -0700 Subject: [PATCH 17/34] Fill out more of the fke connection --- test/support/case.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/support/case.ex b/test/support/case.ex index 27ba83a..3abb3f9 100644 --- a/test/support/case.ex +++ b/test/support/case.ex @@ -23,7 +23,7 @@ defmodule Alice.Handlers.Case do end def fake_conn_with_text(text) do - %Alice.Conn{message: %{text: text, channel: :channel, user: 0}, slack: %{users: ["fake_user"], me: %{id: "alice"}}} + %Alice.Conn{message: %{text: text, channel: :channel, user: :fake_user}, slack: %{users: [fake_user: %{name: "fake_user"}], me: %{id: :alice}}} end def fake_conn_with_capture(message, capture_regex) do From c4da8d8c63ea85f8594ff36e2582aa7f314c5618 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 17:44:32 -0700 Subject: [PATCH 18/34] Remove logger from testing --- lib/alice/router.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/alice/router.ex b/lib/alice/router.ex index a1af363..62a9a1d 100644 --- a/lib/alice/router.ex +++ b/lib/alice/router.ex @@ -89,7 +89,9 @@ defmodule Alice.Router do def match_pattern({pattern, name}, {mod, conn = %Conn{}}) do if Regex.match?(pattern, conn.message.text) do - Logger.info("#{mod}.#{name} responding to -> #{Conn.user(conn)}") + unless(Mix.env() == :test) do + Logger.info("#{mod}.#{name} responding to -> #{Conn.user(conn)}") + end {mod, apply(mod, name, [Conn.add_captures(conn, pattern)])} else {mod, conn} From 8876d6e90178c306c3d04026dbb3ee9768855a35 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 18:18:44 -0700 Subject: [PATCH 19/34] Update README --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ce372e8..48285c2 100644 --- a/README.md +++ b/README.md @@ -211,11 +211,12 @@ end ### Testing Handlers Alice provides several helpers to make it easy to test your handlers. -First you'll need to generate a fake connection which will need to -consist of an example message as well as the regex used to capture it. -Then that will be fed to your actual method. From there you can use -either `first_reply()` to get the first reply sent out or -`replies_received()` which will return a List of replies that have been +First you'll need to invoke to add `use Alice.Handlers.Case, handlers: +[YourHandler]` passing it the handler you're trying to test. Then you +can use `message_received()` within your test, which will simulate a +message coming in from the chat backend and route it through to the +handlers appropriately. From there you can use either `first_reply()` +to get the first reply sent out or `all_replies()` which will return a List of replies that have been received during your test. You can use either to use normal assertions on to ensure your handler behaves in the manner you expect. @@ -224,11 +225,10 @@ In `test/alice/handlers/google_images_test.exs`: ```elixir defmodule Alice.Handlers.GoogleImagesTest do use ExUnit.Case - import Alice.Handlers.Case + use Alice.Handlers.Case, handlers: Alice.Handlers.GoogleImages test "it fetches an image when asked" do - fake_conn_with_capture("img me example image", ~r/(image|img)\s+me (?.+)/i) - |> Alice.Handlers.GoogleImages.fetch + receive_message("img me example image") assert first_reply() == "http://example.com/image_from_google.jpg" end From 1d405bec602fe45824bd3c3f15a894062dc36dcd Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 20:03:58 -0700 Subject: [PATCH 20/34] Unify to one fake_conn function --- test/support/case.ex | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test/support/case.ex b/test/support/case.ex index 3abb3f9..e355ebf 100644 --- a/test/support/case.ex +++ b/test/support/case.ex @@ -18,21 +18,18 @@ defmodule Alice.Handlers.Case do end end - def fake_conn do - %Alice.Conn{message: %{channel: :channel}, slack: :slack} - end - - def fake_conn_with_text(text) do + def fake_conn(), do: fake_conn("") + def fake_conn(text) do %Alice.Conn{message: %{text: text, channel: :channel, user: :fake_user}, slack: %{users: [fake_user: %{name: "fake_user"}], me: %{id: :alice}}} end def fake_conn_with_capture(message, capture_regex) do - fake_conn_with_text(message) + fake_conn(message) |> Alice.Conn.add_captures(capture_regex) end def receive_message(message) do - conn = fake_conn_with_text(message) + conn = fake_conn(message) case Alice.Conn.command?(conn) do true -> Alice.Router.match_commands(conn) false -> Alice.Router.match_routes(conn) From e30d46e6b8a3242ca974db3f8788c81299d8919c Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 20:05:59 -0700 Subject: [PATCH 21/34] Update README to include the command string --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 48285c2..10ed863 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,8 @@ First you'll need to invoke to add `use Alice.Handlers.Case, handlers: [YourHandler]` passing it the handler you're trying to test. Then you can use `message_received()` within your test, which will simulate a message coming in from the chat backend and route it through to the -handlers appropriately. From there you can use either `first_reply()` +handlers appropriately. If you're wanting to invoke a command, you'll +need to make sure your message includes `<@alice>` within the string. From there you can use either `first_reply()` to get the first reply sent out or `all_replies()` which will return a List of replies that have been received during your test. You can use either to use normal assertions on to ensure your handler behaves in the manner you expect. From 58e04a1ffe6e17569b57c85b4f8782c7bba6bc3c Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Fri, 10 Apr 2020 22:32:54 -0700 Subject: [PATCH 22/34] Add tests for typing --- lib/alice/router/helpers.ex | 1 - test/alice/router/helpers_test.exs | 6 ++++++ test/support/case.ex | 8 ++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/alice/router/helpers.ex b/lib/alice/router/helpers.ex index 03af970..2e2ea21 100644 --- a/lib/alice/router/helpers.ex +++ b/lib/alice/router/helpers.ex @@ -89,7 +89,6 @@ defmodule Alice.Router.Helpers do @spec delayed_reply(Conn.t(), String.t(), integer) :: Task.t() @spec delayed_reply(String.t(), integer, Conn.t()) :: Task.t() def delayed_reply(msg, ms, conn = %Conn{}), do: delayed_reply(conn, msg, ms) - def delayed_reply(conn = %Conn{}, message, milliseconds) do Task.async(fn -> conn = indicate_typing(conn) diff --git a/test/alice/router/helpers_test.exs b/test/alice/router/helpers_test.exs index 45f3d99..e77fa26 100644 --- a/test/alice/router/helpers_test.exs +++ b/test/alice/router/helpers_test.exs @@ -54,4 +54,10 @@ defmodule Alice.Router.HelpersTest do assert all_replies() == ["negative"] end + + test "it should indicate typing when asked" do + indicate_typing(fake_conn()) + + assert typing?() == true + end end diff --git a/test/support/case.ex b/test/support/case.ex index e355ebf..0a1fde2 100644 --- a/test/support/case.ex +++ b/test/support/case.ex @@ -36,6 +36,14 @@ defmodule Alice.Handlers.Case do end end + def typing?() do + receive do + {:indicate_typing, _} -> true + after + 0 -> false + end + end + defmacro __using__(opts \\ []) do handlers = opts |> Keyword.get(:handlers, []) From 1fa9ab3e46714af6ec612fd5503e872362854151 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 07:31:57 -0700 Subject: [PATCH 23/34] Update test/alice/router/helpers_test.exs Co-Authored-By: Adam Zaninovich --- test/alice/router/helpers_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/alice/router/helpers_test.exs b/test/alice/router/helpers_test.exs index e77fa26..1416764 100644 --- a/test/alice/router/helpers_test.exs +++ b/test/alice/router/helpers_test.exs @@ -58,6 +58,6 @@ defmodule Alice.Router.HelpersTest do test "it should indicate typing when asked" do indicate_typing(fake_conn()) - assert typing?() == true + assert typing?() end end From 0b934f5c5e98507cd22a7833df7e91be4da24615 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 07:32:22 -0700 Subject: [PATCH 24/34] Use an atom instead of nil Co-Authored-By: Adam Zaninovich --- test/support/case.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/support/case.ex b/test/support/case.ex index 0a1fde2..385fdb2 100644 --- a/test/support/case.ex +++ b/test/support/case.ex @@ -3,10 +3,10 @@ defmodule Alice.Handlers.Case do message = receive do {:send_message, %{response: message}} -> message after - 0 -> nil + 0 -> :no_message_received end case message do - nil -> [] + :no_message_received -> [] message -> [message | all_replies()] end end From d79743a144b5ae075596510ce220aefac9273ea3 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 07:33:53 -0700 Subject: [PATCH 25/34] Better structure on fake_conn_with_capture Co-Authored-By: Adam Zaninovich --- test/support/case.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/support/case.ex b/test/support/case.ex index 385fdb2..7742a5f 100644 --- a/test/support/case.ex +++ b/test/support/case.ex @@ -24,7 +24,8 @@ defmodule Alice.Handlers.Case do end def fake_conn_with_capture(message, capture_regex) do - fake_conn(message) + message + |> fake_conn() |> Alice.Conn.add_captures(capture_regex) end From 80e8dc16ac409bac07baff5faef66bebd5848cb6 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 07:36:05 -0700 Subject: [PATCH 26/34] Removed mox from lockfile --- mix.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/mix.lock b/mix.lock index 5caa965..e49c81f 100644 --- a/mix.lock +++ b/mix.lock @@ -17,7 +17,6 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm", "7a4c8e1115a2732a67d7624e28cf6c9f30c66711a9e92928e745c255887ba465"}, - "mox": {:hex, :mox, "0.5.2", "55a0a5ba9ccc671518d068c8dddd20eeb436909ea79d1799e2209df7eaa98b6c", [:mix], [], "hexpm", "df4310628cd628ee181df93f50ddfd07be3e5ecc30232d3b6aadf30bdfe6092b"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, From 59650ab69272640e8ea2da36ff169a2b0a7ae1b1 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 07:43:39 -0700 Subject: [PATCH 27/34] Rename Alice.Handlers.Case to Alice.HandlersCase --- README.md | 4 ++-- test/alice/handlers/utils_test.exs | 2 +- test/alice/router/helpers_test.exs | 2 +- test/support/{case.ex => handlers_case.ex} | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename test/support/{case.ex => handlers_case.ex} (95%) diff --git a/README.md b/README.md index 10ed863..81f0685 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ end ### Testing Handlers Alice provides several helpers to make it easy to test your handlers. -First you'll need to invoke to add `use Alice.Handlers.Case, handlers: +First you'll need to invoke to add `use Alice.HandlersCase, handlers: [YourHandler]` passing it the handler you're trying to test. Then you can use `message_received()` within your test, which will simulate a message coming in from the chat backend and route it through to the @@ -226,7 +226,7 @@ In `test/alice/handlers/google_images_test.exs`: ```elixir defmodule Alice.Handlers.GoogleImagesTest do use ExUnit.Case - use Alice.Handlers.Case, handlers: Alice.Handlers.GoogleImages + use Alice.HandlersCase, handlers: Alice.Handlers.GoogleImages test "it fetches an image when asked" do receive_message("img me example image") diff --git a/test/alice/handlers/utils_test.exs b/test/alice/handlers/utils_test.exs index f16daad..e75e86a 100644 --- a/test/alice/handlers/utils_test.exs +++ b/test/alice/handlers/utils_test.exs @@ -1,6 +1,6 @@ defmodule Alice.Handlers.UtilsTest do use ExUnit.Case - use Alice.Handlers.Case, handlers: Alice.Handlers.Utils + use Alice.HandlersCase, handlers: Alice.Handlers.Utils test "it should respond to a ping" do receive_message("ping") diff --git a/test/alice/router/helpers_test.exs b/test/alice/router/helpers_test.exs index 1416764..fca2898 100644 --- a/test/alice/router/helpers_test.exs +++ b/test/alice/router/helpers_test.exs @@ -1,6 +1,6 @@ defmodule Alice.Router.HelpersTest do use ExUnit.Case - import Alice.Handlers.Case + import Alice.HandlersCase import Alice.Router.Helpers test "reply returns the conn" do diff --git a/test/support/case.ex b/test/support/handlers_case.ex similarity index 95% rename from test/support/case.ex rename to test/support/handlers_case.ex index 7742a5f..f36185b 100644 --- a/test/support/case.ex +++ b/test/support/handlers_case.ex @@ -1,4 +1,4 @@ -defmodule Alice.Handlers.Case do +defmodule Alice.HandlersCase do def all_replies() do message = receive do {:send_message, %{response: message}} -> message @@ -51,7 +51,7 @@ defmodule Alice.Handlers.Case do |> List.wrap() quote do - import Alice.Handlers.Case + import Alice.HandlersCase setup do Alice.Router.start_link(unquote(handlers)) From fb74d217da3f6fbf40e631a80a11b124a90b75d0 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 07:54:58 -0700 Subject: [PATCH 28/34] Automatically add ExUnit.Case --- test/alice/handlers/utils_test.exs | 1 - test/support/handlers_case.ex | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/test/alice/handlers/utils_test.exs b/test/alice/handlers/utils_test.exs index e75e86a..9cf5c06 100644 --- a/test/alice/handlers/utils_test.exs +++ b/test/alice/handlers/utils_test.exs @@ -1,5 +1,4 @@ defmodule Alice.Handlers.UtilsTest do - use ExUnit.Case use Alice.HandlersCase, handlers: Alice.Handlers.Utils test "it should respond to a ping" do diff --git a/test/support/handlers_case.ex b/test/support/handlers_case.ex index f36185b..f7a7b45 100644 --- a/test/support/handlers_case.ex +++ b/test/support/handlers_case.ex @@ -51,6 +51,7 @@ defmodule Alice.HandlersCase do |> List.wrap() quote do + use ExUnit.Case import Alice.HandlersCase setup do From 923b358be3ccb91a8223642d493054a4ab2ef65f Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 07:55:50 -0700 Subject: [PATCH 29/34] Remove need for ExUnit in Handler tests --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 81f0685..5b40691 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,6 @@ In `test/alice/handlers/google_images_test.exs`: ```elixir defmodule Alice.Handlers.GoogleImagesTest do - use ExUnit.Case use Alice.HandlersCase, handlers: Alice.Handlers.GoogleImages test "it fetches an image when asked" do From a31ba610b89e6713d76bd730fd99bba55518392b Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 08:03:24 -0700 Subject: [PATCH 30/34] Rename receive_message to send_message --- README.md | 2 +- test/alice/handlers/utils_test.exs | 4 ++-- test/support/handlers_case.ex | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5b40691..b0a1405 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ defmodule Alice.Handlers.GoogleImagesTest do use Alice.HandlersCase, handlers: Alice.Handlers.GoogleImages test "it fetches an image when asked" do - receive_message("img me example image") + send_message("img me example image") assert first_reply() == "http://example.com/image_from_google.jpg" end diff --git a/test/alice/handlers/utils_test.exs b/test/alice/handlers/utils_test.exs index 9cf5c06..5d7abd6 100644 --- a/test/alice/handlers/utils_test.exs +++ b/test/alice/handlers/utils_test.exs @@ -2,13 +2,13 @@ defmodule Alice.Handlers.UtilsTest do use Alice.HandlersCase, handlers: Alice.Handlers.Utils test "it should respond to a ping" do - receive_message("ping") + send_message("ping") assert first_reply() in ["PONG!", "Can I help you?", "Yes...I'm still here.", "I'm alive!"] end test "it should respond with info about the running bot" do - receive_message("<@alice> info") + send_message("<@alice> info") {:ok, version} = :application.get_key(:alice, :vsn) assert first_reply() == "Alice #{version} - https://github.com/alice-bot" diff --git a/test/support/handlers_case.ex b/test/support/handlers_case.ex index f7a7b45..e8a3fa4 100644 --- a/test/support/handlers_case.ex +++ b/test/support/handlers_case.ex @@ -29,7 +29,7 @@ defmodule Alice.HandlersCase do |> Alice.Conn.add_captures(capture_regex) end - def receive_message(message) do + def send_message(message) do conn = fake_conn(message) case Alice.Conn.command?(conn) do true -> Alice.Router.match_commands(conn) From 61aed73185eb2b87b11f8941f18a11a830f0b0a0 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 08:10:07 -0700 Subject: [PATCH 31/34] Split send_message funciton to allow using with a connection --- test/support/handlers_case.ex | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/support/handlers_case.ex b/test/support/handlers_case.ex index e8a3fa4..028eedf 100644 --- a/test/support/handlers_case.ex +++ b/test/support/handlers_case.ex @@ -29,13 +29,18 @@ defmodule Alice.HandlersCase do |> Alice.Conn.add_captures(capture_regex) end - def send_message(message) do - conn = fake_conn(message) + + def send_message(conn = %Alice.Conn{}) do case Alice.Conn.command?(conn) do true -> Alice.Router.match_commands(conn) false -> Alice.Router.match_routes(conn) end end + def send_message(message) do + message + |> fake_conn() + |> send_message() + end def typing?() do receive do From 72e01a509d712771d31efd205b249c9389ad38cb Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 08:18:42 -0700 Subject: [PATCH 32/34] reworked fake_conn_with_capture --- test/support/handlers_case.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/support/handlers_case.ex b/test/support/handlers_case.ex index 028eedf..065f3fa 100644 --- a/test/support/handlers_case.ex +++ b/test/support/handlers_case.ex @@ -22,8 +22,7 @@ defmodule Alice.HandlersCase do def fake_conn(text) do %Alice.Conn{message: %{text: text, channel: :channel, user: :fake_user}, slack: %{users: [fake_user: %{name: "fake_user"}], me: %{id: :alice}}} end - - def fake_conn_with_capture(message, capture_regex) do + def fake_conn(message, capture: capture_regex) do message |> fake_conn() |> Alice.Conn.add_captures(capture_regex) From 17753bd2cdee517881c3f59e6f37ebbaa4618af2 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 10:41:13 -0700 Subject: [PATCH 33/34] ALL THE DOCUMENTATION --- lib/alice/chat_backends/slack_outbound.ex | 3 + lib/alice/router.ex | 2 +- test/support/handlers_case.ex | 110 +++++++++++++++--- .../{outbound_fake.ex => outbound_spy.ex} | 5 + 4 files changed, 102 insertions(+), 18 deletions(-) rename test/support/{outbound_fake.ex => outbound_spy.ex} (54%) diff --git a/lib/alice/chat_backends/slack_outbound.ex b/lib/alice/chat_backends/slack_outbound.ex index 4d45b49..ed8d82b 100644 --- a/lib/alice/chat_backends/slack_outbound.ex +++ b/lib/alice/chat_backends/slack_outbound.ex @@ -1,7 +1,10 @@ defmodule Alice.ChatBackends.SlackOutbound do + @moduledoc "An Adapter for outbound messages to Slack." @behaviour Alice.ChatBackends.OutboundClient + @doc "Sends a message back to slack" def send_message(response, channel, slack), do: Slack.Sends.send_message(response, channel, slack) + @doc "Makes Alice indicate she's typing in the appropriate channel" def indicate_typing(channel, slack), do: Slack.Sends.indicate_typing(channel, slack) end diff --git a/lib/alice/router.ex b/lib/alice/router.ex index 62a9a1d..c39f0f6 100644 --- a/lib/alice/router.ex +++ b/lib/alice/router.ex @@ -89,7 +89,7 @@ defmodule Alice.Router do def match_pattern({pattern, name}, {mod, conn = %Conn{}}) do if Regex.match?(pattern, conn.message.text) do - unless(Mix.env() == :test) do + unless Mix.env() == :test do Logger.info("#{mod}.#{name} responding to -> #{Conn.user(conn)}") end {mod, apply(mod, name, [Conn.add_captures(conn, pattern)])} diff --git a/test/support/handlers_case.ex b/test/support/handlers_case.ex index 065f3fa..461e082 100644 --- a/test/support/handlers_case.ex +++ b/test/support/handlers_case.ex @@ -1,23 +1,37 @@ defmodule Alice.HandlersCase do - def all_replies() do - message = receive do - {:send_message, %{response: message}} -> message - after - 0 -> :no_message_received - end - case message do - :no_message_received -> [] - message -> [message | all_replies()] - end - end + @moduledoc """ + Helpers for writing tests of Alice Handlers. + + When used it accepts the following options: + * `:handlers` - The handler (or List of handlers) that you want to test. Defaults to [] (thereby giving you no handlers to test) + + `use`ing this handler automatically brings in `ExUnit.Case` as well. - def first_reply() do - case all_replies() do - [first_message | _] -> first_message - _ -> nil - end - end + ## Examples + + defmodule Alice.Handlers.ExampleHandlerTest do + use Alice.HandlersCase, handlers: Alice.Handlers.ExampleHandler + + test "it replies" do + send_message("hello") + assert first_reply() == "world" + end + end + """ + @doc """ + Generates a fake connection for testing purposes. + + Can be called as `fake_conn/0` to generate a quick connection. Or it can be called as `fake_conn/1` to pass a message. Or finally can be called as `fake_conn/2` to set options with the message. + + ## Example + + test "you can directly use the reply function" do + conn = fake_conn() + reply("hello world", conn) + assert first_reply() == "hello world" + end + """ def fake_conn(), do: fake_conn("") def fake_conn(text) do %Alice.Conn{message: %{text: text, channel: :channel, user: :fake_user}, slack: %{users: [fake_user: %{name: "fake_user"}], me: %{id: :alice}}} @@ -28,7 +42,18 @@ defmodule Alice.HandlersCase do |> Alice.Conn.add_captures(capture_regex) end + @doc """ + Sends a message through Alice that can be captured by the handlers. + + Can either be called with a `String` or with an `Alice.Conn` + ## Examples + + test "it sends a message" do + send_message("test message") + assert first_reply() == "reply from handler" + end + """ def send_message(conn = %Alice.Conn{}) do case Alice.Conn.command?(conn) do true -> Alice.Router.match_commands(conn) @@ -41,6 +66,57 @@ defmodule Alice.HandlersCase do |> send_message() end + @doc """ + Retrieves a `List` of all the replies that Alice has sent out since the test began. + + ## Examples + + test "you can send multiple messages" do + send_message("first") + send_message("second") + assert all_replies() == ["first", "second"] + end + """ + def all_replies() do + message = receive do + {:send_message, %{response: message}} -> message + after + 0 -> :no_message_received + end + case message do + :no_message_received -> [] + message -> [message | all_replies()] + end + end + + @doc """ + Retrieves the first reply that Alice sent out since the test began. + + ## Examples + + test "it only brings back the first message" do + send_message("first") + send_message("second") + assert first_reply() == "first" + end + """ + def first_reply() do + case all_replies() do + [first_message | _] -> first_message + _ -> nil + end + end + + @doc """ + Verifies that typing was indicated during the test. + + ## Examples + + test "the handler indicated typing" do + send_message("message that causes the handler to indicate typing") + assert typing? + end + """ def typing?() do receive do {:indicate_typing, _} -> true diff --git a/test/support/outbound_fake.ex b/test/support/outbound_spy.ex similarity index 54% rename from test/support/outbound_fake.ex rename to test/support/outbound_spy.ex index 597da40..4d2657a 100644 --- a/test/support/outbound_fake.ex +++ b/test/support/outbound_spy.ex @@ -1,10 +1,15 @@ defmodule Alice.ChatBackends.OutboundSpy do + @moduledoc """ + A Spy to capture messages sent to the OutboundClient during testing. + """ @behaviour Alice.ChatBackends.OutboundClient + @doc "Sends the message back to the process so it can be retrieved later during the test" def send_message(response, channel, slack) do send(self(), {:send_message, %{response: response, channel: channel, slack: slack}}) end + @doc "Sends a message indicating typing back to the process so it can be retrieved later during the test" def indicate_typing(channel, slack) do send(self(), {:indicate_typing, %{channel: channel, slack: slack}}) end From 350e448c1bda7ae7ba0a8c8926ce76747f4e4ca4 Mon Sep 17 00:00:00 2001 From: Nathaniel Barnes Date: Sat, 11 Apr 2020 10:43:10 -0700 Subject: [PATCH 34/34] Formatting changes --- lib/alice/chat_backends/outbound_client.ex | 3 +- lib/alice/chat_backends/slack_outbound.ex | 3 +- lib/alice/router.ex | 1 + lib/alice/router/helpers.ex | 1 + test/support/handlers_case.ex | 45 +++++++++++++--------- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/lib/alice/chat_backends/outbound_client.ex b/lib/alice/chat_backends/outbound_client.ex index e438d15..a3cdf9c 100644 --- a/lib/alice/chat_backends/outbound_client.ex +++ b/lib/alice/chat_backends/outbound_client.ex @@ -3,6 +3,7 @@ defmodule Alice.ChatBackends.OutboundClient do Documentation for the OutboundClient behavior. This defines a behavior for modules that serve as an outbound connection to a backend. """ - @callback send_message(response :: String.t(), channel :: String.t(), backend :: map()) :: String.t() + @callback send_message(response :: String.t(), channel :: String.t(), backend :: map()) :: + String.t() @callback indicate_typing(channel :: String.t(), backend :: map()) :: String.t() end diff --git a/lib/alice/chat_backends/slack_outbound.ex b/lib/alice/chat_backends/slack_outbound.ex index ed8d82b..866fe37 100644 --- a/lib/alice/chat_backends/slack_outbound.ex +++ b/lib/alice/chat_backends/slack_outbound.ex @@ -3,7 +3,8 @@ defmodule Alice.ChatBackends.SlackOutbound do @behaviour Alice.ChatBackends.OutboundClient @doc "Sends a message back to slack" - def send_message(response, channel, slack), do: Slack.Sends.send_message(response, channel, slack) + def send_message(response, channel, slack), + do: Slack.Sends.send_message(response, channel, slack) @doc "Makes Alice indicate she's typing in the appropriate channel" def indicate_typing(channel, slack), do: Slack.Sends.indicate_typing(channel, slack) diff --git a/lib/alice/router.ex b/lib/alice/router.ex index c39f0f6..bf49b25 100644 --- a/lib/alice/router.ex +++ b/lib/alice/router.ex @@ -92,6 +92,7 @@ defmodule Alice.Router do unless Mix.env() == :test do Logger.info("#{mod}.#{name} responding to -> #{Conn.user(conn)}") end + {mod, apply(mod, name, [Conn.add_captures(conn, pattern)])} else {mod, conn} diff --git a/lib/alice/router/helpers.ex b/lib/alice/router/helpers.ex index 2e2ea21..03af970 100644 --- a/lib/alice/router/helpers.ex +++ b/lib/alice/router/helpers.ex @@ -89,6 +89,7 @@ defmodule Alice.Router.Helpers do @spec delayed_reply(Conn.t(), String.t(), integer) :: Task.t() @spec delayed_reply(String.t(), integer, Conn.t()) :: Task.t() def delayed_reply(msg, ms, conn = %Conn{}), do: delayed_reply(conn, msg, ms) + def delayed_reply(conn = %Conn{}, message, milliseconds) do Task.async(fn -> conn = indicate_typing(conn) diff --git a/test/support/handlers_case.ex b/test/support/handlers_case.ex index 461e082..45a7767 100644 --- a/test/support/handlers_case.ex +++ b/test/support/handlers_case.ex @@ -1,14 +1,14 @@ defmodule Alice.HandlersCase do @moduledoc """ Helpers for writing tests of Alice Handlers. - + When used it accepts the following options: * `:handlers` - The handler (or List of handlers) that you want to test. Defaults to [] (thereby giving you no handlers to test) - + `use`ing this handler automatically brings in `ExUnit.Case` as well. ## Examples - + defmodule Alice.Handlers.ExampleHandlerTest do use Alice.HandlersCase, handlers: Alice.Handlers.ExampleHandler @@ -21,11 +21,11 @@ defmodule Alice.HandlersCase do @doc """ Generates a fake connection for testing purposes. - + Can be called as `fake_conn/0` to generate a quick connection. Or it can be called as `fake_conn/1` to pass a message. Or finally can be called as `fake_conn/2` to set options with the message. ## Example - + test "you can directly use the reply function" do conn = fake_conn() reply("hello world", conn) @@ -33,9 +33,14 @@ defmodule Alice.HandlersCase do end """ def fake_conn(), do: fake_conn("") + def fake_conn(text) do - %Alice.Conn{message: %{text: text, channel: :channel, user: :fake_user}, slack: %{users: [fake_user: %{name: "fake_user"}], me: %{id: :alice}}} + %Alice.Conn{ + message: %{text: text, channel: :channel, user: :fake_user}, + slack: %{users: [fake_user: %{name: "fake_user"}], me: %{id: :alice}} + } end + def fake_conn(message, capture: capture_regex) do message |> fake_conn() @@ -56,10 +61,11 @@ defmodule Alice.HandlersCase do """ def send_message(conn = %Alice.Conn{}) do case Alice.Conn.command?(conn) do - true -> Alice.Router.match_commands(conn) + true -> Alice.Router.match_commands(conn) false -> Alice.Router.match_routes(conn) end end + def send_message(message) do message |> fake_conn() @@ -78,11 +84,13 @@ defmodule Alice.HandlersCase do end """ def all_replies() do - message = receive do - {:send_message, %{response: message}} -> message - after - 0 -> :no_message_received - end + message = + receive do + {:send_message, %{response: message}} -> message + after + 0 -> :no_message_received + end + case message do :no_message_received -> [] message -> [message | all_replies()] @@ -93,7 +101,7 @@ defmodule Alice.HandlersCase do Retrieves the first reply that Alice sent out since the test began. ## Examples - + test "it only brings back the first message" do send_message("first") send_message("second") @@ -102,8 +110,8 @@ defmodule Alice.HandlersCase do """ def first_reply() do case all_replies() do - [first_message | _] -> first_message - _ -> nil + [first_message | _] -> first_message + _ -> nil end end @@ -126,9 +134,10 @@ defmodule Alice.HandlersCase do end defmacro __using__(opts \\ []) do - handlers = opts - |> Keyword.get(:handlers, []) - |> List.wrap() + handlers = + opts + |> Keyword.get(:handlers, []) + |> List.wrap() quote do use ExUnit.Case