Testing a Hypermedia REST API using Traverson

Joe • updated : October 13, 2020

In the previous post, we discussed how to create hypermedia driven REST APIs using Spring HATEOAS. In this post, we will have a quick look at how to test the restful API we developed in that previous post using Traverson. 

Traverson is a Spring Hateoas component inspired by a similar JavaScript library that makes it easier to navigate hypermedia APIs by following links with relation types.

Consider this endpoint

curl -X GET "http://localhost:8080/computingfacts/rest/enzymes/1.1.1.1" -H "accept: application/hal+json"
{
  "enzymeName": "Alcohol dehydrogenase",
  "ecNumber": "1.1.1.1",
  "enzymeFamily": "Oxidoreductases",
  "alternativeNames": [
    "Aldehyde reductase."
  ],
  "catalyticActivities": [
    "primary alcohol + NAD(+) = an aldehyde + H(+) + NADH"
  ],
  "cofactors": [
    "Zn(2+) or Fe cation."
  ],
  "associatedProteins": {
    "_embedded": {
      "proteins": [
        {
          "accession": "E1ACQ9",
          "proteinName": "Alcohol dehydrogenase notN",
          "organismName": "Aspergillus sp. (strain MF297-2)",
          "_links": {
            "protein": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9"
            },
            "protein structure": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/proteinStructure"
            },
            "reactions": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/reaction?limit=10"
            },
            "pathways": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/pathways"
            },
            "small molecules": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/smallmolecules"
            },
            "diseases": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/diseases"
            },
            "literature": {
              "href": "http://localhost:8080/computingfacts/rest/protein/E1ACQ9/citation?limit=10"
            }
          }
        },
        {
          "accession": "P9WQC6",
          "proteinName": "Alcohol dehydrogenase B",
          "organismName": "Mycobacterium tuberculosis (strain CDC 1551 / Oshkosh)",
          "_links": {
            "protein": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6"
            },
            "protein structure": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/proteinStructure"
            },
            "reactions": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/reaction?limit=10"
            },
            "pathways": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/pathways"
            },
            "small molecules": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/smallmolecules"
            },
            "diseases": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/diseases"
            },
            "literature": {
              "href": "http://localhost:8080/computingfacts/rest/protein/P9WQC6/citation?limit=10"
            }
          }
        }
      ]
    }
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/1.1.1.1"
    },
    "enzymes": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/?page=0&size=10"
    },
    "associated Proteins": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/1.1.1.1/proteins?limit=10"
    }
  }
}

The response is an Enzyme resource in HAL format with links to related resources.  Some of the linked resources also have nested links that describes them. We can consume the response in our GET request by following the links. Traversing these links will help us to gain more insight or build a better picture about the enzyme resource.

How to use Traverson

First, we instantiate the Traverson object with our API’s base URI:

String baseUrl = "http://localhost:" + this.port + "/computingfacts/rest/";

Traverson traverson = new Traverson(URI.create(baseUrl), MediaTypes.HAL_JSON);

Considering the above given endpoint (enzymes by EC  /enzymes/1.1.1.1), to retrieve the enzyme resource, we follow the IANA-based self link and get the concrete enzyme resource by calling the Traverson toObject() method, which executes the traversal and marshals our API response into an object of our given type.

Traverson.TraversalBuilder client = traverson.follow(IanaLinkRelations.SELF.value());

EnzymeModel enzymeModel = client.toObject(EnzymeModel.class);

Similarly, suppose we are interested in retrieving all the associated Protein to the Enzyme resource in the above example, as we already know that the associated Protein link has an href property that links to the Proteins resource, so we need to follow the associated Protein link by calling the follow() method on the Traverson Object and ingest the content the ProteinModel resource by calling the toObject() method.

CollectionModel<ProteinModel> proteins = traverson.follow("associated Proteins")
.toObject(new TypeReferences.CollectionModelType<ProteinModel>() {});

Let’s consider another scenario where we are only interested in Diseases associated to a Protein. The Protein resource have links to other resources that are related to Proteins and Diseases is one of them.

