๐Ÿ  Home ๐Ÿ”’ Record Sharing ⚙ Apex Triggers ๐Ÿ” SOQL ๐Ÿ’ป LWC ๐Ÿ”— Integration ๐Ÿค– Flows & Automation ๐Ÿค– Agentforce & AI ๐ŸŽˆ Agentforce Course — Free ☁ Data Cloud ๐ŸŽ“ DC Course — Free ๐Ÿš€ DevOps Course — Free ๐Ÿ’ต CPQ ๐ŸŽฏ 100 Scenario Questions ๐Ÿ† 150 Advanced Questions ๐Ÿ“ง Marketing Cloud ๐ŸŽค Mock Interview Community ๐Ÿ—️ Company Wise ๐Ÿ‘ฅ About Us Start Learning Free →

LWC Zero to Hero Module 13 — Testing with Jest

๐Ÿ“…  Hero To Zero LWC
LWC Zero to Hero - Module 13: Testing with Jest | SF Interview Pro
๐Ÿงช LWC Zero to Hero — Module 13 of 15

Testing with Jest

Module 12 made your components fast. This module makes sure they stay CORRECT as your codebase grows, without manually re-testing everything by hand every time.

Module 13 of 15 (counting Module 0 as a bonus prerequisite) · Phase 5: Advanced & Capstone
๐ŸŽฏ What You'll Master in This Module
Jest is a JavaScript testing framework, and sfdx-lwc-jest is Salesforce's specific adapter that lets Jest understand and test LWC components. Automated tests catch regressions immediately when you change code, giving you the confidence to refactor or extend a component without manually re-clicking through every scenario by hand each time.
Why automated component testing matters, beyond just "it is good practice"
Setting up sfdx-lwc-jest and the standard test file conventions
Writing your first real component test from scratch
Testing user interactions like clicks and verifying resulting state changes
Mocking @wire adapters to test components without a real Salesforce org
Mocking imperative Apex calls, including both success and error paths
Testing best practices, and recognizing what is not worth testing
Concept 1 of 7
Why Test LWC Components
A component that works correctly today can silently break in the future when someone (possibly you) modifies a shared utility, refactors a getter, or changes an unrelated part of the codebase. Automated tests catch these regressions IMMEDIATELY, the moment they happen, instead of weeks later when a user reports a bug.
⚡ Why This Matters
Recall Module 12's "measure before you optimize" principle — testing is the correctness equivalent: "verify before you assume it still works," rather than trusting that nothing broke just because you did not notice anything obviously wrong.
THE CONFIDENCE THIS GIVES YOU
// Without tests: "I changed this getter, I THINK everything still works..."
// (requires manually re-clicking through every scenario to verify)

// With tests: run the test suite, get an immediate pass/fail answer
// for every scenario someone already wrote a test for, in seconds
// npm run test:unit
๐Ÿ› ️ Practical Mini-Implementation
Tests pay off most dramatically during REFACTORING — changing HOW something works without changing WHAT it does. A solid test suite means you can confidently restructure a component's internals (say, converting a getter to the setter pattern from Module 12) and know immediately if you broke anything observable.
⚠️ Common Gotcha
Tests written AFTER a bug is already found and fixed are still valuable — they specifically prevent that exact bug from silently returning later (a "regression test"). Do not feel like testing is only worthwhile if done perfectly upfront; adding tests incrementally, including for bugs you have already fixed, is a normal and valuable practice.
Concept 2 of 7
Setting Up sfdx-lwc-jest
sfdx-lwc-jest is Salesforce's official wrapper around the popular Jest testing framework, pre-configured to understand LWC's component structure, decorators, and module imports. It is typically already included in modern Salesforce DX projects, or added as a development dependency.
⚡ Why This Matters
Plain Jest does not understand LWC-specific syntax like @api/@wire decorators or the lightning/... module imports out of the box — sfdx-lwc-jest provides the necessary configuration and mocks to make testing actual LWC components possible at all.
STANDARD PROJECT STRUCTURE — the __tests__ folder convention
orderCard/
├── orderCard.html
├── orderCard.js
├── orderCard.js-meta.xml
└── __tests__/
    └── orderCard.test.js   <-- test file, same name + .test.js suffix

// Running tests (typically configured in package.json scripts):
// npm run test:unit
THE BASIC TEST FILE SKELETON
import { createElement } from 'lwc';
import OrderCard from 'c/orderCard';

