Testing Elixir/Phoenix Service modules

290 views Asked by At

I've been playing around with Elixir/Phoenix third-party modules. ( Modules that are used to fetch some data from a 3rd party service ) One of those module looking like so:

module TwitterService do
  @twitter_url "https://api.twitter.com/1.1"

  def fetch_tweets(user) do
     # The actual code to fetch tweets
     HTTPoison.get(@twitter_url)
     |> process_response
  end      

  def process_response({:ok, resp}) do
    {:ok, Poison.decode! resp}
  end

  def process_response(_fail), do: {:ok, []}
end

The actual data doesn't matter in my question. So now, I'm interested in how can I dynamically configure the @twitter_url module variable in tests to make some of the tests fail on purpose. For example:

module TwitterServiceTest
  test "Module returns {:ok, []} when Twitter API isn't available"
    # I'd like this to be possible ( coming from the world of Rails )
    TwitterService.configure(:twitter_url, "new_value") # This line isn't possible
    # Now the TwiterService shouldn't get anything from the url
    tweets = TwitterService.fetch_tweets("test")
    assert {:ok, []} = tweets
  end
end

How can I achieve this? Note: I know I can use :configs to configure @twiter_url separately in dev and test environments, but I'd like to be able to test on a real response from the Twitter API too, and that would change the URL on the entire Test environment.
One of the solutions that I came up with was

def fetch_tweets(user, opts \\ []) do
  _fetch_tweets(user, opts[:fail_on_test] || false)
end

defp _fetch_tweets(user, [fail_on_test: true]) do
  # Fails
end

defp _fetch_tweets(user, [fail_on_test: false]) do
  # Normal fetching
end

But that just seems hackish and silly, there must be a better solution to this.

2

There are 2 answers

0
Aleksei Matiushkin On BEST ANSWER

As it was suggested by José in Mocks And Explicit Contracts, the best way would be probably to use a dependency injection:

module TwitterService do
  @twitter_url "https://api.twitter.com/1.1"

  def fetch_tweets(user, service_url \\ @twitter_url) do
     # The actual code to fetch tweets
     service_url
     |> HTTPoison.get()
     |> process_response
  end      

  ...
end

Now in tests you just inject another dependency when necessary:

# to test against real service
fetch_tweets(user)

# to test against mocked service
fetch_tweets(user, SOME_MOCK_URL)

This approach will also make it easier to plug in different service in the future. The processor implementation should not depend on it’s underlying service, assuming the service follows some contract (responds with json given a url in such a particular case.)

3
Dogbert On

config sounds like a good way here. You can modify the value in the config at runtime in your test and then restore it after the test.

First, in your actual code, instead of @twitter_url, use Application.get_env(:my_app, :twitter_url).

Then, in your tests, you can use a wrapper function like this:

def with_twitter_url(new_twitter_url, func) do
  old_twitter_url = Application.get_env(:my_app, :twitter_url)
  Application.set_env(:my_app, :twitter_url, new_twitter_url)
  func.()
  Application.set_env(:my_app, :twitter_url, old_twitter_url)
end

Now in your tests, do:

with_twitter_url "<new url>", fn ->
  # All calls to your module here will use the new url.
end

Make sure you're not using async tests for this as this technique modifies global environment.