Working with Public APIs (and Reading the Documentation) for setting up Automation Tests

General Testing Strategies and Goals

As Scott Barstow summarizes succinctly in his explanation of APIs, the following characteristics should be kept in mind:
  • An API a series of messages that an application or platform agrees to exchange with other applications
  • The message exchanged between applications is governed by a protocol, which is a fancy word for a kind of "contract" applications agree to use for communication
  • Modern applications typically use the JSON format to relay those messages (though some also support XML)

The take away from these characteristics already gives a solid foundations for developers and testers to be able to test an API that is made publicly available. For many companies, such as Facebook, Twitter, Instagram, Google, etc. that have a diverse userbase, it is vital that the API has a robust documentation implemented. For beginners in API testing, perhaps the biggest hurdle starting out is being able to develop the proficiency to read the developer's API documentation.

At some point in a client-related project it will be necessary to navigate the extensive documentation outlined by the developers. With time and practice it will become more natural to be able to read and understand how make use of the APIU documentation, and by the end of this tutorial hopefully the following strategies will be highlighted:
  • Developing the mindset to read and understand the various endpoints as explained by the API documentation for an application
  • Verify the functionality of the API calls on a fundamental level for the application (we will explore the use of the command line as well as Postman!)
  • Translate the API calls into a programming language of choice that will be able to fit into an Automation Framework (for this tutorial Java will be used to convey the functionality needed)

Analyzing the API documentation:

A well documented API will be user and developer friendly in terms of navigating, as well as understanding, method calls that can be invoked within the API. To illustrate the effectiveness of solid API documentation, the Github API will be used to illustrate how a developer would take an approach to the problem presented.

Github API <a class="foswikiNewLink" href="/bin/edit/Automation/HomePage?topicparent=Automation.WorkingWithPublicAPIsForAutomationTests" rel="nofollow" title="Create this topic">HomePage</a>.png

While the Github documentation may have a layout different from other publicly made available APIs, the API documentation generally outlines a plethora of example guides and will cover specific areas within the API (which as a general rule of thumb, it is always good to check how Authentication is handled on the backend). Before getting too overwhelmed with what is presented here, one of the first places to check is either a reference link, or a page that lists all the end points for the API that applications may use. Either approach will give a general idea of all the possible actions that an application may call in order to work with the Application API, so a general understanding of the application will further aid in writing tests.

If we turn our attention to the API documentation page "Getting started with REST API," the documentation some great examples of some basic end points with expected outcomes. Assuming that cURL is installed on a local machine, an easy command to check with the Github API is shown for someone to dabble with and test:

> curl https://api.github.com/zen

It should return a simple message once the GET request is made, and thankfully did not take too long to test the waters with the API documentation.

Troubleshooting with Postman

Working with a robust API means more often than not requires a method of authentication when working with user accounts. This can be a little tricky for new beginners, and even the documentation expands on several different cURL commands which can allow a user to make an authenticated call. While trial and error will certainly work after a few hours of trying to get the commands right, we might consider using an alternative to pure cURL and is a little bit more user friendly: Postman!

While Postman advertised as an collaboration platform for API development, it offers an excellent user interface to set up and make API calls that maybe tricky to get right the first time. Let's use it to get more familiar with how to make a basic authorization call. The Github API describes in detail that a username and an API key associated with that account is needed. The instructions are straightforward when it comes to getting the API key required, but formatting the cURL command can be somewhat open ended as the documentation covers several different authentication calls.

Fight now, we should consider the structure which needs to be achieved for a simple call to fetch a user account:

  • The call should be a GET request, since we just want to fetch information
  • A username and API Key are required at the minimum
  • The call should be written consistently and easily for repeated use (vital to reproduce the same type of calls later)

Postman can make this all straightforward with the interface. A collection for testing Github's API can be made, and can save a request which holds the information needed to pass through:

PostmanDemo.gif

