Logo

dev-resources.site

for different kinds of informations.

Test Driven Api Development With Cypress

Published at
9/16/2024
Categories
tdd
typescript
cypress
dotnet
Author
papasanto
Categories
4 categories in total
tdd
open
typescript
open
cypress
open
dotnet
open
Author
9 person written this
papasanto
open
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:

tdd Article's
30 articles in total
Favicon
Test in Isolation
Favicon
Why Test Driven Development
Favicon
How to start with test driven development (TDD)
Favicon
What we will test
Favicon
How Test-Driven Development (TDD) Enhances Code Refactoring and Maintains Software Quality
Favicon
Why should I care about Quality? I'm a developer!
Favicon
TDD with spring-boot: A struggle of an experienced developer
Favicon
Test with Spy and Mock
Favicon
Test-Driven Development (TDD) with Bun Test
Favicon
Test with Dummy and Stub
Favicon
Modern Test Pyramid
Favicon
Some Essential Coding Practices Every Experienced Developer Recommends
Favicon
Not everything is a Mock, let's explore Test Doubles
Favicon
When does the TDD approach make sense?
Favicon
Go-DOM - 1st major milestone
Favicon
Stop saying that Test-Driven Development is just a testing methodology!
Favicon
Minitest Advantages: Simple Testing for Rails Projects
Favicon
Comprehensive Testing in .NET 8: Using Moq and In-Memory Databases
Favicon
From PHPUnit to Go: Data-Driven Unit Testing for Go Developers
Favicon
YAGNI For Types
Favicon
Go-DOM - A headless browser written in Go.
Favicon
Test-Driven Development: A Comprehensive Guide
Favicon
There's no place for Test-Driven Development (TDD)
Favicon
EasyTdd 0.5.0: Streamlining Mocking with Incremental FluentMock
Favicon
Learn TDD with Ruby - Loops, Blocks and Strings
Favicon
Shaping the state of Test-Driven Development
Favicon
Test-Driven Development For Analytics Engineering
Favicon
Test Driven Api Development With Cypress
Favicon
Guide to Building a Complete Blog App with Django using TDD methodology and PostgreSQL database: Installation and Setup
Favicon
Mock server

Featured ones: