A Journey of a Shopping Cart πŸ›’: Test-Driven Development πŸ› οΈ (Part 2)

Subrata ChSubrata Ch
10 min read

When developing reliable software, it's crucial to ensure your code performs as expected. A Test-Driven Development (TDD) approach helps by encouraging you to write tests before implementing the actual functionality. In this post, we will explore how to apply a test-driven approach to a class implementation using the Mocha test framework. Our focus will be on a PricingCalculator class, which calculates pricing details based on quantity, price, discount rate, and VAT rate.

If you have missed Part 1, A Journey of a Shopping Cart: From Corner Shop to E-Shop πŸ›’πŸ›οΈ (Part 1) and landed on Part 2 of this discussion, then allow me to reintroduce the PricingCalculator class and set the stage for our exploration.

class PricingCalculator {
    constructor(quantity, price, discountRate, vatRate) {
        this.quantity = Number(quantity);
        this.price = Number(price);
        this.discountRate = Number(discountRate);
        this.vatRate = Number(vatRate);
        this.validateInputs();
    }

    validateInputs() {
        if (isNaN(this.quantity) || this.quantity <= 0) {
            throw new Error('Quantity must be a positive number.');
        }
        if (isNaN(this.price) || this.price < 0) {
            throw new Error('Price must be a non-negative number.');
        }
        if (isNaN(this.discountRate) || this.discountRate < 0 || this.discountRate > 100) {
            throw new Error('Discount rate must be a number between 0 and 100.');
        }
        if (isNaN(this.vatRate) || this.vatRate < 0 || this.vatRate > 100) {
            throw new Error('VAT rate must be a number between 0 and 100.');
        }
    }

    getTotal() {
        return this.quantity * this.price;
    }

    applyDiscount(totalAmount) {
        const discount = this.discountRate / 100;
        const discountAmount = totalAmount * discount;
        return {
            discountedAmount: totalAmount - discountAmount,
            discountAmount: discountAmount
        };
    }

    addVAT(totalAmount) {
        const vat = this.vatRate / 100;
        const vatAmount = totalAmount * vat;
        return {
            totalWithVAT: totalAmount + vatAmount,
            vatAmount: vatAmount
        };
    }

    calculateFinalAmount() {
        let totalAmount = this.getTotal();
        const discountResult = this.applyDiscount(totalAmount);
        const vatResult = this.addVAT(discountResult.discountedAmount);
        return vatResult.totalWithVAT;
    }

    logCalculations() {
        try {
            let totalAmount = this.getTotal();
            console.log(`Total before the discount: Β£${totalAmount.toFixed(2)}`);
            const discountResult = this.applyDiscount(totalAmount);
            console.log(`Discount amount (${this.discountRate}%): Β£${discountResult.discountAmount.toFixed(2)}`);
            console.log(`Total after ${this.discountRate}% discount: Β£${discountResult.discountedAmount.toFixed(2)}`);
            const vatResult = this.addVAT(discountResult.discountedAmount);
            console.log(`VAT amount (${this.vatRate}%): Β£${vatResult.vatAmount.toFixed(2)}`);
            console.log(`Total after adding ${this.vatRate}% VAT: Β£${vatResult.totalWithVAT.toFixed(2)}`);
            return vatResult.totalWithVAT;
        } catch (error) {
            console.error('Error:', error.message);
        }
    }
}

// Example usage
const calculator = new PricingCalculator(1, 100, "10", 20);
const finalAmount = calculator.calculateFinalAmount();
calculator.logCalculations();

/*  OUTPUT: 
Total before the discount: Β£100.00
Discount amount (10%): Β£10.00
Total after 10% discount: Β£90.00
VAT amount (20%): Β£18.00
Total after adding 20% VAT: Β£108.00
*/

What is Test-Driven Development (TDD)?

Test-Driven Development (TDD) is a software development process where you write tests before writing the actual code. This approach follows a cycle: write a test, run the test (which initially fails), write the minimal code needed to pass the test, and then refactor the code while keeping the tests passing. This iterative process helps ensure your code is thoroughly tested and works correctly.