Postman also offers a way to convert any of the requests to several different code snippets depending on the specified language, which can be invaluable when testing several requests with Postman and need to convert them to a language specific format. Consider using such tools when going over the API documentation and getting a strong grasp of the commands covered. A bit of experimentation along with trial and error will pave the way towards mastering the documented API for an application.

Putting it all together for Automation Tests

Navigating the API documentation, exploring the methods, and understanding what information is exchanged between applications will pave the way in writing up the required software to test the API in a project framework. For this tutorial, a Maven built Java project will be used to structure and execute out tests.

The following dependencies will be set up in the pom.xml file:
  • Java Rest Assured (io.rest-assured)
  • Dot Env Java (to hide credentials needed to execute the API testing. Any .env should never be stored in a remote repository if a SCM is set up)
  • Test NG (for a testing framework)
  • Selenium Java + Webdriver (to capture screenshots of the webpage after API calls are made)

For this project a simple class to outline the Github user account will be used for testing:

/**
 * Models a basic Github account which at the minimum requires
 * a {@link #userName} and {@link #userAPIKey}
 */
public class GitHub
{
    /**
    * A username for a {@link com.ultranauts.github.GitHub GitHub } account
    */
    protected String userName;

    /**
    * An apikey that is authenticated for a {@link com.ultranauts.github.GitHub GitHub } account.
    * This should be matched for the appripriate username, and configured
    * for approriate permissions when testing Github's API
    */
    protected String userAPIKey;

    /**
     * Constructor will pull user name and api key from
     * the .env file located in the project directory and assign them to
     * {@link com.ultranauts.github.GitHub GitHub } instance's {@link #userName} and {@link #userAPIKey}
     * @param dotenv to get username and api key from the .env file
     */
    public GitHub (Dotenv dotenv){
        userName = dotenv.get("PERSONAL_USER_NAME");
        userAPIKey = dotenv.get("PERSONAL_API_KEY");
    }

    /**
     * Constructor that can allow a tester to enter custom
     * username and password if .env becomes unavailable for
     * the {@link com.ultranauts.github.GitHub GitHub } Object
     * @param name to set {@link #userName}
     * @param key to set {@link #userAPIKey}
     */
    public GitHub (String name, String key){
        userName = name;
        userAPIKey = key;
    }

    /**
     * Returns the username asociated with the {@link com.ultranauts.github.GitHub GitHub } object
     * @return the {@link #userName} associated with the {@link #GitHub} Object
     */
    public String getUserName() {
        return this.userName;
    }

    /**
     * Sets a new username for the {@link com.ultranauts.github.GitHub GitHub } object
     * @param name to set {@link #userName}
     */
    public void setUserName(String name) {
        this.userName = name;
    }

    /**
     * Returns the apikey asociated with the {@link com.ultranauts.github.GitHub GitHub }
     * object (ideally paired with the proper username)
     * @return the {@link #userAPIKey} associated with the {@link com.ultranauts.github.GitHub GitHub } Object
     */
    public String getUserAPIKey() {
        return this.userAPIKey;
    }  

    /**
     * Sets a apikey string for the {@link com.ultranauts.github.GitHub GitHub } object
     * (ideaiily paired with the proper username)
     * @param key to set {@link #userAPIKey}
     */
    public void setUserAPIKey(String key) {
        this.userAPIKey = key;
    }
}

One of the constructors that generates the GitHub object takes a Dotenv object, which represents all the contents that are stored in the .env file located in a project directory. Since we are testing with APIs and require API keys, it is a crucial reminder to hide sensitive data from the actual source code written.

The actual test class has a clear outline of each of the API calls that are being used for the Github account. While a personal account has been elected to test the functionality, a brief summary of the API tests are as follows for an authenticated user:

  • GET the user account
  • POST a new repository for the user
  • PUT topics within the repository
  • PATCH a description into the repository
  • DELETE the repository
  • Anytime an API call is made that changes the user account, take a screenshot to verify the visual change