curl -X GET "http://localhost:8080/computingfacts/rest/protein/O15297" -H "accept: application/hal+json"
{
  "accession": "O15297",
  "proteinName": "Protein phosphatase 1D",
  "organismName": "Human",
  "_links": {
    "protein": {
      "href": "http://localhost:8080/computingfacts/rest/protein/O15297"
    },
    "protein structure": {
      "href": "http://localhost:8080/computingfacts/rest/protein/O15297/proteinStructure"
    },
    "reactions": {
      "href": "http://localhost:8080/computingfacts/rest/protein/O15297/reaction?limit=10"
    },
    "pathways": {
      "href": "http://localhost:8080/computingfacts/rest/protein/O15297/pathways"
    },
    "small molecules": {
      "href": "http://localhost:8080/computingfacts/rest/protein/O15297/smallmolecules"
    },
    "diseases": {
      "href": "http://localhost:8080/computingfacts/rest/protein/O15297/diseases"
    },
    "literature": {
      "href": "http://localhost:8080/computingfacts/rest/protein/O15297/citation?limit=10"
    }
  }
}

We could manually call the disease endpoint to retrieve the diseases associated with the protein by passing the protein identifier (UniProt accession).

curl -X GET "http://localhost:8080/computingfacts/rest/protein/O15297/diseases" -H "accept: application/hal+json"

But in practice a client code will discover links to resources of our interest as it consumes the API responses. Our client code could start from the root of the API endpoint and follow links to /enzymes/  -->   /enzymes/{ec} --> /enzymes/{ec}/proteins -->  /protein/{accession} -->  then to diseases (/protein/{accession}/diseases)

To retrieve only Diseases data, we can follow the links on the protein endpoint as shown in this test example;

    /**
     * Test of findProteinByAccession method, of class ProteinController.
     */
    @Test
    public void testFindProteinByAccession() {

        String accession = "O15297";
        String baseUrl = "http://localhost:" + this.port + "/computingfacts/rest/protein/" + accession;
        Traverson traverson = new Traverson(URI.create(baseUrl), MediaTypes.HAL_JSON);

        CollectionModel diseases = traverson
                .follow("protein", "diseases")
                .toObject(new TypeReferences.CollectionModelType() {
                });

        assertNotNull(diseases);
        assertNotNull(diseases.getContent());

        assertThat(diseases.getContent()).hasSize(3);
        assertThat(diseases.getContent()).asString().contains("Breast cancer (BC)");


    }

The integration test shows that one of the associated diseases linked to our Protein is Breast cancer (BC).

We have seen so far that Traverson enables clients to easily navigate an API using hyperlinks embedded in the responses.

