Back to Repositories

Testing useMemo Hook Implementation in PreactJS

This test suite validates the useMemo hook implementation in Preact, focusing on memoization behavior, dependency tracking, and component rendering optimization. The tests ensure proper caching and recomputation of values based on dependency changes.

Test Coverage Overview

The test suite provides comprehensive coverage of useMemo functionality in Preact:

  • Dependency-based recomputation validation
  • Dependency array length changes
  • Error handling and recovery
  • Component memoization and render optimization
  • State updates and render skip scenarios
  • Edge cases with falsy and undefined values

Implementation Analysis

The testing approach utilizes Jest and Sinon for spy functionality to track function calls and verify memoization behavior. Tests employ component rendering patterns with controlled props and state updates to validate the hook’s caching mechanism and dependency tracking system.

Key implementation patterns include setup/teardown cycles, isolated component testing, and controlled state management using useState in conjunction with useMemo.

Technical Details

Testing infrastructure includes:

  • Jest as the primary testing framework
  • Sinon for spy/stub functionality
  • Preact test-utils for act() wrapper
  • Custom scratch element setup/teardown helpers
  • JSX transformation with createElement

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through isolation of test cases, comprehensive edge case coverage, and clear test organization. Notable practices include:

  • Consistent beforeEach/afterEach cleanup
  • Explicit state management verification
  • Granular function call tracking
  • Complex rendering scenario coverage
  • DOM state verification

preactjs/preact

hooks/test/browser/useMemo.test.js

            
import { createElement, render } from 'preact';
import { setupScratch, teardown } from '../../../test/_util/helpers';
import { useMemo, useState } from 'preact/hooks';
import { act } from 'preact/test-utils';

/** @jsx createElement */

describe('useMemo', () => {
	/** @type {HTMLDivElement} */
	let scratch;

	beforeEach(() => {
		scratch = setupScratch();
	});

	afterEach(() => {
		teardown(scratch);
	});

	it('only recomputes the result when inputs change', () => {
		let memoFunction = sinon.spy((a, b) => a + b);
		const results = [];

		function Comp({ a, b }) {
			const result = useMemo(() => memoFunction(a, b), [a, b]);
			results.push(result);
			return null;
		}

		render(<Comp a={1} b={1} />, scratch);
		render(<Comp a={1} b={1} />, scratch);

		expect(results).to.deep.equal([2, 2]);
		expect(memoFunction).to.have.been.calledOnce;

		render(<Comp a={1} b={2} />, scratch);
		render(<Comp a={1} b={2} />, scratch);

		expect(results).to.deep.equal([2, 2, 3, 3]);
		expect(memoFunction).to.have.been.calledTwice;
	});

	it('should rerun when deps length changes', () => {
		let memoFunction = sinon.spy(() => 1 + 2);

		function Comp({ all }) {
			const deps = [1, all && 2].filter(Boolean);
			const result = useMemo(() => memoFunction(), deps);
			return result;
		}

		render(<Comp all />, scratch);
		expect(memoFunction).to.have.been.calledOnce;
		render(<Comp all={false} />, scratch);
		expect(memoFunction).to.have.been.calledTwice;
	});

	it('should rerun when first run threw an error', () => {
		let hasThrown = false;
		let memoFunction = sinon.spy(() => {
			if (!hasThrown) {
				hasThrown = true;
				throw new Error('test');
			} else {
				return 3;
			}
		});

		function Comp() {
			const result = useMemo(() => memoFunction(), []);
			return result;
		}

		expect(() => render(<Comp />, scratch)).to.throw('test');
		expect(memoFunction).to.have.been.calledOnce;
		expect(() => render(<Comp />, scratch)).not.to.throw();
		expect(memoFunction).to.have.been.calledTwice;
	});

	it('short circuits diffing for memoized components', () => {
		let spy = sinon.spy();
		let spy2 = sinon.spy();
		const X = ({ count }) => {
			spy();
			return <span>{count}</span>;
		};

		const Y = ({ count }) => {
			spy2();
			return <p>{count}</p>;
		};

		const App = ({ x }) => {
			const y = useMemo(() => <Y count={x} />, [x]);
			return (
				<div>
					<X count={x} />
					{y}
				</div>
			);
		};

		render(<App x={0} />, scratch);
		expect(spy).to.be.calledOnce;
		expect(spy2).to.be.calledOnce;
		expect(scratch.innerHTML).to.equal('<div><span>0</span><p>0</p></div>');

		render(<App x={0} />, scratch);
		expect(spy).to.be.calledTwice;
		expect(spy2).to.be.calledOnce;
		expect(scratch.innerHTML).to.equal('<div><span>0</span><p>0</p></div>');

		render(<App x={1} />, scratch);
		expect(spy).to.be.calledThrice;
		expect(spy2).to.be.calledTwice;
		expect(scratch.innerHTML).to.equal('<div><span>1</span><p>1</p></div>');

		render(<App x={1} />, scratch);
		expect(spy2).to.be.calledTwice;
		expect(scratch.innerHTML).to.equal('<div><span>1</span><p>1</p></div>');

		render(<App x={2} />, scratch);
		expect(spy2).to.be.calledThrice;
		expect(scratch.innerHTML).to.equal('<div><span>2</span><p>2</p></div>');
	});

	it('should not commit memoization from a skipped render', () => {
		const calls = [];
		let set;
		const App = () => {
			const [greeting, setGreeting] = useState('hi');
			set = setGreeting;

			const value = useMemo(() => {
				calls.push('doing memo');
				return greeting;
			}, [greeting]);
			calls.push(`render ${value}`);

			if (greeting === 'bye') {
				setGreeting('hi');
			}

			return <p>{value}</p>;
		};

		render(<App />, scratch);
		expect(calls.length).to.equal(2);
		expect(calls).to.deep.equal(['doing memo', 'render hi']);

		act(() => {
			set('bye');
		});
		expect(calls.length).to.equal(6);
		expect(calls).to.deep.equal([
			'doing memo',
			'render hi',
			'doing memo',
			'render bye',
			'doing memo',
			'render hi'
		]);
	});

	it('should promote falsy value after a skipped render', () => {
		let update;

		function App() {
			const [v, set] = useState(0);
			update = set;
			const res = useMemo(() => 0, [v > 1]);

			if (v === 0) {
				set(v + 1);
			}
			return <p>{res}</p>;
		}

		render(<App />, scratch);
		expect(scratch.textContent).to.equal('0');

		act(() => {
			update(v => v + 1);
		});
		act(() => {
			update(v => v + 1);
		});

		expect(scratch.textContent).to.equal('0');
	});

	it('should promote undefined value after a skipped render', () => {
		let value;
		function Comp({ all }) {
			const result = (value = useMemo(() => (all ? 5 : undefined), [all]));
			return result;
		}
		render(<Comp all />, scratch);
		expect(value).to.equal(5);
		render(<Comp all={false} />, scratch);
		expect(value).to.equal(undefined);
		render(<Comp all={false} />, scratch);
		expect(value).to.equal(undefined);
	});
});