A combination of visual testing and API testing is used to confirm the overall functionality as written in the API documentation.

/**
 * Written class to demonstrate accessing a publically written API.
 * A personal account was used to interact with Github's API, and can
 * be reconfigured to use another personal via the .env file.  
 * @author Austin Bell
 * @version 1.0
 */
public class GitHubTest {

    /**
    * An instance of the {@link com.ultranauts.github.GitHub GitHub } class to
    * model a personal user account on GitHub. An apikey must be generated for
    * the user account in order to use the test class GitHubTest
    */
    private GitHub personalGithub;

    /**
    * The {@link io.github.cdimascio.dotenv.Dotenv Dotenv} class is used to store
    * all the information of the personal Github account. The {@link com.ultranauts.github.GitHub#GitHub(Dotenv dotenv) GitHub (Dotenv dotenv)}
    * constructor for {@link com.ultranauts.github.GitHub GitHub } will be configured
    * with the {@link io.github.cdimascio.dotenv.Dotenv Dotenv} class
    */
    private Dotenv dotenv;

    /**
    * The root URL which all the api calls make as documented in the API documentation
    */
    private String apiURL;

    /**
    * A Selenium based webdriver used to view the webpage after API calls are made when
    * performing actions that change the state of the user account
    */
    private WebDriver driver;

    /**
    * The webpage which will be used to grab a screenshot and confirm the API call was
    * successfully able to change the front end experience
    */
    private String repositoryURL;

    /**
     *
     */
    private String repositoryName;

    /**
    * A relative directory path that saves screenshots of the webpage after API calls
    * are made. This can be configured as needed
    */
    private String screenshotFolder;

    /**
    * Firefox Options set up for the driver to know which browser to use. This can
    * be changed to the appropriate browser if Firefox is not available.
    */
    private FirefoxOptions options;

    /**
     * A method intentially designed to delay 15 seconds so after an API call is made,
     * the methods executed later on can grab the appropriate state of the webpage
     * and get the correct screenshot.
     *

     * The test class navigated to webpages too quickly after the API call was made,
     * which resulted in incorrect screen grabs.
     * @throws InterruptedException
     */
    private void delay() throws InterruptedException {
        Thread.sleep(15000);
    }  

    /**
     * Generates an Header used in the API call formated "Authorization: token APIKEY", where the
     * api key of the Github account is the token value
     * @return A {@link io.restassured.http.Header Header} that authenticates the API call being made in the tests
     */
    private Header authorizedHeader(){
        String apiKey = personalGithub.getUserAPIKey();

        Header authHeader = new Header("Authorization", "token " + apiKey);

        return authHeader;
    }

    /**
     * Casts driver into an instance of {#link org.openqa.selenium.TakesScreenshot TakesScreenshot } amd
     * stores it in the designated directory for screenshots captured in the test class
     * @param driver that can capture a screenshot
     * @param fileName for the screenshot
     * @throws IOException
     */
    private void saveWebSnapShot(WebDriver driver, String fileName) throws IOException{

        TakesScreenshot scrShot =((TakesScreenshot)driver);

        File SrcFile =scrShot.getScreenshotAs(OutputType.FILE);

        File DestFile =new File(screenshotFolder + "/" + fileName);  

        FileUtils.copyFile(SrcFile, DestFile);
    }

    /**
     * Initializes all the basic member fields of the test class
     *

     * dotenv - loads the .env file
     *

     * personalGithub - loads username and api key from dotenv
     *

     * apiurl - definied in Github's API documentation "https://api.github.com"
     *

     * screenshotFolder - under root directory of project, "resources/screenshots"
     *

     * repositoryURL - [Github URL]/{User Name}?tab=repositories
     *

     * repositoryName - Can be modified as needed for testing purposes
     * @throws IOException
     */
    @BeforeTest
    public void init() throws IOException{

        dotenv = Dotenv.configure().load();
        personalGithub = new GitHub (dotenv);
        apiURL = "https://api.github.com";
        screenshotFolder = "resources/screenshots";
        repositoryURL = "https://github.com/" + personalGithub.getUserName() + "?tab=repositories";
        repositoryName = "APIGeneratedRepoPublic";
    }