Here is the complete code for integration test:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class EnzymeControllerIT {

    @LocalServerPort
    private int port;

    private Traverson getTraversonClient(String endpoint) {
        String url = "http://localhost:" + this.port + "/computingfacts/rest/" + endpoint;
        return new Traverson(URI.create(url), MediaTypes.HAL_JSON);
    }

    @Test
    public void testFindEnzymeByEcNumber() {
        String ec = "1.1.1.1";
        String endPoint = "enzymes/" + ec;
        Traverson traverson = getTraversonClient(endPoint);

        Traverson.TraversalBuilder client = traverson.follow(IanaLinkRelations.SELF.value());

        EnzymeModel enzymeModel = client.toObject(EnzymeModel.class);

        assertNotNull(enzymeModel);

        assertEquals("1.1.1.1", enzymeModel.getEcNumber());
        assertEquals("Alcohol dehydrogenase", enzymeModel.getEnzymeName());
        assertEquals("Oxidoreductases", enzymeModel.getEnzymeFamily());
        assertThat(enzymeModel.getCatalyticActivities()).hasSizeGreaterThanOrEqualTo(1);
        assertThat(enzymeModel.getAssociatedProteins()).hasSizeGreaterThanOrEqualTo(2);
        assertThat(enzymeModel.getAlternativeNames()).containsAnyOf("Aldehyde reductase.");

        //links
        assertTrue(enzymeModel.hasLinks());
        assertTrue(enzymeModel.getLinks().hasSize(3));
        assertTrue(enzymeModel.hasLink(LinkRelation.of(IanaLinkRelations.SELF.value())));
        assertTrue(enzymeModel.hasLink(LinkRelation.of("enzymes")));
        assertTrue(enzymeModel.hasLink("associated Proteins"));

        //enzymes link
        PagedModel enzymes = client
                .follow("enzymes")
                .toObject(new TypeReferences.PagedModelType() {
                });

        assertNotNull(enzymes);
        assertNotNull(enzymes.getMetadata());
        assertThat(enzymes.getMetadata().getSize()).isEqualTo(10L);

        //associated protein link
        CollectionModel proteins = traverson.follow("associated Proteins")
                .toObject(new TypeReferences.CollectionModelType() {
                });

        assertNotNull(proteins);
        assertNotNull(proteins.getContent());
        assertTrue(proteins.hasLinks());
        assertTrue(proteins.hasLink(IanaLinkRelations.SELF));
        assertThat(proteins.getContent()).hasSize(10);

    }


     @Test
    public void testEnzymes() {
        String endPoint = "enzymes/";
        Traverson traverson = getTraversonClient(endPoint);
        Traverson.TraversalBuilder client = traverson.follow(IanaLinkRelations.SELF.value());

        PagedModel enzymes = client
                .toObject(new TypeReferences.PagedModelType() {
                });

        assertNotNull(enzymes);
        assertNotNull(enzymes.getContent());
        assertTrue(enzymes.hasLinks());

        assertTrue(enzymes.hasLink(IanaLinkRelations.FIRST));
        assertTrue(enzymes.hasLink(IanaLinkRelations.SELF));
        assertTrue(enzymes.hasLink(IanaLinkRelations.NEXT));
        assertTrue(enzymes.hasLink(IanaLinkRelations.LAST));

        //page object
        assertNotNull(enzymes.getMetadata());
        assertTrue(enzymes.getMetadata().getNumber() == 0);
        assertTrue(enzymes.getMetadata().getTotalPages() > 100);
        assertTrue(enzymes.getMetadata().getTotalElements() > 1_000);
        assertTrue(enzymes.getMetadata().getSize() == 10);

        List enzymeNames = client.toObject("$._embedded.enzymes.[*].enzymeName");
        List associatedProteinsAccession = client.toObject("$._embedded.enzymes.[0].associatedProteins._embedded.proteins[*].accession");

        assertThat(enzymeNames).hasSizeGreaterThanOrEqualTo(10);
        assertThat(associatedProteinsAccession).hasSizeGreaterThanOrEqualTo(2);

    }

     @Test
    public void testFindAssociatedProteinsByEcNumber() {

        String ec = "1.1.1.1";
        int limit = 10;
        String endPoint = "enzymes/" + ec + "/proteins?limit=" + limit;
        Traverson traverson = getTraversonClient(endPoint);

        Traverson.TraversalBuilder client = traverson.follow(IanaLinkRelations.SELF.value());

        TypeReferences.CollectionModelType collectionModelType = new TypeReferences.CollectionModelType() {
        };

        CollectionModel proteinModel = traverson.
                follow(rel(IanaLinkRelations.SELF.value())).
                toObject(collectionModelType);

        assertTrue(proteinModel.hasLink(IanaLinkRelations.SELF));

        List accessions = client.toObject("$._embedded.proteins.[*].accession");

        assertThat(accessions).hasSizeGreaterThanOrEqualTo(2);
        
                //templated
        String proteinsHref = "/enzymes/{ec}/proteins{?limit}";
        Link proteinLink = Link.of(proteinsHref);

        assertThat(proteinLink.isTemplated()).isTrue();
        assertThat(proteinLink.getVariableNames()).contains("ec", "limit");
    }

}

As usual, the source code is available on GitHub. Comments and feedbacks are welcome. Happy coding!!!

Similar Posts ..

Subscribe to our monthly newsletter. No spam, we promise !

Guest