JavaScript 67 🧬 Testing
Testing is how you prove your code works — and how you keep it working as it changes. Without tests, every edit is a gamble. With tests, you can refactor confidently, catch regressions immediately, and document what the code is supposed to do. JavaScript has a mature testing ecosystem, and the concepts are the same across frameworks.
The core idea is simple: run a piece of code with known input, check that the output matches what you expect, and report the result. Everything else — test runners, assertions, mocking, coverage — is built on top of that one idea.
Key point: A test is a small piece of code that checks another piece of code and fails loudly when the behavior is wrong. The value is not in the test itself but in the safety net it gives you to change code without fear.
a – Types of tests
Tests come in different sizes and scopes. The names vary between teams, but the categories are consistent.
Unit tests — test a single function or module in isolation. Fast, focused, and the majority of your test suite. If add(2, 3) doesn’t return 5, the unit test catches it.
Integration tests — test how two or more units work together. A function that queries a database, or a component that talks to an API. Slower than unit tests, but catch issues that isolated tests miss.
End-to-end (E2E) tests — drive the actual application like a user would: open a browser, click a button, verify the result. Slow and brittle, but they prove the whole system works.
The testing pyramid:
▲
/E\ few
/2E2\ slow
/-----\ expensive
/integr-\ some
/-ation--\ medium
/----------\ medium
/ unit \ many
/--------------\ fast
cheap
Most teams write many unit tests, some integration tests, and few E2E tests. The proportions matter — too many E2E tests and the suite becomes slow and flaky.
Test-driven development (TDD):
A workflow where you write the test before the code. The cycle is:
- Red — write a failing test
- Green — write the minimum code to make it pass
- Refactor — clean up without breaking the test
TDD isn’t required, but it forces you to think about the interface first and guarantees every line of code has a test.
Behavior vs implementation:
Good tests check behavior — what the code does. Bad tests check implementation — how it does it. Behavior tests survive refactoring; implementation tests break on every change.
// ✅ Behavior — survives refactoring
expect(sum([1, 2, 3])).toBe(6);
// ❌ Implementation — breaks if internals change
expect(sum.toString()).toContain('reduce');
What to test:
- Public functions and APIs
- Edge cases (empty input, null, negative numbers)
- Error conditions
- Boundary values (0, max, min)
- Integration points
- User-facing behavior
What not to test:
- Third-party libraries (they have their own tests)
- Getters and setters with no logic
- Trivial code that can’t break
- Implementation details that will change
b – Writing tests
Most JavaScript testing follows the same pattern regardless of framework: describe, arrange, act, assert.
The anatomy of a test:
describe('add', () => {
it('adds two numbers', () => {
// Arrange
const a = 2;
const b = 3;
// Act
const result = add(a, b);
// Assert
expect(result).toBe(5);
});
});
- describe groups related tests
- it (or test) declares a single test case
- expect asserts a condition
- The test passes if no assertion throws
Common assertions:
| Assertion | Checks |
|---|---|
expect(x).toBe(y) | Strict equality |
expect(x).toEqual(y) | Deep equality |
expect(x).toBeTruthy() | Truthy |
expect(x).toBeFalsy() | Falsy |
expect(x).toBeNull() | null |
expect(x).toBeUndefined() | undefined |
expect(x).toContain(y) | Contains value |
expect(x).toHaveLength(n) | Length |
expect(fn).toThrow() | Throws |
expect(fn).toThrow('message') | Throws with message |
A real test file:
// math.js
export function add(a, b) { return a + b; }
export function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
// math.test.js
import { add, divide } from './math.js';
describe('add', () => {
it('adds positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
it('adds negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
it('adds zero', () => {
expect(add(5, 0)).toBe(5);
});
});
describe('divide', () => {
it('divides positive numbers', () => {
expect(divide(10, 2)).toBe(5);
});
it('throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
Each test covers one behavior. Together they document what the functions do and guard against regressions.
Setup and teardown:
beforeEach, afterEach, beforeAll, afterAll run code at specific points in the test lifecycle.
let db;
beforeEach(async () => {
db = await connect();
});
afterEach(async () => {
await db.close();
});
it('finds a user', async () => {
const user = await db.findUser(1);
expect(user.name).toBe('Alice');
});
Use these for shared setup — connections, fixtures, mocks — not for tests themselves.
Async tests:
Most test runners handle Promises and async/await natively:
it('fetches user data', async () => {
const user = await getUser(1);
expect(user.name).toBe('Alice');
});
it('rejects on error', async () => {
await expect(fetchUser(-1)).rejects.toThrow('Not found');
});
Return the Promise or await it. If you don’t, the test finishes before the assertion runs and passes falsely.
Testing errors:
it('throws on invalid input', () => {
expect(() => parse('bad')).toThrow(TypeError);
});
it('async throws', async () => {
await expect(fetchData('bad')).rejects.toThrow();
});
Always test error paths — they’re the ones most likely to break silently.
Parameterized tests:
When the same logic applies to many inputs, use a data table:
it.each([
[1, 2, 3],
[0, 0, 0],
[-1, 1, 0],
[100, 200, 300],
])('adds %i + %i = %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
One test declaration covers all cases. Failures clearly identify which input failed.
Common test runners:
| Runner | Notes |
|---|---|
| Jest | Most popular, all-in-one |
| Vitest | Fast, Vite-native |
| Mocha | Flexible, needs assertion library |
| Node’s test runner | Built-in since Node 18 |
Assertion libraries:
| Library | Notes |
|---|---|
Jest’s expect | Built in |
| Chai | Classic, flexible |
Node’s assert | Built-in |
Vitest’s expect | Jest-compatible |
Test file conventions:
*.test.js— unit tests*.spec.js— alternative naming__tests__/— folder for tests*.e2e.js— end-to-end tests
Running tests:
npm test
npx jest
npx vitest
node --test
The output tells you which tests passed, which failed, and why.
c – Mocking and coverage
Real code touches the outside world — APIs, files, time, randomness. Tests need to control those dependencies to be reliable. That’s what mocking does.
Why mock:
- Speed — no network calls
- Determinism — same result every run
- Isolation — test one unit at a time
- Simulation — test error paths that are hard to trigger
Mocking a function:
const mock = jest.fn();
mock('hello');
expect(mock).toHaveBeenCalled();
expect(mock).toHaveBeenCalledWith('hello');
expect(mock).toHaveBeenCalledTimes(1);
jest.fn() creates a spy that records calls. Useful for callbacks and injected dependencies.
Mocking return values:
const mock = jest.fn()
.mockReturnValue(42)
.mockReturnValueOnce(1)
.mockReturnValueOnce(2);
mock(); // 1
mock(); // 2
mock(); // 42
mockReturnValue sets a default; mockReturnValueOnce sets a one-time value.
Mocking a module:
jest.mock('./api', () => ({
fetchUser: jest.fn().mockResolvedValue({ name: 'Alice' })
}));
import { fetchUser } from './api';
it('loads the user', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
});
The entire module is replaced. The real fetchUser never runs.
Mocking async functions:
const fetchData = jest.fn()
.mockResolvedValue({ data: 'ok' })
.mockRejectedValueOnce(new Error('fail'));
mockResolvedValue returns a resolved Promise; mockRejectedValue rejects.
Spying on existing methods:
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
console.log('test');
expect(spy).toHaveBeenCalledWith('test');
spy.mockRestore();
spyOn replaces a method temporarily. mockRestore puts the original back.
Mocking time:
jest.useFakeTimers();
setTimeout(() => console.log('done'), 1000);
jest.advanceTimersByTime(1000);
// [ done ]
Fake timers let you control setTimeout, setInterval, and Date.now(). Essential for testing debounce, throttle, and polling.
Mocking random:
jest.spyOn(Math, 'random').mockReturnValue(0.5);
Any non-deterministic source can be mocked for reproducible tests.
What to mock:
- Network requests
- File system operations
- Database calls
- Time and dates
- Random numbers
- Third-party services
- Expensive computations
What not to mock:
- The code under test
- Pure functions
- Trivial utilities
- Anything you want to actually verify
Over-mocking is a smell. If a test mocks five things to check one behavior, the design is probably wrong — dependencies should be injectable.
Test coverage:
Coverage measures what percentage of your code runs during tests. It’s a useful signal, not a goal.
| Metric | Meaning |
|---|---|
| Statements | % of statements executed |
| Branches | % of if/else paths taken |
| Functions | % of functions called |
| Lines | % of lines executed |
npx jest --coverage
Output:
File | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
math.js | 100 | 100 | 100 | 100 |
api.js | 80 | 60 | 90 | 80 |
The 100% trap:
Chasing 100% coverage leads to bad tests — assertions with no value, tests that exercise lines without verifying behavior. 80% coverage of meaningful code beats 100% coverage of noise.
Coverage tells you what’s not tested. It doesn’t tell you what is tested well.
Integration tests:
Some behavior only shows up when components interact. Integration tests run real code with real (or lightly mocked) dependencies.
it('creates a user in the database', async () => {
const user = await createUser({ name: 'Alice' });
const found = await db.findUser(user.id);
expect(found.name).toBe('Alice');
});
The real database is used — often a test database, cleaned between runs.
E2E tests:
Tools like Playwright, Cypress, and Puppeteer drive a real browser:
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.fill('#username', 'alice');
await page.fill('#password', 'secret');
await page.click('button[type=submit]');
await expect(page.locator('h1')).toHaveText('Dashboard');
});
E2E tests prove the whole stack works — but they’re slow and can be flaky. Keep them small in number and focused on critical paths.
The test pyramid in practice:
| Level | Count | Speed | Reliability |
|---|---|---|---|
| Unit | Many | ms | High |
| Integration | Some | 100ms–1s | Medium |
| E2E | Few | seconds | Lower |
A healthy test suite:
- Runs in seconds
- Fails clearly when something breaks
- Doesn’t flake on CI
- Covers critical paths
- Doesn’t mock everything
CI integration:
Tests should run automatically on every push:
# .github/workflows/test.yml
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm install
- run: npm test
A failing test blocks the merge. This is what makes tests valuable — they run before bugs reach production.
Complete Example Session
// ============================================
// PART 1: SIMPLE UNIT TEST
// ============================================
function add(a, b) { return a + b; }
describe('add', () => {
it('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
});
// ============================================
// PART 2: MULTIPLE CASES
// ============================================
describe('add', () => {
it('adds positive', () => expect(add(2, 3)).toBe(5));
it('adds negative', () => expect(add(-1, -1)).toBe(-2));
it('adds zero', () => expect(add(5, 0)).toBe(5));
});
// ============================================
// PART 3: TESTING ERRORS
// ============================================
function divide(a, b) {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
it('throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
// ============================================
// PART 4: ASYNC TEST
// ============================================
async function fetchUser(id) {
return { id, name: 'Alice' };
}
it('fetches a user', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
});
// ============================================
// PART 5: ASYNC REJECTION
// ============================================
async function failFetch() {
throw new Error('Network error');
}
it('rejects on failure', async () => {
await expect(failFetch()).rejects.toThrow('Network error');
});
// ============================================
// PART 6: PARAMETERIZED
// ============================================
it.each([
[1, 2, 3],
[0, 0, 0],
[-1, 1, 0],
])('adds %i + %i = %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
// ============================================
// PART 7: SETUP AND TEARDOWN
// ============================================
let users;
beforeEach(() => {
users = [{ id: 1, name: 'Alice' }];
});
afterEach(() => {
users = null;
});
it('finds a user', () => {
expect(users.find(u => u.id === 1).name).toBe('Alice');
});
// ============================================
// PART 8: MOCK FUNCTION
// ============================================
const callback = jest.fn();
callback('hello');
expect(callback).toHaveBeenCalledWith('hello');
// ============================================
// PART 9: MOCK RETURN
// ============================================
const getValue = jest.fn()
.mockReturnValue(42)
.mockReturnValueOnce(1);
expect(getValue()).toBe(1);
expect(getValue()).toBe(42);
// ============================================
// PART 10: MOCK ASYNC
// ============================================
const fetchData = jest.fn()
.mockResolvedValue({ data: 'ok' })
.mockRejectedValueOnce(new Error('fail'));
await expect(fetchData()).rejects.toThrow('fail');
await expect(fetchData()).resolves.toEqual({ data: 'ok' });
// ============================================
// PART 11: MOCK MODULE
// ============================================
jest.mock('./api', () => ({
fetchUser: jest.fn().mockResolvedValue({ name: 'Alice' })
}));
// ============================================
// PART 12: SPY ON METHOD
// ============================================
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
console.log('hello');
expect(spy).toHaveBeenCalledWith('hello');
spy.mockRestore();
// ============================================
// PART 13: FAKE TIMERS
// ============================================
jest.useFakeTimers();
let called = false;
setTimeout(() => (called = true), 1000);
jest.advanceTimersByTime(1000);
expect(called).toBe(true);
jest.useRealTimers();
// ============================================
// PART 14: COVERAGE
// ============================================
// $ npx jest --coverage
// Generates a report showing which lines ran
// ============================================
// PART 15: E2E (Playwright)
// ============================================
// test('login', async ({ page }) => {
// await page.goto('/login');
// await page.fill('#username', 'alice');
// await page.click('button');
// await expect(page).toHaveURL('/dashboard');
// });
// ============================================
// PART 16: CI WORKFLOW
// ============================================
// .github/workflows/test.yml
// on: [push]
// jobs:
// test:
// runs-on: ubuntu-latest
// steps:
// - uses: actions/checkout@v4
// - run: npm install
// - run: npm test
Quick Reference
Test Structure
| Function | Purpose |
|---|---|
describe(name, fn) | Group tests |
it(name, fn) / test(...) | Single test |
beforeEach(fn) | Before each test |
afterEach(fn) | After each test |
beforeAll(fn) | Before all tests |
afterAll(fn) | After all tests |
Common Matchers
| Matcher | Checks |
|---|---|
.toBe(x) | Strict equality |
.toEqual(x) | Deep equality |
.toBeTruthy() | Truthy |
.toBeFalsy() | Falsy |
.toBeNull() | Null |
.toBeUndefined() | Undefined |
.toContain(x) | Contains |
.toHaveLength(n) | Length |
.toThrow() | Throws |
.toMatch(regex) | Regex match |
Async Matchers
| Matcher | Checks |
|---|---|
await expect(p).resolves.toBe(x) | Resolves |
await expect(p).rejects.toThrow(x) | Rejects |
Mocks
| Method | Purpose |
|---|---|
jest.fn() | Create mock |
.mockReturnValue(x) | Default return |
.mockReturnValueOnce(x) | One-time return |
.mockResolvedValue(x) | Resolved Promise |
.mockRejectedValue(x) | Rejected Promise |
.mockImplementation(fn) | Custom implementation |
jest.spyOn(obj, 'method') | Spy on method |
spy.mockRestore() | Restore original |
Assertions on Mocks
| Assertion | Checks |
|---|---|
.toHaveBeenCalled() | Called once or more |
.toHaveBeenCalledWith(...) | Called with args |
.toHaveBeenCalledTimes(n) | Called N times |
.toHaveBeenLastCalledWith(...) | Last call args |
Coverage Metrics
| Metric | Meaning |
|---|---|
| Statements | Lines executed |
| Branches | if/else paths |
| Functions | Functions called |
| Lines | Lines executed |
Test Types
| Type | Scope | Speed | Count |
|---|---|---|---|
| Unit | Single function | Fast | Many |
| Integration | Multiple units | Medium | Some |
| E2E | Whole app | Slow | Few |
Test Runners
| Tool | Notes |
|---|---|
| Jest | All-in-one |
| Vitest | Fast, Vite |
| Mocha | Flexible |
| Node test | Built-in |
E2E Tools
| Tool | Notes |
|---|---|
| Playwright | Modern, cross-browser |
| Cypress | Developer-friendly |
| Puppeteer | Chrome API |
Best Practices
✅ Do This:
// Test behavior, not implementation
expect(sum([1, 2, 3])).toBe(6); // ✅
// Test edge cases
expect(add(0, 0)).toBe(0);
expect(() => divide(1, 0)).toThrow(); // ✅
// Use parameterized tests for many inputs
it.each([[1, 1, 2], [2, 3, 5]])(...) // ✅
// Mock external dependencies
jest.mock('./api'); // ✅
// Restore mocks after use
spy.mockRestore(); // ✅
// Use fake timers for time-based code
jest.useFakeTimers(); // ✅
// Assert on meaningful outcomes
expect(user.role).toBe('admin'); // ✅
// Run tests in CI
on: [push] // ✅
// Keep tests fast
// Break slow tests into smaller ones // ✅
❌ Don’t Do This:
// Don't test implementation details
expect(sum.toString()).toContain('reduce'); // ❌
// Don't write tests without assertions
it('runs the function', () => {
add(2, 3); // ❌ no check
});
// Don't forget to await async
it('fetches', () => {
fetchUser(1).then(u => expect(u).toBeTruthy()); // ❌ returns immediately
});
// Don't mock everything
jest.mock('./utils'); // ⚠️ over-mocking
// Don't chase 100% coverage
// 80% meaningful > 100% trivial // ⚠️
// Don't test third-party libraries
expect(lodash.map(...)).toEqual(...) // ❌
// Don't share state between tests
let counter = 0; // ⚠️ use beforeEach
// Don't use real time in tests
await sleep(5000); // ⚠️ use fake timers
// Don't leave failing tests in main
it.skip('broken test'); // ❌ fix or delete
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Missing await | Test passes early | Await the Promise |
| No assertion | Test always passes | Add expect |
| Shared state | Tests affect each other | beforeEach reset |
| Over-mocking | Tests pass, code broken | Mock only externals |
| Testing implementation | Breaks on refactor | Test behavior |
| Slow tests | Suite takes minutes | Speed up or split |
| Flaky tests | Random failures | Remove randomness |
| No edge cases | Bugs slip through | Test boundaries |
| 100% coverage chase | Meaningless tests | Aim for meaningful |
| Mocks not restored | Leak between tests | mockRestore |
Real-World Examples
1. Basic unit test
it('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
One function, one assertion.
2. Testing an error path
it('throws on invalid input', () => {
expect(() => parse('bad')).toThrow('Invalid');
});
Errors are behaviors too — test them.
3. Async test
it('loads the user', async () => {
const user = await getUser(1);
expect(user.name).toBe('Alice');
});
await ensures the test finishes after the assertion.
4. Async rejection
await expect(fetchBad()).rejects.toThrow('Network');
The modern way to test rejections.
5. Parameterized
it.each([[1, 2, 3], [0, 0, 0]])('adds %i + %i', (a, b, r) => {
expect(add(a, b)).toBe(r);
});
One test, many cases.
6. Setup with beforeEach
beforeEach(() => {
db = createTestDb();
});
Fresh state for every test.
7. Mock function
const save = jest.fn();
save('data');
expect(save).toHaveBeenCalledWith('data');
Verifies the callback ran with the right arguments.
8. Mock return
const getId = jest.fn().mockReturnValue(42);
Replaces a real function with a controlled one.
9. Mock async
const fetchUser = jest.fn().mockResolvedValue({ name: 'Alice' });
No network call — instant, deterministic.
10. Spy on console
const spy = jest.spyOn(console, 'log').mockImplementation(() => {});
// ...test...
spy.mockRestore();
Verify side effects without printing noise.
11. Fake timers
jest.useFakeTimers();
setTimeout(fn, 1000);
jest.advanceTimersByTime(1000);
Control time — essential for debounce tests.
12. Coverage report
npx jest --coverage
See what’s untested — not what’s tested well.
13. E2E with Playwright
await page.goto('/login');
await page.fill('#user', 'alice');
await page.click('button');
await expect(page).toHaveURL('/dashboard');
Real browser — proves the whole stack.
14. CI workflow
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install && npm test
Tests run before merge — that’s the point.
Visual: The Testing Pyramid
▲
/E\ Few — slow — brittle
/2E2\
/-----\
/integr-\ Some — medium — valuable
/-ation--\
/----------\
/ unit \ Many — fast — focused
/--------------\
The base supports everything. If unit tests are weak, the pyramid collapses.
Visual: The TDD Cycle
┌─────────┐
│ RED │ Write failing test
└────┬────┘
│
▼
┌─────────┐
│ GREEN │ Write minimum code
└────┬────┘
│
▼
┌─────────┐
│REFACTOR │ Clean up
└────┬────┘
│
└──────► back to RED
Each iteration adds one behavior.
Visual: Mocking
┌──────────────────────────────────────────────┐
│ Without mocking │
│ │
│ Code ──► fetch() ──► network ──► response │
│ │
│ Slow, non-deterministic, needs network │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ With mocking │
│ │
│ Code ──► fetch() ──► mock ──► fake response │
│ │
│ Fast, deterministic, no network │
│ │
└──────────────────────────────────────────────┘
Visual: Test Lifecycle
┌──────────────────────────────────────────────┐
│ beforeAll │
│ │ │
│ ▼ │
│ ┌─── beforeEach ──► test 1 ──► afterEach ─┐ │
│ │ │ │
│ └─── beforeEach ──► test 2 ──► afterEach ─┘ │
│ │
│ ▼ │
│ afterAll │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Tool | Purpose |
|---|---|---|
| Unit test | it / test | One behavior |
| Assertion | expect | Check outcome |
| Group | describe | Organize tests |
| Setup | beforeEach | Fresh state |
| Teardown | afterEach | Cleanup |
| Mock | jest.fn() | Replace dependency |
| Spy | jest.spyOn | Watch method |
| Async | async / await | Handle Promises |
| Rejection | .rejects.toThrow | Async errors |
| Parameterized | it.each | Many inputs |
| Fake timers | jest.useFakeTimers | Control time |
| Coverage | --coverage | Untested lines |
| E2E | Playwright | Full user flow |
| CI | GitHub Actions | Auto-run tests |
Key takeaways:
- Tests prove behavior — not implementation
- Unit tests are fast, focused, and the majority of your suite
- Integration tests check how units work together
- E2E tests drive the real app — few, but they prove the whole stack
describe,it,expect— the shape of nearly every JavaScript test- Test edge cases — empty input, zero, negative, null, undefined
- Test error paths — they break silently otherwise
- Mock external dependencies — APIs, files, time, randomness
- Don’t over-mock — if you mock everything, you’re testing your mocks
- Always await async — or the test passes before the assertion runs
- Coverage is a signal — aim for meaningful, not 100%
- Run tests in CI — that’s where they earn their keep
- Fix or delete flaky tests — flaky tests erode trust
Remember: Tests are a safety net. They let you change code without fear, catch regressions instantly, and document what the code is supposed to do. Write behavior tests, not implementation tests. Mock the outside world, not your own code. Keep unit tests fast, integration tests meaningful, E2E tests focused. Chase coverage of critical paths, not a number. And run everything in CI — tests that don’t run aren’t tests. Master testing, and your code becomes something you can change.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!