    /**
     * Initializes the basic configurations. The test methods that
     * initialize the WebDriver will actually open the web browser
     * and not with this function call
    */
    @BeforeMethod
    public void initBrowser(){

        WebDriverManager.firefoxdriver().setup();

        options = new FirefoxOptions ();
        options.setPageLoadStrategy(PageLoadStrategy.EAGER);
    }

    /**
     * Test mothod to confirm that the api key is correctly accessed
     * from the .env file (mostly to verify how Dotenv worked)
     */
    @Test (priority = 0)
    public void confirmPERSONALAPIKEY(){

        String key = dotenv.get("PERSONAL_API_KEY");

        System.out.println(key);

        Assert.assertEquals(personalGithub.getUserAPIKey(), key);   
    }

    /**
     * Test mothod to confirm that the username is correctly accessed
     * from the .env file (mostly to verify how Dotenv worked)
     */
    @Test (priority = 0)
    public void confirmPERSONALUSERNAME(){

        String name = dotenv.get("PERSONAL_USER_NAME");

        System.out.println(name);

        Assert.assertEquals(personalGithub.getUserName(), name);   
    }

    /**
     * Authorized API call made to verify that the Github account's data is returned.
     *

     * A basic example of GET that should generate status code 200
    */
    @Test (priority = 0)
    public void getPersonalGithubAccount(){

        Header authHeader = authorizedHeader();

        given().
            header(authHeader).
        when().
            get(apiURL + "/user").
        then().
            statusCode(200).
            log().all(true);
    }

    /**
     * Authorized API call made to verify that a user is able to make a repository
     *

     * A basic example of POST that should generate status code 201
     * @throws IOException
     * @throws InterruptedException
     */
    @Test (priority = 1)
    public void createRepository() throws IOException, InterruptedException {
        driver = new FirefoxDriver (options);

        Header authHeader = authorizedHeader();

        Map&lt;String,Object&gt; bodyParameters = new HashMap &lt;String,Object&gt;();

        bodyParameters.put("name", "APIGeneratedRepoPublic");

        JSONObject newRepo = new JSONObject(bodyParameters);

        given().
            header(authHeader).
            body(newRepo.toJSONString()).
        when().
            post(apiURL + "/user/repos").
        then().
            statusCode(201).
            log().all(true);

        delay();

        //Maximize Window
        driver.manage().window().maximize();
        driver.manage().timeouts().implicitlyWait(20, TimeUnit.SECONDS);

        driver.get(repositoryURL);

        saveWebSnapShot(driver,"SCRN 001 - Created Repository.png");

        driver.quit();
    }

    /**
     * Authorized API call made to verify that a user's repository
     * can have the topic fields updated
     *

     * A basic example of PUT that should generate status code 200
     * @throws IOException
     * @throws InterruptedException
     */
    @Test (priority = 2)
    public void replaceRepositoryTopics() throws IOException, InterruptedException {
        driver = new FirefoxDriver (options);

        Header authHeader = authorizedHeader();

        Header mediaType = new Header("Accept", "application/vnd.github.mercy-preview+json");

        Map&lt;String,Object&gt; bodyParameters = new HashMap &lt;String,Object&gt;();

        bodyParameters.put("names", Arrays.asList("sample","api","java","automation"));

        JSONObject newRepo = new JSONObject(bodyParameters);

        given().
            header(authHeader).
            header(mediaType).
            body(newRepo.toJSONString()).
        when().
            put(apiURL + "/repos/" + personalGithub.getUserName() + "/" + repositoryName + "/topics").
        then().
            statusCode(200).
            log().all(true);

        delay();

        //Maximize Window
        driver.manage().window().maximize();
        driver.manage().timeouts().implicitlyWait(20, TimeUnit.SECONDS);

        driver.get(repositoryURL);

        saveWebSnapShot(driver,"SCRN 002 - Repository topics updated.png");

        driver.quit();
    }

