SfSkills Domains Docs GitHub

Home / lwc / lwc-testing

lwc-testing

Jest unit tests for LWC — @salesforce/sfdx-lwc-jest, wire adapter mocks, DOM assertions. Triggers: LWC Jest, sfdx-lwc-jest, component test. NOT for Apex tests — use apex/test-class-standards. NOT for browser E2E — use devops/automated-regression-testing.

Open the skill folder on GitHub


Use this skill when the component works in the browser but the test suite does not prove it reliably. LWC testing is mostly about isolating behavior, mocking the right Salesforce boundaries, and waiting for rerenders intentionally instead of guessing at timing.


Before Starting

Gather this context before working on anything in this domain:


Questions to Ask Before Configuring

Ask these before writing the first it(). Each one traces to a failure in references/gotchas.md that a green suite will not tell you about.

AskWhy it mattersWhat a good answer adds
"Which of this component's inputs are known before mount, and which arrive after it?"Properties set before appendChild render synchronously; anything set after schedules an async rerenderThe exact list for Object.assign(element, …) versus the assignments that need an awaited microtask
"Every @salesforce/* module this component imports — name them."apex, label, schema, user/Id are compiler-generated with no file on disk; each needs { virtual: true } or a moduleNameMapper entry, and a label resolves to its path if left aloneThe mock block, and the config delta, before the first Cannot find module
"Is each Apex call wired or imperative?"A wire is driven by emit() on the imported adapter; an imperative import is a promise you resolve or rejectWhich of the two mocking shapes each import gets — mixing them produces tests that pass against a contract the component does not have
"What does the failure path render, and who dispatches nothing when it fires?"Suites that only emit success leave the catch block and the error branch at zero coverage while the report still looks healthyA named negative test per boundary, plus a not.toHaveBeenCalled() on the event that must not fire
"For every custom event: does it bubble, and does it cross the shadow boundary?"bubbles and composed both default to false, so a listener anywhere but the host element never firesWhere the listener attaches, and the detail shape to assert — not just that the handler was called
"Shadow DOM or light DOM?"Light DOM returns null from this.template, does not retarget events, and needs different queriesWhether the suite queries through shadowRoot at all, and what event.target will be
"What makes this suite go red — show me the mutation."A test that appends, never awaits, and asserts not.toBeNull() on a conditional region passes on both branchesOne deliberate break that proves the assertion reaches the DOM, before the suite is trusted as a gate

What a proper Jest harness adds over just writing tests: the suite fails for the reasons the component can actually fail, every mocked boundary is declared once instead of drifting per file, and a green run in CI means the error paths ran — not merely that nothing threw.


Core Concepts

Jest unit tests run in Node and use mocks instead of live Salesforce services. That means a good test verifies behavior at the component boundary, not whether the real platform is present. Most flaky tests fail because they mix production mental models with a mocked unit-test runtime.

Test Behavior, Not Private Implementation

The test should render the component, drive the public interaction, and assert visible behavior or emitted events. A test that knows too much about private fields or incidental DOM structure becomes brittle during harmless refactors.

Async Rerendering Must Be Awaited Intentionally

LWC rerendering is asynchronous. After setting properties, emitting wire values, or resolving promises, the test must wait for microtasks before asserting the DOM. Many "random" failures are simply assertions that run before the component has had a chance to rerender.

Wire And Imperative Apex Use Different Mocking Patterns

A wired Apex method or wire adapter should use the appropriate wire test utilities and emitted values. Imperative Apex calls should be mocked as module functions that resolve or reject promises. Mixing those patterns creates confusing tests that do not reflect the component's actual contract.

Base Components And Platform Services Are Test Doubles

lightning-* base components are stubs in Jest. Navigation, toasts, labels, and many platform modules are mocked. That is expected. The test should focus on your component behavior, not on reproducing the entire browser-plus-Salesforce runtime.


Common Patterns

Render, Interact, Assert

When to use: The component behavior is driven by user actions or reactive property updates.

How it works: Create the element, append it to document.body, simulate the user action, wait for rerender, then assert the visible state or dispatched event.

Why not the alternative: Snapshot-only tests or direct private-field assertions tend to miss the actual behavior users care about.

Wire Adapter Emission Testing

When to use: A component reads data through @wire.

How it works: Import the same adapter the component imports, create the component, append it, then emit data or an error payload and assert both the happy path and the error state after an awaited microtask. Registering the adapter under test was the Spring '21-and-earlier shape and is no longer recommended (lwc_guide unit-testing-using-wire-utility L12533, L12548, L12562).

Why not the alternative: Mocking the rendered HTML without exercising the wire contract gives false confidence.