Hold on a second! πŸ˜•... The description of TDD and the provided code for the PricingCalculator class might seem contradictory if the tests were written after the code! However, here the code provided for the PricingCalculator class assumes that the implementation already exists, and tests are written to verify that implementation.

Let’s take a Mocha to test my patience with Testing...! β˜•

Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser (https://mochajs.org/). It provides an easy way to structure and run tests, making it a great choice for verifying JavaScript code behaviour. With Mocha, we can create test suites, write individual test cases, and use various hooks to manage test execution.

In my previous post (Part 1), we only tested the output of these calculations and did not test the validations and error handling. Let's write some tests to cover those aspects.

How to:

I assume you have VS Code (IDE) installed on your computer. If not, you can download it from here. I will use a Windows PC for simplicity, but the documentation has plenty of instructions for Mac, Linux, and other platforms.

Once you're ready, create a project folder with a name of your choice. Here, I named it shopping_cart_test. Open this folder in VS Code and create a JavaScript file called PricingCalculator.js. Copy the provided code into this file.

Let's set up Mocha🍡 :

How to setup Mocha for testing:

  1. Install Node js : Please visit the Node.js official website. Download the Windows Installer (.msi) for either the LTS (Long Term Support) or the current version. Run the downloaded installer and follow the setup wizard to complete the installation.

  2. Verify installation by opening the command prompt or PowerShell. Run the following commands to check if Node.js and npm (Node Package Manager) were installed correctly:
    node -v

    npm -v

  3. Install Mocha (First, you need to install Mocha globally or as a development dependency in your project.) Globally (recommended if you’re running tests across multiple projects):)

    npm install -g mocha

    Locally (as a dev dependency in your project):

    npm install --save-dev mocha

  4. Export PricingCalculator module by adding this line at the end of the PricingCalculator.js file:

    module.exports = PricingCalculator;

  5. add the following line to the package.json file

    "scripts": { "test": "mocha" }

  6. Create a folder namely "test"

  7. Inside the test folder create a test file namely "PricingCalculatorTest.js" and import the assert & PricingCalculator modules:

    const assert = require('assert');

    const PricingCalculator = require('../PricingCalculator'); // Adjust the path if necessary

  8. Now keep writing your tests in the "PricingCalculatorTest.js" file. example

const assert = require('assert');
const PricingCalculator = require('../PricingCalculator');  // Adjust the path if necessary

describe('PricingCalculator', function() {
    it('should throw error for invalid price', function() {
        assert.throws(() => new PricingCalculator(2, -50, 10, 20), /Price must be a non-negative number./);
        assert.throws(() => new PricingCalculator(2, 'b', 10, 20), /Price must be a non-negative number./);
    });
});
console.log("All tests passed!");
  1. Now run mocha :

    mocha test/pricingCalculatorTest.js

    Alternatively, if you installed Mocha locally, you can run it using:

    npx mocha test/pricingCalculatorTest.js

  2. If everything goes well, you should see the following in the terminal:

All tests passed!

  PricingCalculator
    βœ” should throw error for invalid price

  1 passing (5ms)

Now that you have successfully run a single test, Let’s get started with writing more tests and developing the class to meet those requirements!😴

const assert = require('assert'); // Import the assert module for making assertions in tests
const PricingCalculator = require('../PricingCalculator');  // Import the PricingCalculator class from the specified path


