DEV Community

Cover image for Test Driven Api Development With Cypress
Anthony Santonocito
Anthony Santonocito

Posted on • Edited on

Test Driven Api Development With Cypress

Delivering reliable and high-quality APIs is crucial for ensuring seamless integration and functionality. Test-Driven Development (TDD) has emerged as a powerful methodology to enhance code quality and streamline the development process.

This article will review technical requirements and develop Cypress test code for TDD. The following guidelines will be illustrated in the test code.

Testing Guidelines

  • Test code should build upon previous test code
  • Test whenever possible for early bug detection
  • Develop the API between each test for faster debugging
  • Tests should not impact the database
  • Ensure your tests are robust against unrelated changes
  • Failed Tests Improve Refactoring Confidence

Guideline - Test code should build upon previous test code

  • Check that POST returns the proper status/properties:
    POST

  • Check that POST results in properly stored data:
    POST > GET checks posted data

  • Check that DELETE results in the deletion of the data:
    POST > DELETE > GET confirms a bad request for deleted information

  • Watch the Test Driven API Development Demonstration

Requirements:

Model the following database:

Image description

Seed the database with the following 3 products:

[
0: {
id: 1
name: "Dog Food"
description: "Your dog will love our beef and rice flavored dry dog food."
price: 9.99
}
1: {
id: 2
name: "Cat Food"
description: "Your cat will passively enjoy our salmon flavored wet cat food."
price: 12.99
}
2: {
id: 3
name: "Lizard Food"
description: "Your lizard likes to eat bugs. So this is made of bugs."
price: 3.99
}
]

Enter fullscreen mode Exit fullscreen mode

Requirement 1:

  • Create a GET endpoint returning all products in a JSON array of objects.
  • Return all fields including (id, name, description, price).

TEST NAME: cy.gets-all-products

network_requests.cy.js

const base = "http://localhost:5090/api/";