describe('c-order-card', () => {
  afterEach(() => {
    // clean up the DOM after every test — covered fully in Concept 3
    while (document.body.firstChild) {
      document.body.removeChild(document.body.firstChild);
    }
  });

  it('does something testable', () => {
    // individual test cases go here — Concept 3 builds out a real one
  });
});
๐Ÿ› ️ Practical Mini-Implementation
describe() groups related tests together (usually one per component), and it() defines an individual test case with a human-readable description. This structure is standard across Jest and most JavaScript testing frameworks, so this pattern transfers directly to other JS testing contexts beyond just LWC.
⚠️ Common Gotcha
The test file MUST live inside a folder literally named __tests__ (with double underscores on both sides) directly inside the component's bundle folder — this exact naming convention is required for sfdx-lwc-jest to automatically discover and run the tests. A typo in this folder name means your tests simply will not be found or run at all.
Concept 3 of 7
Writing Your First Component Test
Every LWC test follows the same basic shape: create an instance of the component using createElement, attach it to document.body so it actually renders, then inspect the resulting DOM using querySelector to verify it shows what you expect.
⚡ Why This Matters
This pattern — create, attach, inspect — is the foundation every other testing concept in this module builds on. Once this feels natural, testing interactions and mocking data are just variations on this same core shape.
A COMPLETE, REAL FIRST TEST
import { createElement } from 'lwc';
import OrderCard from 'c/orderCard';

describe('c-order-card', () => {
  afterEach(() => {
    while (document.body.firstChild) {
      document.body.removeChild(document.body.firstChild);
    }
  });

  it('displays the order name passed via the orderName property', () => {
    // CREATE
    const element = createElement('c-order-card', { is: OrderCard });
    element.orderName = 'Order #1001';  // setting an @api property directly

    // ATTACH
    document.body.appendChild(element);

    // INSPECT
    const heading = element.shadowRoot.querySelector('h2');
    expect(heading.textContent).toBe('Order #1001');
  });
});
๐Ÿ› ️ Practical Mini-Implementation
Notice element.shadowRoot.querySelector(...) — NOT just element.querySelector(...). Because LWC uses Shadow DOM (briefly mentioned in earlier modules), the component's actual rendered content lives inside its shadowRoot, and tests must query through that specifically to find rendered elements.
⚠️ Common Gotcha
Forgetting the afterEach cleanup block means components from previous tests remain attached to document.body, potentially causing later tests to accidentally find and interact with the WRONG component instance, or simply causing memory buildup across a large test suite. Always clean up after every single test.
Concept 4 of 7
Testing User Interactions
Testing a click means finding the relevant element via querySelector, firing a synthetic click event on it, waiting for LWC to process the resulting changes, and then verifying the component reached the expected new state.
⚡ Why This Matters
Recall Module 6's custom events and Module 8's button-driven actions — these are exactly the kinds of behaviors most worth testing, since they represent real user-facing functionality, not just internal implementation details.
TESTING A BUTTON CLICK UPDATES COMPONENT STATE
it('increments the counter when the button is clicked', async () => {
  const element = createElement('c-counter', { is: Counter });
  document.body.appendChild(element);

  const button = element.shadowRoot.querySelector('button');
  button.click();  // fires a real click event on the rendered button

  await Promise.resolve();  // wait one microtask for LWC to process the re-render

  const countDisplay = element.shadowRoot.querySelector('.count');
  expect(countDisplay.textContent).toBe('1');
});
TESTING THAT A CUSTOM EVENT FIRES (Module 6)
it('dispatches an orderselected event with the correct detail', () => {
  const element = createElement('c-order-row', { is: OrderRow });
  document.body.appendChild(element);

  const handler = jest.fn();  // a mock function to spy on the event
  element.addEventListener('orderselected', handler);

  element.shadowRoot.querySelector('button').click();

  expect(handler).toHaveBeenCalled();
  expect(handler.mock.calls[0][0].detail.orderId).toBe('001xx');
});
๐Ÿ› ️ Practical Mini-Implementation
jest.fn() creates a "mock function" specifically designed for testing — it records every call made to it (arguments, call count), letting you assert on HOW it was called, exactly what you need to verify a custom event's detail payload was correct.
⚠️ Common Gotcha
Forgetting await Promise.resolve() (or an equivalent small delay) after triggering an action that causes a re-render means your assertion may run BEFORE LWC has actually finished updating the DOM, causing a test to fail not because the code is wrong, but because the test checked too early.
Concept 5 of 7
Mocking @wire Adapters
Tests run in a simulated environment with no real Salesforce org behind them — so wired data must be MOCKED (faked) rather than actually fetched. sfdx-lwc-jest provides specific utilities for this, letting you simulate a wire adapter emitting data or an error.
⚡ Why This Matters
Without mocking, any component using Module 9's @wire patterns would be untestable — there is no real org to fetch from during a Jest test run, so the test environment must simulate the wire service's behavior instead.
MOCKING A CUSTOM APEX WIRE ADAPTER
// __tests__/orderList.test.js
import { createElement } from 'lwc';
import OrderList from 'c/orderList';
import getOrdersForAccount from '@salesforce/apex/OrderController.getOrdersForAccount';