// Hint: constructor(quantity, price, discountRate, vatRate)
describe('PricingCalculator', function() {
    // Test Case: Valid Inputs
    it('T1: Should calculate final amount correctly for valid inputs', function() {
        // Create an instance of PricingCalculator with valid inputs
        //Hint : constructor(quantity, price, discountRate, vatRate)
        const calculator = new PricingCalculator(2, 50, 10, 20);
        // Check that the final amount calculated is correct
        assert.strictEqual(calculator.calculateFinalAmount(), 108);
    });

    // Test Case: Invalid Quantity
    it('T2: Should throw error for invalid quantity', function() {
        // Test that creating a PricingCalculator with invalid quantity throws the expected error
        // Hint : constructor(quantity, price, discountRate, vatRate)
        assert.throws(() => new PricingCalculator(-1, 50, 10, 20), /Quantity must be a positive number./);
        assert.throws(() => new PricingCalculator(0, 50, 10, 20), /Quantity must be a positive number./);
        assert.throws(() => new PricingCalculator('a', 50, 10, 20), /Quantity must be a positive number./);
    });

    // Test Case: Invalid Price
    it('T3: Should throw error for invalid price', function() {
        // Test that creating a PricingCalculator with an invalid price throws the expected error
        // Hint : constructor(quantity, price, discountRate, vatRate)
        assert.throws(() => new PricingCalculator(2, -50, 10, 20), /Price must be a non-negative number./);
        assert.throws(() => new PricingCalculator(2, 'b', 10, 20), /Price must be a non-negative number./);
    });

    // Test Case: Invalid Discount Rate
    it('T4: Should throw error for invalid discount rate', function() {
        // Test that creating a PricingCalculator with an invalid discount rate throws the expected error
        // Hint : constructor(quantity, price, discountRate, vatRate)
        assert.throws(() => new PricingCalculator(2, 50, -10, 20), /Discount rate must be a number between 0 and 100./);
        assert.throws(() => new PricingCalculator(2, 50, 110, 20), /Discount rate must be a number between 0 and 100./);
        assert.throws(() => new PricingCalculator(2, 50, 'c', 20), /Discount rate must be a number between 0 and 100./);
    });

    // Test Case: Invalid VAT Rate
    it('T5: Should throw error for invalid VAT rate', function() {
        // Test that creating a PricingCalculator with an invalid VAT rate throws the expected error
        // Hint : constructor(quantity, price, discountRate, vatRate)
        assert.throws(() => new PricingCalculator(2, 50, 10, -20), /VAT rate must be a number between 0 and 100./);
        assert.throws(() => new PricingCalculator(2, 50, 10, 120), /VAT rate must be a number between 0 and 100./);
        assert.throws(() => new PricingCalculator(2, 50, 10, 'd'), /VAT rate must be a number between 0 and 100./);
    });

    // Test Case: Edge Cases
    it('T6: Should calculate correct values for edge cases', function() {
        // Test edge cases to ensure calculations are handled correctly
        // Hint : constructor(quantity, price, discountRate, vatRate)
        let calculator = new PricingCalculator(2, 50, 0, 20);
        assert.strictEqual(calculator.calculateFinalAmount(), 120);

        calculator = new PricingCalculator(2, 50, 10, 0);
        assert.strictEqual(calculator.calculateFinalAmount(), 90);

        calculator = new PricingCalculator(2, 50, 100, 20);
        assert.strictEqual(calculator.calculateFinalAmount(), 0);

        calculator = new PricingCalculator(2, 50, 10, 100);
        assert.strictEqual(calculator.calculateFinalAmount(), 180);
    });

    // Test Case 1: Valid String Inputs 
    it('T7: Should handle valid string inputs as numbers', function() {
        // Test that valid numeric strings are correctly converted and handled
        // Hint : constructor(quantity, price, discountRate, vatRate)
        const calculator = new PricingCalculator("2", "50", "10", "20");
        assert.strictEqual(calculator.calculateFinalAmount(), 108);
    });

    // Test Case 2: Valid String Inputs 
    it('T8: Should throw error for only string inputs', function() {
        // Test that invalid string inputs are correctly handled
        // Hint : constructor(quantity, price, discountRate, vatRate)
        assert.throws(() => new PricingCalculator("abc", "abc", "def", "abcd"), /Quantity must be a positive number./);
        assert.throws(() => new PricingCalculator("abc", 50, "def", "abcd"), /Quantity must be a positive number./);
        assert.throws(() => new PricingCalculator(2, "abc", "def", "abcd"), /Price must be a non-negative number./);
        assert.throws(() => new PricingCalculator(2, 50, "def", "abcd"), /Discount rate must be a number between 0 and 100./);
        assert.throws(() => new PricingCalculator(2, 50, 10, "abcd"), /VAT rate must be a number between 0 and 100./);
    });

    // Test Case: Zero Values for Discount and VAT
    it('T9: Should handle zero values for discount and VAT', function() {
        // Hint : constructor(quantity, price, discountRate, vatRate)
        const calculator = new PricingCalculator(10, 100, 0, 0);
        assert.strictEqual(calculator.calculateFinalAmount(), 1000);
    });

    // Test Case: High Values for Discount and VAT
    it('T10: Should handle high values for discount and VAT', function() {
        // Hint : constructor(quantity, price, discountRate, vatRate)
        const calculator = new PricingCalculator(1, 100, 100, 100);
        assert.strictEqual(calculator.calculateFinalAmount(), 0);
    });

    // Test Case: Large Values for Quantity and Price
    it('T11: Should handle large values for quantity and price', function() {
        // Hint : constructor(quantity, price, discountRate, vatRate)
        const calculator = new PricingCalculator(1000000, 1000000, 10, 20);
        assert.strictEqual(calculator.calculateFinalAmount(), 1080000000000);
    });

    // Test Case: Negative Numbers as Strings
    it('T12: Should throw error for negative numbers as strings', function() {
        // Hint : constructor(quantity, price, discountRate, vatRate)
        assert.throws(() => new PricingCalculator("-10", "50", "10", "20"), /Quantity must be a positive number./);
        assert.throws(() => new PricingCalculator("2", "-50", "10", "20"), /Price must be a non-negative number./);
        assert.throws(() => new PricingCalculator("2", "50", "-10", "20"), /Discount rate must be a number between 0 and 100./);
        assert.throws(() => new PricingCalculator("2", "50", "10", "-20"), /VAT rate must be a number between 0 and 100./);
    });

    // Test Case: Mixed Valid and Invalid Inputs
    it('T13: Should throw error for mixed valid and invalid inputs', function() {
        // Hint : constructor(quantity, price, discountRate, vatRate)
        assert.throws(() => new PricingCalculator("2", 50, "def", "abcd"), /Discount rate must be a number between 0 and 100./);
        assert.throws(() => new PricingCalculator("2", "abc", 10, "abcd"), /Price must be a non-negative number./);
    });

    // Test Case: Empty String Inputs
    it('T14: Should throw error for empty string inputs', function() {
        // Hint : constructor(quantity, price, discountRate, vatRate)
        assert.throws(() => new PricingCalculator("", "", "", ""), /Quantity must be a positive number./);
    });

});