context("Network Requests", () => {
  it("cy.gets-all-products", () => {
    cy.request(`${base}product`).then((response) => {
      expect(response.status).to.eq(200);
      expect(response.body[0]).to.include.keys(
        "id",
        "name",
        "description",
        "price"
      );
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

This test makes an GET api call then checks the status and keys of the call. Requirement 3 will show a more thorough GET test.

Guideline - Develop the api between each test for faster debugging

Testing and development should be continuous

Writing all of the test code before the API code is similar to writing all of the API code before the test code.

Requirement 2:

  • Create a Post endpoint accepting lineitem fields (id, quantity, userId).
  • This endpoint must create an orderheader and add this lineitem to that order.
  • It should also set the total field on the orderheader.
  • It must return the orderheaderid of the orderheader created.
network_requests.cy.js

const base = "http://localhost:5090/api/";
const item = { productid: 1, quantity: 2, userid: 1 };
 ...
  it("cy.posts-order", () => {
    // Create Order
    cy.request("POST", `${base}order`, item).then((response) => {
      expect(response.status).to.eq(201);
      expect(response.body).to.include.keys("id");
    });

// Delete Order

});
Enter fullscreen mode Exit fullscreen mode

A POST request sent lineitem information along with the userid to create an order. The status and keys of a POST call are checked.

Guideline - Test whenever possible for early bug detection:

Even if the test seems partial, it should still be written. We do not have a way to delete this POST yet. We also cannot properly check the data in the database without a way to retrieve an orderheader.

Requirement 3:

  • Create a GET endpoint accepting a query labeled "id" which will be used to pass the orderheaderid.
  • Use the orderheaderid to query the database and return the orderheader and related orderlines as well as the orderlines related product.
  • Return all fields of all tables involved.

TEST NAME: cy.retrieves-order-by-id

network_requests.cy.js

const base = "http://localhost:5090/api/";
const item = { productid: 1, quantity: 2, userid: 1 };

...

  it("cy.retrieves-order-by-id", () => {
    // Create Order
    cy.request("POST", `${base}order`, item).then((response) => {
      cy.wrap(response.body.id).as("id");
    });

    // Get Order By ID
    cy.get("@id").then((id) => {
      cy.request({
        url: `${base}order/byid`,
        qs: {
          id: id,
        },
      }).then((response) => {
        expect(response.status).to.eq(200);
        expect(response.body.lineitems[0]).to.include.keys(
          "id",
          "quantity",
          "price",
          "productid",
          "product",
          "orderheaderid",
          "orderheader"
        );
        expect(response.body.lineitems[0].product).to.include.keys(
          "id",
          "name",
          "description",
          "price"
        );
        expect(response.body.lineitems).to.be.an("array").and.have.lengthOf(1);
        expect(response.body.total).eq(
            response.body.lineitems[0].price * response.body.lineitems[0].quantity
          );
        expect(response.body.customer).eq(1);
        expect(response.body.lineitems[0].productid).eq(1);
        expect(response.body.lineitems[0].quantity).eq(2);
        expect(response.body.lineitems[0].price).eq(
          response.body.lineitems[0].product.price
        );
      });

      // Delete Order
    });
  });
Enter fullscreen mode Exit fullscreen mode

While within a single test block we can save values with
cy.wrap(<value>).as("<key>") the key can be retrieved with cy.get("@<key>").then((<value>) => {})

This test confirms every field is properly populated during our POST.

Guideline - Ensure your tests are robust against unrelated changes:

Avoid hardcoding values that might change.

In the above code, the price of the orderline is tested against the price of the product. The total of the orderheader is linked to the price and quantity of the orderline. This test will not fail if the product price is change. It will fail if the API is not properly updating the lineitem price.

Requirement 4:

  • Create a DELETE endpoint accepting a query labeled "id" which will be used to pass the orderheaderid.
  • This endpoint must delete an orderheader.
  • It should return JSON with key "success" and value true.

Guideline - Tests should not impact the database:

After running all API tests, the database should be left unchanged
You will notice delete in all the relevant positions in the test code. Failed tests may impact the database.

This is the final code.

TEST NAME: cy.deletes-an-order

network_requests.cy.js

const base = "http://localhost:5090/api/";
const item = { productid: 1, quantity: 2, userid: 1 };

context("Network Requests", () => {
  it("cy.gets-all-products", () => {
    cy.request(`${base}product`).then((response) => {
      expect(response.status).to.eq(200);
      expect(response.body[0]).to.include.keys(
        "id",
        "name",
        "description",
        "price"
      );
    });
  });

  it("cy.posts-order", () => {
    // Create Order
    cy.request("POST", `${base}order`, item).then((response) => {
      expect(response.status).to.eq(201);
      expect(response.body).to.include.keys("id");
      cy.wrap(response.body.id).as("id");
    });

    // Delete Order
    cy.get("@id").then((id) => {
      cy.request("DELETE", `${base}order?id=${id}`).then((response) => {
        expect(response.status).to.eq(200);
        expect(response.body).to.include.keys("success");
      });
    });
  });

  it("cy.retrieves-order-by-id", () => {
    // Create Order
    cy.request("POST", `${base}order`, item).then((response) => {
      cy.wrap(response.body.id).as("id");
    });

    // Get Order By ID
    cy.get("@id").then((id) => {
      cy.request({
        url: `${base}order/byid`,
        qs: {
          id: id,
        },
      }).then((response) => {
        expect(response.status).to.eq(200);
        expect(response.body.lineitems[0]).to.include.keys(
          "id",
          "quantity",
          "price",
          "productid",
          "product",
          "orderheaderid",
          "orderheader"
        );
        expect(response.body.lineitems[0].product).to.include.keys(
          "id",
          "name",
          "description",
          "price"
        );
        expect(response.body.lineitems).to.be.an("array").and.have.lengthOf(1);
        expect(response.body.total).eq(
          response.body.lineitems[0].price * response.body.lineitems[0].quantity
        );
        expect(response.body.customer).eq(1);
        expect(response.body.lineitems[0].productid).eq(1);
        expect(response.body.lineitems[0].quantity).eq(2);
        expect(response.body.lineitems[0].price).eq(
          response.body.lineitems[0].product.price
        );
        // Delete Order
        cy.request("DELETE", `${base}order?id=${id}`).then((response) => {
          expect(response.status).to.eq(200);
          expect(response.body).to.include.keys("success");
        });
      });
    });
  });

  it("cy.deletes-an-order", () => {
    // Create Order
    cy.request("POST", `${base}order`, item).then((response) => {
      cy.wrap(response.body.id).as("id");
    });

    cy.get("@id").then((id) => {

      // Delete Order
      cy.request("DELETE", `${base}order?id=${id}`).then((response) => {
        expect(response.status).to.eq(200);
        expect(response.body).to.include.keys("success");
      });

      // Get Deleted Order By Id
      cy.request({
        url: `${base}order/byid`,
        failOnStatusCode: false,
        qs: {
          id: id,
        },
      }).then((response) => {
        expect(response.status).to.eq(400);
      });
    });
  });
});

Enter fullscreen mode Exit fullscreen mode

Failed API calls result in a Cypress test failure. Including failOnStatusCode: false in the request, negates that behavior.

The next requirement might force us to ensure a cascading delete of all lineitems upon deletion of an orderheader. Try writing that test. Here is the sudo code.

  • POST an order
  • GET orderheader
  • Save the lineitemid
  • DELETE the orderheader
  • GET lineitem by passing the lineitemid to an endpoint the returns a lineitem by id - expect this get to fail

Guideline - Failed Tests Improve Refactoring Confidence:

Testing for functionality should include tests that expect to fail.
In the repo, you will see many tests expecting to fail after the order status is changed. Note that this results in a test passing.

View the finished code including the API on github:

Top comments (0)