When you write a Jest test for a Gutenberg block’s save function, there are usually two paths you can take: test what was called, or test what got rendered. They look similar on the surface, but they lead to very different test suites.
Approach 1: Testing Implementation Details
test('calls useBlockProps.save with correct className', () => {
const { useBlockProps } = require('@wordpress/block-editor');
render(React.createElement(save, mockProps));
expect(useBlockProps.save).toHaveBeenCalledWith({
className: 'newsly_block__featured_posts',
});
});
This test checks that useBlockProps.save was called with the right arguments. It’s easy to write, and it feels like a real test — it passes, it fails, it has an assertion.
But it’s testing a mechanism, not an outcome. It knows too much about how the class name gets applied internally. If you ever refactor the block — compose the className differently, wrap the element, switch to a different helper — this test breaks, even if the actual markup the browser sees is exactly the same.
Worse, it can give you false confidence. You could call useBlockProps.save() correctly and still forget to spread the result onto your JSX element. This test would still pass, while your real output is broken.
Approach 2: Testing Behavior
test('renders with correct block class name', () => {
const { container } = render(React.createElement(save, mockProps));
expect(container.firstChild).toHaveClass('newsly_block__featured_posts');
});
This test doesn’t care how the class name got there. It only checks the thing that actually matters: does the rendered DOM have the right class?
This single assertion quietly covers more ground than the first test:
- It confirms
useBlockProps.savewas called correctly. - It confirms the return value was actually applied to the element.
- It survives internal refactors, as long as the output contract doesn’t change.
This is the core idea behind Testing Library’s philosophy: the more your test resembles how your code is actually used, the more confidence it gives you.
The Rule of Thumb
| Implementation test | Behavior test | |
|---|---|---|
| Breaks on refactor | Yes | No |
| Catches “forgot to apply the value” bugs | No | Yes |
| Resembles real usage | No | Yes |
| Useful for | Verifying a specific API contract | Verifying real output |
Default to behavior tests. They’re more resilient and catch more real bugs with less code.
If you specifically need to lock in a WordPress API contract — for example, making sure you’re passing the exact right attributes into useBlockProps.save — keep an implementation test as a secondary check, not your primary source of confidence. The rendered-output test should always be the one you trust.
Takeaway: test what the user (or in this case, the browser) actually sees, not the internal calls your code happens to make to get there.