Strategies for testing static calls in Play projects

Play forum is a place with a lot of healthy doubts. Other day, an interesting topic popped there: How to test static methods. So, let’s talk about a little bit about this subject.

For example, if you have this piece of code:

    public void save(final RecentPhotosURL photosURL,
    		final Function2<String, Integer, Void> callback) {
    	WSRequestHolder holder = WS.url(photosURL.getEndpoint());
    	photosURL.fill(holder);
    	Promise promise = holder.get();
    	promise.map(savePhotosFunction);
    }

How can you test it? You are using WS class and making a static call. Libs like Mockito, only can test instance methods. One way to workaround things here is the usage of a lib which instruments your code and is able to mock some static calls :). This powerful tool is PowerMock. So, to test your method you could use something like this.

    @RunWith(PowerMockRunner.class)
    @PrepareForTest({ WS.class })
    public class NewPhotosSaverTest {

    	@Test
    	public void shouldGetJsonResponseAndSaveIt() {
    		mockStatic(WS.class);
    		Mockito.when(WS.url(url).thenReturn(new MyRequestHolder(url));
    		new NewPhotosSaver(...).save(photosURL,callback)
    	}

    	static class MyRequestHolder extends WSRequestHolder{

    		public MyRequestHolder(String url) {
    			super(url);
    		}

    		public  Promise get(){...}

    	}

    }

Voilà, you can unit test a lot of methods that use static approach. This could be used to test legacy system too :). I have no doubts that use static methods are easier than use a lot of instance methods. The main reason is that you can invoke these methods from everywhere. But oops, this can be your big problem too :(.

Now, if someone has to add some logic in this method, there is a big chance that a new class with some static methods be created. The complexity of this method would be increased because it would be responsible for creating and using objects instead of just use them and, even worst, static calls can be made from every piece of your code. I am not sure if you have seen something like this, but imagine a code inside a template:

    for(photo       code here
    }

So you have to know really well your team before start using static methods everywhere. Another strategy can be used to prevent maintainability problems like this. Instead of using Play API’s directly, you could wrap some of them and force your code to instantiate some objects.

    public class ServiceRequester {
      private String endpoint;

      public ServiceRequester(String enpoint){
        ...
      }

      public Promise get(Map<String,String> params){
        WSRequestHolder holder = WS.url(endpoint);
    		Set<Entry<String, String>> entries = params.entrySet();
    		for (Entry<String, String> entry : entries) {
    			holder.setQueryParameter(entry.getKey(),entry.getValue());
    		}
    		return holder.get();

      }
    }

    //using this code

    public class NewPhotosSaver {
        public void save(final RecentPhotosURL photosURL,
        		final Function2<String, Integer, Void> callback) {
        	ServiceRequester requester = new ServiceRequester(photosURL.getEndpoint());
        	Promise promise = requester.get(photosURL.getParams());
        	promise.map(savePhotosFunction);
        }
    }

Now you can refactor a little more and receive these dependencies as constructor args.

    public class NewPhotosSaver {

        public NewPhotosSaver(ServiceRequester requester){
          ...
        }

        public void save(final RecentPhotosURL photosURL,
        		final Function2<String, Integer, Void> callback) {
        	Promise promise = requester.get(photosURL.getParams());
        	promise.map(savePhotosFunction);
        }
    }

It is easy to test now! Just mock this parameter and test your code without problems.

    @Test
    public void shouldGetJsonResponseAndSaveIt() {
      ServiceRequester requester = mock(ServiceRequester.class);
      when(requester.get(params)).thenReturn(somePromiseHere)
      new NewPhotosSaver(requester).save(photosURL,callback);
      //asserts here
    }

Now you have, at least, two options of approach to your code. You can use this same strategy with your DAO. Instead of create static methods that use JPA.em() or Ebean.something, create instance methods and pass DAO’s instances as arguments to other classes.

Of course you have to think about what kind of approach you prefer, it is possible to mix them too. Your tests send signals about your code, pay attention :). Thanks to Rafael Ponte who talked about this situation with me and inspired me to write this post :).

Advertisements

4 thoughts on “Strategies for testing static calls in Play projects

  1. Good post, Alberto.

    I sincerely don’t like to use frameworks like PowerMock because those kind of tools make you think less about the design of the classes. Of course they can help you when you’re working on legacy code where you can’t or you are afraid of changing any class.

    Another option, it’s to write integration or acceptance tests, but as you probably already know, they also have their own issues.

  2. It’s a possibility, we discussed this too. I remember :). I think this a good way when you are trying to test controllers, daos, etc. And I use this part of Play, heavily :).

  3. Good post and good advice, Alberto. I find that wrapping static calls inside injectable dependencies is indeed a sound trick but, unfortunately, one that is also still underused.

  4. Alberto, what are the dependencies I should add to play in order to use PowerMock?
    I’ve been using PowerMock and Mockito in my projects for some years, but now I started working with Play and I can’t make Powermock and Play work together.
    Thanks!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s