Simple JavaScript Automation Unit Testing Using Jest
Objective Learn to implement automated unit testing using Jest in a Node.js application. By the end of this lab, you will: Set up a Node.js project with Jest for unit testing Write unit tests for functions with validation and error handling Understand the difference between statement and branch coverage Run tests and generate coverage reports to evaluate code quality Node.js (version 14 or higher) installed Basic knowledge of JavaScript and Node.js A code editor (e.g., VS Code) Terminal or command-line interface This lab guides you through creating a Node.js application with a divide function. You will progressively enhance the function while learning about unit testing, the AAA pattern, and the critical difference between statement and branch coverage. Unit testing ensures individual code components work as expected, catching errors early and maintaining software quality. Without unit tests, bugs can reach production and cause serious issues. Real-world example: Suppose a developer accidentally modifies a divide function to use multiplication instead of division while fixing a bug. This would make divide(10, 2) return 20 instead of 5, causing incorrect calculations. A unit test like expect(divide(10, 2)).toBe(5) would immediately fail, alerting the developer to the error. CI/CD Integration: Unit tests integrate with Continuous Integration/Continuous Deployment pipelines. In systems like GitHub Actions, tests run automatically on every code change. If any test fails, the pipeline blocks the code from being merged or deployed to production, ensuring faulty code never reaches users. Before diving into testing, it's important to understand the AAA pattern (Arrange-Act-Assert), a fundamental structure for writing clear and effective unit tests: Arrange: Set up the test data and conditions Prepare the inputs Initialize any required variables or objects Set up the environment for the test Act: Execute the code being tested Call the function or method Perform the action you want to test This is usually a single line of code Assert: Verify the expected outcome Check that the result matches expectations Use assertion methods like expect().toBe() Confirm the code behaved correctly The AAA pattern makes tests easier to read, understand, and maintain. You'll see this pattern throughout the lab. Create a project directory: mkdir jest-divide-app cd jest-divide-app Initialize a Node.js project: npm init -y Install Jest: npm install --save-dev jest Update package.json: Open package.json and modify the scripts section: "scripts": { "test": "jest", "test:coverage": "jest --coverage" } Create divide.js: Create a file named divide.js in the project root with the following simple code: function divide(a, b) { return a / b; } module.exports = divide; Verify the file structure: jest-divide-app/ ├── node_modules/ ├── package.json ├── package-lock.json └── divide.js Create divide.test.js: Create a file named divide.test.js in the project root: const divide = require('./divide'); test('divides 10 by 2 to equal 5', () => { expect(divide(10, 2)).toBe(5); // Arrange, Act, Assert combined }); test('divides 20 by 4 to equal 5', () => { expect(divide(20, 4)).toBe(5); }); test('divides 100 by 10 to equal 10', () => { expect(divide(100, 10)).toBe(10); }); Test code explanation: require('./divide'): Imports the divide function from divide.js test(): Defines individual test cases with a description and assertion expect(divide(10, 2)).toBe(5): Follows the AAA pattern in a single line Arrange: The values 10 and 2 are the test inputs Act: divide(10, 2) calls the function Assert: .toBe(5) verifies the expected result Execute the tests: npm test Expected output: PASS ./divide.test.js ✓ divides 10 by 2 to equal 5 (2 ms) ✓ divides 20 by 4 to equal 5 (1 ms) ✓ divides 100 by 10 to equal 10 (1 ms) Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total Snapshots: 0 total Time: 0.5 s Run the coverage command: npm run test:coverage Expected output: PASS ./divide.test.js √ divides 10 by 2 to equal 5 (3 ms) √ divides 20 by 4 to equal 5 (1 ms) √ divides 100 by 10 to equal 10 (1 ms) -----------------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -----------------|---------|----------|---------|---------|------------------- All files | 100 | 100 | 100 | 100 | divide.js | 100 | 100 | 100 | 100 | -----------------|---------|----------|---------|---------|------------------- Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total Snapshots: 0 total Time: 4.653 s, estimated 13 s Ran all test suites. View the HTML coverage report: Jest creates a coverage directory in your project root Open coverage/lcov-report/index.html in a web browser The report displays which lines, functions, and branches are covered by tests 💡 Tip: Coverage doesn’t guarantee correctness. You can still have 100% statement/branch coverage and a failing test suite (for example, if the code is correct but a test has the wrong expected value, uses stale requirements, or has a flawed assertion). Always run and fix failing tests first, then use coverage to find what you’re missing—not to prove everything is correct. Test results: All 3 tests should pass Tests cover basic division operations If any test fails, review the test case and function implementation Coverage report: The report should show 100% coverage for divide.js All lines of the simple divide function are exercised by the tests Mathematical operations have rules. Division by zero is undefined and should be handled properly. Update divide.js: Add error handling for division by zero: function divide(a, b) { if (b === 0) { throw new Error('Cannot divide by zero'); } return a / b; } module.exports = divide; Run existing tests: npm test Important: All 3 existing tests should still pass. They use non-zero divisors, but the coverage report will look like this: PASS ./divide.test.js √ divides 10 by 2 to equal 5 (3 ms) √ divides 20 by 4 to equal 5 (1 ms) √ divides 100 by 10 to equal 10 (1 ms) -----------------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -----------------|---------|----------|---------|---------|------------------- All files | 75 | 50 | 100 | 75 | divide.js | 75 | 50 | 100 | 75 | 3 -----------------|---------|----------|---------|---------|------------------- Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total Snapshots: 0 total Time: 4.653 s, estimated 13 s Ran all test suites. Update divide.test.js: Add test cases for the zero-division error: const divide = require('./divide'); // Tests for valid division operations test('divides 10 by 2 to equal 5', () => { expect(divide(10, 2)).toBe(5); }); test('divides 20 by 4 to equal 5', () => { expect(divide(20, 4)).toBe(5); }); test('divides 100 by 10 to equal 10', () => { expect(divide(100, 10)).toBe(10); }); // Tests for error handling (AAA pattern for error testing) test('throws error when dividing by zero', () => { // Arrange - prepare invalid input const divisor = 0; // Act & Assert - expect function to throw error expect(() => divide(10, divisor)).toThrow('Cannot divide by zero'); }); test('throws error when dividing zero by zero', () => { expect(() => divide(0, 0)).toThrow('Cannot divide by zero'); }); Note on testing errors with AAA: - **Arrange**: Prepare invalid inputs - **Act & Assert**: Use `expect(() => ...)` to wrap the function call and verify it throws - The arrow function prevents the error from stopping the test execution Run the updated tests: npm test Expected output: PASS ./divide.test.js ✓ divides 10 by 2 to equal 5 (2 ms) ✓ divides 20 by 4 to equal 5 (1 ms) ✓ divides 100 by 10 to equal 10 (1 ms) ✓ throws error when dividing by zero (3 ms) ✓ throws error when dividing zero by zero (1 ms) Test Suites: 1 passed, 1 total Tests: 5 passed, 5 total Snapshots: 0 total Time: 0.6 s Verify coverage: npm run test:coverage The coverage should still be 100% as all code paths are tested. Now we'll learn a critical lesson: 100% statement coverage does NOT guarantee fully tested code. Update divide.js with conditional logic: Add a feature to handle negative numbers by returning the absolute value: function divide(a, b) { if (b === 0) { throw new Error('Cannot divide by zero'); } if (a { describe('positive numbers', () => { test('divides 10 by 2 to equal 5', () => { expect(divide(10, 2)).toBe(5); }); test('divides 20 by 4 to equal 5', () => { expect(divide(20, 4)).toBe(5); }); test('divides 100 by 10 to equal 10', () => { expect(divide(100, 10)).toBe(10); }); }); describe('negative numbers (returns absolute value)', () => { test('divides negative dividend by positive divisor', () => { // Arrange const dividend = -10; const divisor = 2; // Act const result = divide(dividend, divisor); // Assert expect(result).toBe(5); // |-10 / 2| = 5 }); test('divides positive dividend by negative divisor', () => { expect(divide(10, -2)).toBe(5); // |10 / -2| = 5 }); test('divides two negative numbers', () => { expect(divide(-10, -2)).toBe(5); // |-10 / -2| = 5 }); test('divides negative by negative with different result', () => { expect(divide(-20, -4)).toBe(5); // |-20 / -4| = 5 }); }); describe('error handling', () => { test('throws error when dividing by zero', () => { expect(() => divide(10, 0)).toThrow('Cannot divide by zero'); }); test('throws error when dividing zero by zero', () => { expect(() => divide(0, 0)).toThrow('Cannot divide by zero'); }); test('throws error when dividing negative by zero', () => { expect(() => divide(-10, 0)).toThrow('Cannot divide by zero'); }); }); }); Run the complete test suite: npm test Expected output: PASS ./divide.test.js divide function positive numbers ✓ divides 10 by 2 to equal 5 (2 ms) ✓ divides 20 by 4 to equal 5 (1 ms) ✓ divides 100 by 10 to equal 10 (1 ms) negative numbers (returns absolute value) ✓ divides negative dividend by positive divisor (1 ms) ✓ divides positive dividend by negative divisor (1 ms) ✓ divides two negative numbers (1 ms) ✓ divides negative by negative with different result (1 ms) error handling ✓ throws error when dividing by zero (3 ms) ✓ throws error when dividing zero by zero (1 ms) ✓ throws error when dividing negative by zero (1 ms) Test Suites: 1 passed, 1 total Tests: 10 passed, 10 total Snapshots: 0 total Time: 0.8 s Generate final coverage report: npm run test:coverage Now you should see: -----------------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -----------------|---------|----------|---------|---------|------------------- All files | 100 | 100 | 100 | 100 | divide.js | 100 | 100 | 100 | 100 | -----------------|---------|----------|---------|---------|------------------- Perfect! 100% coverage across all metrics! Check the HTML report: Open coverage/lcov-report/index.html: - All lines should be highlighted in green No yellow or red indicators All branches fully tested Jest Documentation: https://jestjs.io/docs/getting-started Node.js Documentation: https://nodejs.org/en/docs/ Jest Expect API: https://jestjs.io/docs/expect Jest Coverage: https://jestjs.io/docs/cli#--coverageboolean