// registerApexTestWireAdapter is provided by sfdx-lwc-jest's testUtils
import { registerApexTestWireAdapter } from '@salesforce/sfdx-lwc-jest/src/lwc-jest-extensions';

const getOrdersWireAdapter = registerApexTestWireAdapter(getOrdersForAccount);

it('displays orders when wire data is emitted', async () => {
  const element = createElement('c-order-list', { is: OrderList });
  document.body.appendChild(element);

  // simulate the wire service delivering mock data
  getOrdersWireAdapter.emit([{ Id: '001', Name: 'Order A' }]);

  await Promise.resolve();

  const rows = element.shadowRoot.querySelectorAll('.order-row');
  expect(rows.length).toBe(1);
});
๐Ÿ› ️ Practical Mini-Implementation
The same .emit() pattern can simulate an ERROR state too (often by emitting an object shaped like { error: {...} }) — meaning you can test both the success path AND Module 9's error-handling branch, without needing a real failing Apex call to actually occur.
⚠️ Common Gotcha
The exact import path and utility names for wire adapter mocking have evolved across different sfdx-lwc-jest versions — always check your specific project's existing test examples or current Salesforce documentation for the exact current syntax, rather than assuming one fixed API surface never changes.
Concept 6 of 7
Mocking Imperative Apex Calls
For imperative calls (Module 10), the standard Jest approach is jest.mock() on the Apex import path itself, replacing the real function with a controllable fake one whose resolved or rejected value you fully control in each test.
⚡ Why This Matters
This lets you test BOTH success and failure paths of your Module 10 try/catch logic deterministically — you decide exactly what the "Apex call" returns or throws in each specific test, rather than depending on real server behavior.
MOCKING SUCCESS AND FAILURE FOR AN IMPERATIVE CALL
import { createElement } from 'lwc';
import OrderForm from 'c/orderForm';
import saveOrder from '@salesforce/apex/OrderController.saveOrder';

jest.mock('@salesforce/apex/OrderController.saveOrder', () => ({
  default: jest.fn()
}), { virtual: true });

it('shows success state when saveOrder resolves', async () => {
  saveOrder.mockResolvedValue({ Id: '001', Name: 'Saved Order' });

  const element = createElement('c-order-form', { is: OrderForm });
  document.body.appendChild(element);
  element.shadowRoot.querySelector('lightning-button').click();

  await Promise.resolve();
  expect(element.shadowRoot.querySelector('.success-message')).not.toBeNull();
});