    /**
     * Authorized API call made to verify that a user's repository
     * can have the description modified
     *

     * A basic example of PATCH that should generate status code 200
     * @throws IOException
     * @throws InterruptedException
     */
    @Test (priority = 3)
    public void updateRepository() throws IOException, InterruptedException {
        driver = new FirefoxDriver (options);

        Header authHeader = authorizedHeader();

        Map&lt;String,Object&gt; bodyParameters = new HashMap &lt;String,Object&gt;();

        bodyParameters.put("description", "Patched description on my newly created repository");
        //bodyParameters.put("private", "true");

        JSONObject newRepo = new JSONObject(bodyParameters);

        given().
            header(authHeader).
            body(newRepo.toJSONString()).
        when().
            patch(apiURL + "/repos/" + personalGithub.getUserName() + "/" + repositoryName).
        then().
            statusCode(200).
            log().all(true);

        delay();

        //Maximize Window
        driver.manage().window().maximize();
        driver.manage().timeouts().implicitlyWait(20, TimeUnit.SECONDS);

        driver.get(repositoryURL);

        saveWebSnapShot(driver,"SCRN 003 - Repository set to private and updated description.png");

        driver.quit();
    }

    /**
     * Authorized API call made to verify that a user's repository
     * can be deleted
     *

     * A basic example of DELETE that should generate status code 204
     * @throws IOException
     * @throws InterruptedException
     */
    @Test (priority = 4)
    public void deleteRepository() throws IOException, InterruptedException {
        driver = new FirefoxDriver (options);

        Header authHeader = authorizedHeader();

        given().
            header(authHeader).
        when().
            delete(apiURL + "/repos/" + personalGithub.getUserName() + "/" + repositoryName).
        then().
            statusCode(204).
            log().all(true);

        delay();

        //Maximize Window
        driver.manage().window().maximize();
        driver.manage().timeouts().implicitlyWait(20, TimeUnit.SECONDS);

        driver.get(repositoryURL);

        saveWebSnapShot(driver,"SCRN 004 - Repository Deleted.png");

        driver.quit();
    }
}

After the code executes, a tester can confirm from the logs as well as the captured screenshots that everything works from start to finish.

Closing Thoughts

Hopefully this tutorial has given a basic introduction into reading API documentation and being able to develop a project that makes the API calls. There are a plethora of tools such as the command line and Postman that can help test the calls before tests are created, setting up for some amazing automation projects.

For those that wish to experiment more with the base code, the entire code base for the project and necessary directories is currently hosted on a github repository. A git installation is required to access the github repository if you wish to clone it, so open the command line on the target directory and execute the following statement:

> git clone https://github.com/austin-ultratesting/JavaAPICapstoneProject OPTIONAL_DIRECTORY_NAME

Sources used for building the Wiki Topic

Scott Barstow's article on reading API documentation

-- AustinBell - 14 Apr 2021
Topic attachments
I Attachment Action Size Date Who Comment
Github API <a class="foswikiNewLink" href="/bin/edit/Automation/HomePage?topicparent=Automation.WorkingWithPublicAPIsForAutomationTests" rel="nofollow" title="Create this topic">HomePage</a>.pngpng Github API HomePage.png manage 53 K 13 Apr 2021 - 11:23 AustinBell Github API Home page screenshot
PostmanDemo.gifgif PostmanDemo.gif manage 2 MB 14 Apr 2021 - 09:15 AustinBell Postman Gif File to show a simple GET request set up
Topic revision: r4 - 14 Apr 2021, AustinBell
© 2020 Ultranauts - 75 Broad Street, 2nd Floor, Suite 206, New York, NY 10004 - info@ultranauts.co