Selenified was released with the idea to make browser testing easier. Selenified = Selenium Simplified. That said, not all testing is done within the browser. Not only can it not all be done there, but even if it could, it shouldn’t. For this reason, Selenified also allows for simple testing of other interfaces.

Web Services are a relatively simple and easy interface to test against, and they are important. This interface allows for quick data-driven testing, at a layer below the UI. It also allows you to test beyond the enforced UI validation, security rules, and workflows, and can identify errors front-end testing can’t. Unfortunately, this interface is often overlooked, and the hope of including some simple capabilities to test web services within Selenified would make users more likely to actually test them.

Setting Up Your Test Suite

As these tests do not require a browser, the first thing to do is to instruct the test suite that no browser is required. To do this, the suite should contain a method to override the default startTest method. This needs to be annotated with @BeforeMethod. This method will just call the super method of startTest and to handle your test setup. Since we do not need a Selenium driver, the last value passed into startTest should be set to ‘FALSE’. See below for an example:

    @BeforeMethod (alwaysRun = true)
    protected void startTest(Object[] dataProvider, Method method, ITestContext test, ITestResult result) {
        super.startTest(dataProvider, method, test, result, DriverSetup.FALSE);
    }

Writing Your Test Cases

Like the browser-based Selenified tests, the Web Services capabilities are all object oriented. There is now one call object, where all service calls can be made. The first thing you should do is retrieve your call class, which the framework has already instantiated for you.

    Call call = this.calls.get();

This object will give you access to make any required method calls on your web service, such as a GET or POST.

    call.get("post/");
    call.post("post/", request);

These calls return a response, which contains its own custom assertions. Similar to the browser-based testing these asserts provide additional traceability and debugging assistance. Both the response and the HTTP status code can be verified.

    call.get("post/").assertEquals(404);
    call.post("posts/", request).assertContains(response);

Sometimes, some of this data needs to be stored from one call to the next. If this is the case, just split up any of the above calls into two commands.

    Response response = call.get("post/");
    // do something else with response
    response.assertEquals(404);

Similar to our browser based Selenified tests, and the prior releases of Selenified, errors are internally monitored – if you try to click on an element, and it doesn’t exist, the test won’t stop, but an error will be recorded. In order verify that each test passes without any recorded errors, the last step of each test compares the value within errors to the number 0. This action will throw an error if any issues occurred during the test. This allows prior errors to be handled gracefully, but still causes tests to ultimately fail, if errors are encountered. This last line should read as follow:

    finish();

Authentication

Any of the above calls would occur without any authentication; they just are direct GETs, POSTs, etc. Some basic authentication capabilities are built into Selenified. If you have simple user/password authentication for your services, Selenified makes it easy to provide those. Simply set the username and password as environment variables, and Selenified will automatically pick them up, and pass them along with your call. Don’t worry, they’re not passed in clear text, but are encoded, and passed as header authorization information.

set SERVICES_USER=myusername
set SERVICES_PASS=mypassword

You may have some more complex authentication scheme. That is not atypical. Unfortunately, in order to set this up, you’ll actually need to modify the source code a bit. Because authentication is performed in so many different ways, we don’t have a standard setup for OAuth, CSRF tokens, or others. Below is an example of what you might do to handle having a CSRF token that is obtained from an initial login.

The only thing I modified was the HTTP class to add some additional functionality. The required pieces of this authentication scheme are the session, XSRF token, and CSRF tokens. For that, the code now records and stores these variables.

public class HTTP {
    ...
    private String session = null; // the token set in the cookie for headers
    private String xsrf = null; // the token set in the cookie for headers
    private String csrf = null; // the session token
    ...

In the constructor an initial call to login is made.

    public HTTP(String serviceBaseUrl, String user, String pass) {
        this.serviceBaseUrl = serviceBaseUrl;
        this.user = user;
        this.pass = pass;

        JsonObject json = new JsonObject();
        json.addProperty("user", user);
        json.addProperty("pass", pass);
        Request request = new Request(json);
        post('/auth/login/', request);
    }

Then we save those three bits of information after each call is made.

    private String getToken(String fullString) {
        String token = fullString.split(" ")[0];
        token = token.split("=")[1];
        return token.split(";")[0];
    }

    private Response getResponse(HttpURLConnection connection) {    //modify our existing method to gather more data
        ...
        // while reading out the reported output
        rd = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
        while ((line = rd.readLine()) != null) {
            if (line.contains("<meta name=\"csrf-token\"")) {
                csrf = line.split("\"")[3];
            }
        }
        ...
        Map<String, List<String>> map = connection.getHeaderFields();
        if (map.containsKey("Set-Cookie")) {
            if (map.get("Set-Cookie").size() < 0) {
                session = getToken(map.get("Set-Cookie").get(0));
            }
            if (map.get("Set-Cookie").size() < 1) {
                xsrf = getToken(map.get("Set-Cookie").get(1));
            }
        }
        ...
    }

Finally, that information is added back into each call as they are made.

    private Response call(String call, String service, Request request) {
        ...
        connection.setRequestProperty("Cookie", "XSRF-TOKEN=" + xsrf + "; session=" + session + ";");
        connection.setRequestProperty("X-CSRF-Token", csrf);
        ...
    }

And that is it. Of course, your mileage may vary with this particular code, as this is one implementation your site might have for authentication. Drop me a line if you have issues getting this working, as there is a good chance someone else has run into these same issues.

Wrap Up

As always, using an IDE such as Eclipse will help you auto-complete desired commands, and the JavaDocs provided will outline each piece of functionality. If using Maven Central, the JavaDocs and source code will automatically get attached in your IDE, which will further assist with development.

Of course, if you find an issue, be sure to let us know. We’re actively working on development, and are willing and able to fix it. You can also fork the repository, make some fixes or changes yourself, and submit a pull request. We are happy to have an active and interested community!

One thought to “Selenified and Web Services: Moving Beyond the Browser”

Leave a comment

Your email address will not be published. Required fields are marked *

X