testingelixirphoenix-frameworkhttpoison

Testing Elixir/Phoenix Service modules


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.


Solution

  • 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.)