AngularTestingSignalsJestVitest

Testing Signals: Fast Unit Tests in 2026

T
Test Engineer
Featured Guide 15 min read

Stop using fakeAsync.

Testing Observables was painful. You needed marbles, tick(), and complex async schedulers.

Signals are synchronous by default. You set a value, and the computed updates instantly (or lazily upon access). The test code reads like a story.

02. Direct Access

No subscription needed. Just call the signal function.

// counter.component.spec.ts
it
('should double the count', () => {'{'}
  component.count.set(2);
  TestBed.flushEffects(); // Run effect() blocks
  
expect
(component.doubleCount()).toBe(4);
{'}'});

03. Mocking Signals

Deep Dive: Mocking Read-Only Signals

Often you need to mock a Service that exposes a read-only Signal<T>.

Technique: Just create a writable signal and cast it!
userService.currentUser = signal(mockUser) as unknown as Signal<User>;
Now your test can control the value by calling .set() on the underlying writable signal, even though the component sees it as read-only.

04. The Senior Engineer's Take

Effect Testing

Normally, you shouldn't granularly test effect() logic as it's implementation detail. Focus on testing the Computed results.

If you must test an effect (e.g. it writes to localStorage), you must call TestBed.flushEffects() because effects schedule asynchronously.

Interactive Playground

import React, { useState } from 'react';

// 🧪 Test Runner Visualizer

export default function TestDemo() {
    const [state, setState] = useState('idle'); // idle, running, passed
    
    const runTest = async () => {
        setState('running');
        await wait(600);
        setState('passed');
    };

    const wait = (ms) => new Promise(r => setTimeout(r, ms));

    return (
        <div className="bg-slate-50 dark:bg-slate-950 p-8 rounded-3xl border border-slate-200 dark:border-slate-800 shadow-xl">
             <div className="flex justify-between items-center mb-10">
                <h3 className="text-2xl font-black text-gray-900 dark:text-white flex items-center gap-3">
                    <span className="text-green-500">🧪</span> Test Runner (Vitest)
                </h3>
            </div>

            <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
                
                {/* Code Block */}
                <div className="bg-gray-900 rounded-xl p-6 font-mono text-sm relative overflow-hidden text-gray-300 border border-gray-800">
                    <div className="absolute top-0 left-0 right-0 h-6 bg-gray-800 flex items-center px-4 text-xs text-gray-400">
                        user.store.spec.ts
                    </div>
                    <div className="mt-6 space-y-1">
                        <div className="text-purple-400">it(<span className="text-green-400">'updates computed'</span>, () => {'{'}</div>
                        <div className="pl-4">store.users.set([ 'A', 'B' ]);</div>
                        <div className="pl-4 text-gray-500">// No manual "tick()" needed!</div>
                        <div className="pl-4">expect(store.count()).toBe(2);</div>
                        <div className="text-purple-400">{'}'});</div>
                    </div>
                </div>

                {/* Status */}
                <div className="flex flex-col items-center justify-center p-6 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800">
                    {state === 'idle' && (
                         <div className="text-center">
                             <div className="mx-auto text-4xl mb-4 text-gray-300">▶️</div>
                             <button onClick={runTest} className="px-6 py-2 bg-green-500 text-white font-bold rounded-lg hover:bg-green-600 transition">
                                 Run Test
                             </button>
                         </div>
                    )}
                    {state === 'running' && (
                         <div className="text-center">
                             <div className="mx-auto text-4xl mb-4 animate-spin"></div>
                             <p className="font-bold text-gray-700 dark:text-white">Executing...</p>
                         </div>
                    )}
                    {state === 'passed' && (
                         <div className="text-center animate-in zoom-in">
                             <div className="mx-auto text-6xl mb-4 text-green-500"></div>
                             <p className="font-bold text-2xl text-green-600 dark:text-green-400">PASSED</p>
                             <p className="text-sm text-gray-400 mt-2">Duration: 4ms</p>
                             <button onClick={() => setState('idle')} className="mt-6 text-xs text-gray-500 underline">Reset</button>
                         </div>
                    )}
                </div>

            </div>
        </div>
    );
}