skip to Main Content

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

Answers


  1. 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.

    Login or Signup to reply.
  2. 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.)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search