//  Log a message indicating that all tests have passed
console.log("All tests passed!");

Now let's run it all together:

mocha test/pricingCalculatorTest.js

Here is the output on the terminal :

All tests passed!

  PricingCalculator
    βœ” T1: Should calculate final amount correctly for valid inputs
    βœ” T2: Should throw error for invalid quantity
    βœ” T3: Should throw error for invalid price
    βœ” T4: Should throw error for invalid discount rate
    βœ” T5: Should throw error for invalid VAT rate
    βœ” T6: Should calculate correct values for edge cases
    βœ” T7: Should handle valid string inputs as numbers
    βœ” T8: Should throw error for only string inputs
    βœ” T9: Should handle zero values for discount and VAT
    βœ” T10: Should handle high values for discount and VAT
    βœ” T11: Should handle large values for quantity and price
    βœ” T12: Should throw error for negative numbers as strings
    βœ” T13: Should throw error for mixed valid and invalid inputs
    βœ” T14: Should throw error for empty string inputs

  14 passing (12ms)

In our approach, we've executed 14 tests to thoroughly examine the PricingCalculator class across various inputs and scenarios, unless we’ve missed any. By adhering to the TDD methodology, we ensure that the class meets all necessary criteria before finalising its implementation.

0
Subscribe to my newsletter

Read articles from Subrata Ch directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Subrata Ch
Subrata Ch

Software Engineer & Consultant