The methods in TestApi relate to sending messages to the API.
Currently the TestApi supports:
createContext to create a Context. createProject to create a Project.
getContexts to return all the Contexts for the current user, this is returned as a List of Contexts as domain objects i.e. TracksContext.
getProjects to return a List of Projects as domain objects i.e. TracksProject. getProject to return a specific Project as TracksProject.
getProjectTasks to return a List of the TODOs for a specific Project as
domain objects i.e. TracksTodo.
getTodo to return a specific TODO from Tracks as a TracksTodo. amendProject to amend specific fields on a Project.
deleteProject to delete a specific Project.
getLastResponse to return the ‘last response’ received by the HTTP layer.
Also:
createUserAPI to create a user via the API, is listed but marked as @Deprecated because we have to use “App as API” instead.
TracksApiEndPoints
Explained
The TracksApiEndPoints class is a set of static data and methods so I don’t
instantiate it, I call the methods on the class directly e.g.
TracksApiEndPoints.todos gives me access to the URL for the TODOs API
endpoint and I can issue GET or POST requests to that end point: public static final String todos= "/todos.xml";
There are also methods (e.g. todo) which, given the id of a TODO in the system will
return a URL that contains the id in the correct place so that I could GET (to retrieve)
or PUT (to amend) the TODO item details. In the code the String.format is used to
generate the partial URL String:
public static final String todoId ="/todos/%s.xml";
public static String todo(String id) { return String.format(todoId, id); }
TracksApiEndPoints
Since the class is very small I list the full class code below: package api.version_2_3_0.tracks;
public class TracksApiEndPoints {
public static final String todos= "/todos.xml"; public static final String todoId ="/todos/%s.xml"; public static final String users="/users.xml";
public static final String projects="/projects.xml"; public static final String projectId="/projects/%s.xml"; public static final String contexts="/contexts.xml";
public static final String projectIdTodos="/projects/%s/todos.xml";
public static String project(String id) { return String.format(projectId, id); }
public static String projectsTodos(String id) { return String.format(projectIdTodos, id); }
public static String todo(String id) { return String.format(todoId, id); }
}
TracksApiEndPoints
Evolution
The TracksApiEndPoints class evolved via refactoring.
Originally I had code that used String Literals e.g.
@Test
public void aUserCanNotAccessIfNoBasicAuthHeaderUsingGivenWhenThen(){
given(). contentType("text/xml"). when(). get("http://192.168.17.129/todos.xml"). then(). statusCode(401); }
After creating a few tests I saw that I repeatedly used the URL String Literal: "http://192.168.17.129/todos.xml"
I initially refactored this into a Field constant in the test class, but then I found myself repeating it in multiple test classes.
At which point I refactored this into:
a TestEnv class to represent the URL portion i.e. http://192.168.17.129
That way, I had one place to configure the Test Environment URL. And one place where I defined the Tracks API End points, should these ever need to change. Once these classes existed, when I wrote test code that accessed new parts of the API, I added the new endpoints directly into TracksApiEndPoints since that is the
class which contains all the API Endpoint information.
TracksApiEndPoints
Coverage Review
Reviewing the TracksApiEndPoints gives us a pretty good idea of the coverage of
the API achieved.
Any endpoint that is not mentioned in the TracksApiEndPoints has no coverage, so
a reviewer can easily spot any high level gaps.
However, just because an endpoint is mentioned, doesn’t mean that it is used. IntelliJ ‘find usages’ can help us identify where the endpoint is called. Ideally, the only place they are used is in the TracksApi class, but this might not be true given the
‘example’ nature of the code.
Also, just because the endpoint is used in the TracksAPI doesn’t mean that we have
covered all the verbs possible for the endpoint.
Having the URLs obvious in a single class makes it easy to start a review of the API coverage.
URLifier
I’ll describe the URLifier here since it is a very simple class which is used by the TestEnv class which we will look at in more detail in the next section.
found in package api.version_2_3_0.tracks.http
The URLifier converts a String to a URL without forcing the client code to declare
that it throws a MalformedURLException. public class URLifier {
public static URL getURLfromString(String aString){ try {
return new URL(aString);
} catch (MalformedURLException e) { e.printStackTrace();
String.format(
"URL %s is not correctly formatted", aString));
} } }
I make the assumption that I know what I’m doing with this code and that I don’t dynamically amend the URL.
If an error does occur during conversion then a RuntimeException is thrown.
Since I know the use cases of the Abstraction layer I can make decisions like this to make it easier for the client code to use the library code.
TestEnv
The TestEnv class is very simple and is used to collate all the information for a
particular Tracks Test Environment. You can find it in:
package api.version_2_3_0.tracks
It is a wrapper for the URL of the environment, the Admin username, Admin password, and configuration of the proxy (if any) that we access the environment through.
public class TestEnv {
private final String theURL; private final String userName; private final String userPassword; private String proxyUrl;
private int proxyPort; private boolean useProxy;
The URL, username and password are configured via the constructor and cannot be changed once the object has been instantiated.
public TestEnv(String theURL, String userName, String userPassword){ this.theURL = theURL;
this.userName = userName;
this.userPassword = userPassword; this.useProxy = false;
The proxy, can be configured at runtime, and we can set whether we use it at runtime or not.
public void configureProxy(String url, int port){ this.proxyUrl = url;
this.proxyPort = port; }
public void setUseProxy(boolean useProxy) { this.useProxy = useProxy;
}
All of the other methods are accessors that return the data about the environment. The getURL method uses the URLifier to convert a String to a URL.
public URL getURL() {
return URLifier.getURLfromString(theURL); }
The userProxy returns true when we have setUseProxy to true and a URL for the
proxy has been configured.
public boolean useProxy(){
return (useProxy && proxyUrl!=null); }
The rest of the methods are simple accessors that return the data stored about the environment.
public String getUserName() { return userName;
}
public String getUserPassword() { return userPassword;
}
public String getProxyURL() { return proxyUrl;
}
public int getProxyPort() { return this.proxyPort; }
TracksApi
And now the moment you’ve all been waiting for. The code that communicates with the API.
And… sorry, but that comes later. First I’ll describe some thoughts about abstraction layers and how I implemented them.
I took the view that the API has:
A logical level - i.e. what it does.
A physical level - how the messages are sent and implemented.
The TracksApi class provides the logical level view of the Tracks API and its
functionality.
The physical level is a different abstraction model and is in the HttpMessageSender
class.
I partly take this view because I’m modelling both the usage (logical view) and the implementation (physical view) of the API calls. These are different domains, so I separate them in my code.
TracksApi is responsible for:
Building the content of the messages. Routing messages to the correct end point.
Selecting the correct HTTP verb to use e.g. GET, PUT etc.
Converting the XML data returned into Domain Objects i.e. the entity classes.
I do not explain all of the code for the TracksApi here, as you can look at it in the
source code:
src\main\java
package api.version_2_3_0.tracks class TracksApi
I will explain the common patterns so you can spot them easily when you read the source code.
TracksApi
State
The TracksApi doesn’t maintain much state. This is evidenced by the fields that the
class has.
private final URL url;
Both fields are final, so can’t be changed once set.
The ‘last response’ is stored, and accessible through the TracksApi using the method getLastResponse, but the state is actually maintained by the http abstraction layer.
Creating a
TracksApiA TracksApi can be created, either by passing in a TestEnv or a URL, username and
password.
The TestEnv constructor is the main one that the ‘test’ code uses because for most
tests a single user is used.
The URL, username and password is used in the @Test utility to create users and setup
the environment, because we create Projects and Tasks for multiple users.
public TracksApi(URL tracksUrl, String username, String password){ this.url = tracksUrl;
httpMessageSender = new HttpMessageSender(tracksUrl); httpMessageSender.basicAuth(username, password); }
public TracksApi(TestEnv testEnv) {
this( testEnv.getURL(), testEnv.getUserName(), testEnv.getUserPassword()); if(testEnv.useProxy()){
proxy(testEnv.getProxyURL(), testEnv.getProxyPort()); }
}
Code is shared between the two constructors. So the TracksApi constructor that
takes a TestEnv uses the more explicit constructor, but since it also knows about
proxies, it can configure the proxy if appropriate.
The constructor stores the url and configures the HttpMessageSender. Since Basic
Auth is used for the API, the Basic Auth settings are configured during construction of the message sender.