📜 ⬆️ ⬇️

TDD on the example of UrlBuilder

TDD on the example of UrlBuilder


In this article, I would like to tell you about the use of development techniques through testing in a simple but vital example. The main advantage of this approach, which I would like to draw attention to - at the time of the start of development, we already know what we need to develop, and we have a very specific criterion for the efficiency of the code.

We will develop a builder that creates a url for the specified parameters - protocol, host, etc. All matches with real web services please consider the fruit of a sick fantasy.

Problem


Not so long ago, I finished developing a fairly large application for Android. I will not go into details, since the article is not about that, I will only say that the application is a fat client. The web service accessed by the application has many parameters that are transmitted using the GET method. For example, to search the entire database:

http://server.com?action=search&query=%20

To search all database filtering by document type:
')
http://server.com?action=search&query=%20&document_type_id=N

To search for a specific document:

http://server.com?action=search&query=%20&document_id=M

And so on. Just a few dozen classes of possible queries. Plus, each request can be both to the production and to the dev server.

There was a question how to create a url to access this web service. To keep a mask for each specific request, as the iPhone programmer did, solving the same task is clearly not an option. If you add a single parameter, you would have to edit a few dozen lines. And in general, if you do not do it yourself right away on your own will, then you will be forced to make properly disgruntled users.

Uri.Builder does not fit, if only because there is no way to set the host port.

So, I decided to write a class to build a url.

Analysis


First, we define what we want from our class? We want him to construct a url by which we would be able to access our web services and return them as java.lang.String. I would like to be able to build a url from scratch, sequentially specifying the protocol, host, path and parameters, and on the basis of some preset (for example, url to search the entire database, for a specific document, etc.).

Development: class interfaces and unit tests


Of these requirements, you can select at least 2 classes. The first will build an arbitrary url, the second will be the url of some predefined classes. This is quite enough to describe the interfaces of our classes. Obviously, we are using the “builder” and “fluent interface” patterns.

 public class UrlBuilder { public UrlBuilder protocol(String protocol){ return this; } public UrlBuilder host(String host){ return this; } public UrlBuilder port(String port){ return this; } public UrlBuilder path(String path){ return this; } public UrlBuilder param(String key, String value){ return this; } public String build() { return null; } } 


 public class ServerUrlBuilder extends UrlBuilder{ public UrlBuilder fullSearch(String query){ return this; } public UrlBuilder fullSearch(String query, int documentTypeId){ return this; } public UrlBuilder documentSearch(String query, long documentId){ return this; } } 


And immediately write unit tests. First for UrlBuilder ...

 public class UrlBuilderTest extends TestCase { public void testRootUrl(){ UrlBuilder builder = new UrlBuilder(); String expected = "http://server.com/"; String actual = builder1.protocol(UrlBuilder.HTTP).host("server.com").build(); assertEquals(expected, actual); } public void testRootUrlWithOneParam(){ UrlBuilder builder = new UrlBuilder(); String expected = "http://server.com/?a=b"; String actual = builder.protocol(UrlBuilder.HTTP).host("server.com").param("a","b").build(); assertEquals(expected, actual); } public void testFullUrl(){ UrlBuilder builder = new UrlBuilder(); String expected = "http://server.com/folder/?a=b"; String actual = builder.protocol(UrlBuilder.HTTP).host("server.com").path("folder").param("a","b").build(); assertEquals(expected, actual); } } 


... and then for ServerUrlBuilder.

 public class SereverUrlBuilderTest extends TestCase { public void testFullSearch(){ ServerUrlBuilder builder = new ServerUrlBuilder(); String expected = "http://server.com?action=search&query= "; String actual = builder.fullSearch(" "); assertEquals(expected, actual); } public void testFullSearchWithDocumentTupeId(){ ServerUrlBuilder builder = new ServerUrlBuilder(); String expected = "http://server.com?action=search&query= &document_type_id=123"; String actual = builder.fullSearch(" ",123); assertEquals(expected, actual); } public void testDocumentSearch(){ ServerUrlBuilder builder = new ServerUrlBuilder(); String expected = "http://server.com?action=search&query= &document_id=123456"; String actual = builder.documentSearch(" ",123456); assertEquals(expected, actual); } } 


