A Journey of a Shopping Cart π: Test-Driven Development π οΈ (Part 2)
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:
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.
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
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
Export
PricingCalculator
module by adding this line at the end of the PricingCalculator.js file:module.exports = PricingCalculator;
add the following line to the package.json file
"scripts": { "test": "mocha" }
Create a folder namely "test"
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 necessaryNow 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!");
Now run mocha :
mocha test/pricingCalculatorTest.js
Alternatively, if you installed Mocha locally, you can run it using:
npx mocha test/pricingCalculatorTest.js
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.
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