Imperative Apex Promise Testing

When to use: A component saves or refreshes data through an imperative Apex call.

How it works: Mock the imported Apex module with jest.mock, resolve and reject the promise intentionally, and assert loading, success, and failure behavior.

Why not the alternative: Treating an imperative method like a wire adapter hides the actual async behavior and error path.


Decision Guidance

SituationRecommended ApproachReason
Component only needs a render or interaction checkcreateElement plus DOM assertionsFastest path to behavior coverage
Component uses a lightning/ui*Api adapterImport the same adapter and call .emit() on it after appendChildThe sfdx-lwc-jest stub already carries emit(); no registration and no jest.mock needed
Component wires an Apex methodMock the generated module so its default export is an Apex test wire adapterThe wired Apex contract is a stream, not a promise — see references/code-examples.md
Component calls Apex imperativelyjest.mock the module and resolve or reject promisesMatches the import's imperative usage
Component navigates or fires platform eventsMock the platform module and assert invocationUnit tests should verify intent, not real container behavior
Team is relying on snapshots as primary coverageReplace with behavioral assertionsSnapshots rarely prove business behavior well

  1. Inventory the boundaries. List every import the component makes that is not lwc: each @salesforce/* module, each lightning/* service module, each base component. That list is the mock plan; nothing else needs mocking.
  2. Answer the Questions table above. The wired-versus-imperative question and the propagation question decide the shape of half the suite.
  3. Wire the harness once. sf force lightning lwc test setup at the project root — it installs @salesforce/sfdx-lwc-jest, adds the package.json scripts, and writes the __tests__ glob into .forceignore. Then take templates/lwc/jest.config.js as the base and add only the moduleNameMapper delta from references/code-examples.md.
  4. Build the suite from references/code-examples.md. Copy the contactQuickEditor bundle and its test file; they cover wire emit, wire error, Apex resolve, Apex reject, custom-event payload, label mock, cleanup, and the missing-input negative path. Extend templates/lwc/component-skeleton/__tests__/componentSkeleton.test.js rather than starting from a blank file.
  5. Place one await Promise.resolve() per async boundary. Emit after appendChild, never before; a click that awaits Apex and then rerenders needs two awaits, not one — see references/gotchas.md.
  6. Run the checker. python3 skills/lwc/lwc-testing/scripts/check_lwc_testing.py --manifest-dir force-app/main/default/lwc — zero ERRORs before review, --strict in CI so the WARNs (untested bundles, un-flushed emissions, legacy register*TestWireAdapter, missing .forceignore entry) also block.
  7. Prove the suite can fail. Break one assertion's expected value on purpose and confirm it goes red, then run npm run test:unit:coverage and check the failure branches — not just the totals — against the thresholds in jest.config.js. Verification steps are in references/code-examples.md.

Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. LWC rerenders asynchronously - assertions immediately after a click or emit() run before the DOM updates; one awaited microtask per async boundary, two for a click that awaits Apex.
  2. Imperative Apex and wired Apex are not mocked the same way - using a wire utility for an imperative import produces misleading tests.
  3. Base components are stubs in Jest - the test should not assume exact runtime markup from the live platform.
  4. Platform modules such as navigation or toast are mocked boundaries - assert that your component asked for the action, not that the container performed it.
  5. A custom event with default propagation is invisible off the host - bubbles and composed both default to false, so the listener belongs on the element createElement returned.
  6. A @salesforce/label import resolves to its path, not its text - mock it, or the assertion compares against c.MyLabel forever.
  7. A property set before appendChild renders synchronously - only assignments made after the append need an awaited microtask.

Output Artifacts

ArtifactDescription
Test planCoverage strategy for render, interaction, wire, Apex, and error paths
Jest review findingsMissing mocks, flaky async patterns, weak assertions, or setup gaps
Test scaffoldReusable test skeleton with cleanup, mocks, and rerender handling

Reference Files

FileRead it when
references/code-examples.mdYou are writing the suite: full bundle, .js-meta.xml, the eight-test Jest file, the jest.config.js delta, .forceignore, CLI/CI commands, and the verification table
references/gotchas.mdA test is flaky, falsely green, or fails on something the browser does correctly
references/llm-anti-patterns.mdReviewing generated Jest code, or self-checking your own
references/examples.mdYou want the short single-scenario patterns rather than the whole bundle
references/well-architected.mdMapping the review to pillars, or checking which official source backs a claim
templates/lwc-testing-template.mdYou need the bare skeleton to paste into a new __tests__ file
scripts/check_lwc_testing.pyAuditing an existing lwc/ tree: --manifest-dir <dir>, add --strict in CI

Package files