For full coverage, it would be good to write about a dozen more tests for different combinations, as well as to check negative scenarios when one of the parameters is not entered or an incorrect value is entered. But dear readers can not forgive such code sheets.

Run our tests. Of course, none of them will be passed. But now we have a clear criterion for the efficiency of the code. It remains to add a couple of lines.

Implementation


Add constants to the UrlBuilder class - several protocols and delimiters used in the url. Let's program the logic of each method.

 public class UrlBuilder { public static final String HTTP = "http"; public static final String HTTPS = "https"; public static final String FTP = "ftp"; private static final String sProtocolHostSeparator = "://"; private static final String sPathSeparator = "/"; private static final String sParamSeparator = "?"; private static final String sParamEquals = "="; private static final String sParamConcotinator = "&"; private String mProtocol; private String mHost; private final ArrayList<String> mPath = new ArrayList<String>(); private final ArrayList<String> mParamKeys = new ArrayList<String>(); private final ArrayList<String> mParamValues = new ArrayList<String>(); public UrlBuilder(){} public UrlBuilder protocol(String protocol){ mProtocol = protocol; return this; } public UrlBuilder host(String host){ mHost = host; return this; } public UrlBuilder path(String path){ mPath.add(path); return this; } public UrlBuilder param(String key, String value){ mParamKeys.add(key); mParamValues.add(value); return this; } public String build() { final StringBuilder builder = new StringBuilder(); builder.append(mProtocol); builder.append(sProtocolHostSeparator); builder.append(mHost); builder.append(sPathSeparator); for(String path : mPath){ builder.append(path); builder.append(sPathSeparator); } if(mParamKeys.size()>0){ builder.append(sParamSeparator); for(int i=0; i<mParamKeys.size(); i++){ String key = mParamKeys.get(i); if(i!=0){ builder.append(sParamConcotinator); } String value = mParamValues.get(i); builder.append(key); builder.append(sParamEquals); builder.append(value); } } String result = builder.toString(); return result; } } 


Run the test again. Hurray, some of them are successful. It remains to implement the ServerUrlBuilder logic.

 public class ServerUrlBuilder extends UrlBuilder{ public static final String SERVER_HOST = "server.com"; public static final String PARAM_ACTION = "action"; public static final String ACTION_SEARCH = "search"; public static final String PARAM_QUERY = "query"; public static final String PARAM_DOCUMENT_TYPE_ID = "document_type_id"; public static final String PARAM_DOCUMENT_ID = "document_id"; public UrlBuilder fullSearch(String query){ protocol(HTTP); host(SERVER_HOST); param(PARAM_ACTION, ACTION_SEARCH); param(PARAM_QUERY, query); return this; } public UrlBuilder fullSearch(String query, int documentTypeId){ protocol(HTTP); host(SERVER_HOST); param(PARAM_ACTION, ACTION_SEARCH); param(PARAM_QUERY, query); param(PARAM_DOCUMENT_TYPE_ID, documentTypeId); return this; } public UrlBuilder documentSearch(String query, long documentId){ protocol(HTTP); host(SERVER_HOST); param(PARAM_ACTION, ACTION_SEARCH); param(PARAM_QUERY, query); param(PARAM_DOCUMENT_ID, documentId); return this; } } 


Run the tests again - they all pass. This suggests that we implemented exactly what was required. Of course, to the extent that the tests were complete and accurate.

Conclusion


In this article, we implemented the UrlBuilder class and its ServerUrlBuilder derived from it using the TDD methodology. All development stages were shown: problem detection, analysis, test writing, and finally implementation. Of course, much more needs to be improved. For example, you can add the transfer of arrays as parameters, UrlEncode for special characters. It would be great to build not only a String, but also a URI and Uri. But this is a separate conversation, beyond the scope of this article.

Source: https://habr.com/ru/post/130574/


All Articles