it('shows error state when saveOrder rejects', async () => {
  saveOrder.mockRejectedValue({ body: { message: 'Validation failed' } });

  const element = createElement('c-order-form', { is: OrderForm });
  document.body.appendChild(element);
  element.shadowRoot.querySelector('lightning-button').click();

  await Promise.resolve();
  expect(element.shadowRoot.querySelector('.error-message').textContent).toContain('Validation failed');
});
๐Ÿ› ️ Practical Mini-Implementation
Testing the ERROR path (mockRejectedValue) is just as important as testing success, and it is something genuinely hard to reliably trigger against a real org on demand — mocking makes previously-difficult-to-test error scenarios from Module 10's try/catch logic straightforward to verify.
⚠️ Common Gotcha
The { virtual: true } option in jest.mock() is required specifically because the @salesforce/apex/... module does not actually exist as a real file Jest can resolve normally — it is a special Salesforce-specific import that only has meaning when deployed to an actual org. Forgetting virtual: true typically causes a "module not found" error during test execution.
Concept 7 of 7
Testing Best Practices & What NOT to Test
Good tests verify BEHAVIOR (what the user observes — text displayed, events fired, buttons enabled/disabled) rather than IMPLEMENTATION details (exact internal variable names, private helper method calls) — implementation can change during refactoring without breaking correct behavior, and good tests should not break just because of that kind of internal change.
⚡ Why This Matters
Tests overly tied to implementation details become a maintenance burden, breaking on every refactor even when behavior is unchanged — defeating the confidence-giving purpose testing is supposed to provide in the first place.
❌ TOO TIED TO IMPLEMENTATION — fragile, breaks on harmless refactors
// Testing an internal private property directly, rather than observable output
expect(element._internalCacheFlag).toBe(true);  // breaks if you rename this internal detail
✅ TESTING OBSERVABLE BEHAVIOR — robust to internal refactors
// Testing what the USER would actually observe
expect(element.shadowRoot.querySelector('.loading-spinner')).toBeNull();
// This survives ANY internal refactor, as long as the spinner correctly hides when expected
๐Ÿ› ️ Practical Mini-Implementation
A useful guiding question when writing any test: "would this test still pass if I completely rewrote the component's internals, but the user-facing behavior stayed identical?" If yes, you are testing behavior correctly. If the test would break purely from an internal rename or restructure, it may be too tightly coupled to implementation.
⚠️ Common Gotcha — Module Wrap-Up
Not everything needs a test — trivial getters with zero logic, or simple pass-through @api properties, rarely justify dedicated tests. Focus testing effort on genuine logic: conditional rendering decisions, event handling, data transformations, and error handling — the places where bugs actually tend to hide. Module 14 covers Security and Deployment, the final technical topics before the full Capstone Project in Module 15.
๐Ÿ’ฌ Module 13 Interview Questions (6)
Q1Why must a test query through element.shadowRoot rather than calling querySelector directly on the element itself?
Lightning Web Components use Shadow DOM, which encapsulates a component's internal rendered markup within its own shadow root rather than exposing it directly as children of the component element in the regular DOM tree. Calling querySelector directly on the component element searches the regular light DOM, where the component's actual rendered content is not present, while calling it on element.shadowRoot specifically searches within the component's encapsulated shadow tree, where its rendered template content actually lives, making this the correct way to access and verify what a component has rendered.
"LWC components encapsulate their rendered content inside a shadow root due to Shadow DOM, so tests must query through element.shadowRoot specifically, since querying the element directly searches the regular DOM where the component's actual rendered markup is not present."
Q2A test clicks a button and immediately asserts on the resulting DOM state, but the assertion fails even though the underlying logic is correct. What is a likely cause, and how would you fix it?
A likely cause is that the assertion ran before LWC had actually finished processing the resulting re-render triggered by the click, since DOM updates following a state change are not necessarily synchronous and immediate within the test's execution flow. The fix is typically to await a resolved Promise (such as await Promise.resolve()) or another appropriate async waiting mechanism immediately after triggering the click and before making the assertion, giving the rendering engine the opportunity to complete its update cycle before the test checks the resulting DOM state.
"The assertion likely ran before the re-render triggered by the click had actually completed; awaiting a resolved Promise immediately after the click, before the assertion, gives the rendering engine time to finish updating the DOM."
Q3Why can't a Jest test simply call a real @wire-connected Apex method and expect actual data back, and what is the general solution to this problem?
Jest tests run in a simulated JavaScript environment with no actual connection to a real Salesforce org, meaning there is no genuine Apex runtime available to execute a real method and return real query results during a test run. The general solution is mocking — using sfdx-lwc-jest's provided utilities to register a test wire adapter that can simulate emitting specific mock data (or a mock error) on demand, allowing the test to verify how the component behaves in response to specific simulated wire results without requiring any real org connectivity.
"Jest tests have no real Salesforce org connection, so wired Apex methods cannot actually execute; the solution is mocking, using sfdx-lwc-jest's test wire adapter utilities to simulate specific data or error results being emitted, without needing real org connectivity."
Q4What does the virtual:true option do when calling jest.mock() on an imported Apex method path, and why is it specifically necessary in this context?
The virtual:true option tells Jest that the module being mocked does not need to actually exist as a real, resolvable file on disk, which is necessary specifically because @salesforce/apex/... import paths are a special Salesforce-specific construct that only has real meaning when a component is deployed into an actual Salesforce org, and no such file genuinely exists in the local project structure during a Jest test run. Without virtual:true, Jest would attempt to resolve this import path as a real file and typically fail with a module-not-found error, since it has no corresponding file to actually locate.
"virtual:true tells Jest the mocked module does not need to exist as a real file, which is necessary because @salesforce/apex import paths only have meaning when deployed to an actual org and correspond to no real local file Jest could otherwise resolve."
Q5Explain the difference between testing observable behavior and testing implementation details, and why the distinction matters for long-term test maintenance.
Testing observable behavior means verifying what an end user would actually perceive or experience, such as text displayed on screen, whether a button is enabled or disabled, or whether a specific event was dispatched, while testing implementation details means verifying internal, non-user-facing specifics such exact private variable names or internal helper method invocations that have no direct external observable effect. This distinction matters significantly for long-term maintenance because implementation details frequently change during legitimate refactoring even when the actual user-facing behavior remains completely unchanged, meaning tests tied too closely to implementation details will break unnecessarily on harmless refactors, creating ongoing maintenance burden and eroding the confidence that a passing test suite is supposed to provide.
"Testing observable behavior verifies what users actually experience (displayed text, event dispatches, enabled states), while testing implementation details verifies internal non-user-facing specifics; behavior-focused tests remain stable through legitimate refactors, while implementation-tied tests break unnecessarily and create ongoing maintenance burden."
Q6A developer writes a dedicated test asserting on the exact return value of a trivial getter that simply returns this.isLoading directly with no additional logic. Is this generally a worthwhile test to maintain, and why or why not?
This is generally not considered a particularly worthwhile dedicated test on its own, since a getter with zero actual logic beyond directly returning an existing property's value introduces essentially no risk of a meaningful bug, making a dedicated test for it provide very little real protective value relative to the ongoing maintenance cost of keeping that test updated over time. Testing effort is generally better focused on components and logic where actual bugs are likely to occur, such as conditional rendering decisions based on multiple combined conditions, event handling logic, data transformations, and error handling paths, rather than on trivial pass-through code with no meaningful logic of its own.
"A dedicated test for a trivial getter with no real logic provides little protective value relative to its maintenance cost; testing effort is better focused on genuine logic like conditional rendering, event handling, data transformations, and error handling, where actual bugs are far more likely to occur."
๐Ÿ“ Module 13 Recap — Testing with Jest Mastered
✅ Automated tests catch regressions immediately, giving confidence during refactoring
✅ sfdx-lwc-jest tests live in a __tests__ folder, following createElement → attach → inspect
✅ Always query through element.shadowRoot, since LWC encapsulates rendered content there
✅ Testing interactions requires awaiting a microtask (Promise.resolve()) before asserting on the resulting DOM
✅ Mock @wire adapters using sfdx-lwc-jest's test wire adapter utilities to simulate data/error states
✅ Mock imperative Apex calls with jest.mock() + { virtual: true }, using mockResolvedValue/mockRejectedValue
✅ Test observable behavior, not implementation details — and skip dedicated tests for genuinely trivial code
๐ŸŽฏ Before Moving to Module 14...
Try this: take the complete Delete flow component from Module 10, Concept 6, and write a full test suite for it — test the modal-confirmed deletion path (mocking the Apex delete call to resolve successfully), the modal-cancelled path (verifying no Apex call happens), and the error path (mocking a rejected Apex call and verifying the error toast logic). This single exercise touches every technique from this module. Module 14 covers Security (Lightning Web Security) and Deployment — the final technical topics before the full Capstone Project.
☕ Enjoyed this article?
SF Interview Pro is 100% free and maintained by a Salesforce professional. No ads, no paywalls, and no signup required. If this guide helped you prepare for an interview, earn a certification, or grow your Salesforce career, consider buying me a coffee! ☕๐Ÿ’œ
๐Ÿ‡ฎ๐Ÿ‡ณ UPI (India)
UPI QR Code to support sfinterviewpro
Pay by QR
GPay · PhonePe · Paytm · BHIM
๐ŸŒŽ International
PayPal QR Code to support sfinterviewpro
Scan or tap to pay