diff --git a/modules/signals/rxjs-interop/src/rx-method.ts b/modules/signals/rxjs-interop/src/rx-method.ts
index 03f2e32652..079e2878b7 100644
--- a/modules/signals/rxjs-interop/src/rx-method.ts
+++ b/modules/signals/rxjs-interop/src/rx-method.ts
@@ -12,7 +12,9 @@ import { isObservable, noop, Observable, Subject, Unsubscribable } from 'rxjs';
type RxMethodInput = Input | Observable | Signal;
-type RxMethod = ((input: RxMethodInput) => Unsubscribable) &
+export type RxMethod = ((
+ input: RxMethodInput
+) => Unsubscribable) &
Unsubscribable;
export function rxMethod(
diff --git a/modules/signals/testing/index.ts b/modules/signals/testing/index.ts
new file mode 100644
index 0000000000..0b6112f7a9
--- /dev/null
+++ b/modules/signals/testing/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Public API Surface of fake-rx-method
+ */
+
+export {
+ FakeRxMethod,
+ FAKE_RX_METHOD,
+ newFakeRxMethod,
+ asFakeRxMethod,
+ getRxMethodFake,
+} from './src/fake-rx-method';
+
+/*
+ * Public API Surface of mock-signal-store
+ */
+
+export {
+ MockSignalStore,
+ ProvideMockSignalStoreParams,
+ provideMockSignalStore,
+ UnwrapProvider,
+ asMockSignalStore,
+ asSinonSpy,
+} from './src/mock-signal-store';
diff --git a/modules/signals/testing/ng-package.json b/modules/signals/testing/ng-package.json
new file mode 100644
index 0000000000..1dc0b0bd36
--- /dev/null
+++ b/modules/signals/testing/ng-package.json
@@ -0,0 +1,5 @@
+{
+ "lib": {
+ "entryFile": "index.ts"
+ }
+}
diff --git a/modules/signals/testing/spec/fake-rx-method.spec.ts b/modules/signals/testing/spec/fake-rx-method.spec.ts
new file mode 100644
index 0000000000..02fcfad91a
--- /dev/null
+++ b/modules/signals/testing/spec/fake-rx-method.spec.ts
@@ -0,0 +1,85 @@
+import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import {
+ FakeRxMethod,
+ asFakeRxMethod,
+ getRxMethodFake,
+ newFakeRxMethod,
+} from '../src/fake-rx-method';
+import { Subject } from 'rxjs';
+import { RxMethod } from 'modules/signals/rxjs-interop/src/rx-method';
+
+@Component({
+ selector: 'app-test',
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: ``,
+})
+export class TestComponent {
+ fakeRxMethod = newFakeRxMethod();
+}
+
+describe('FakeRxMethod', () => {
+ describe('newFakeRxMethod and getRxMethodFake', () => {
+ let component: TestComponent;
+ let fixture: ComponentFixture;
+ let fakeRxMethod: FakeRxMethod;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [TestComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(TestComponent);
+ component = fixture.componentInstance;
+ fakeRxMethod = component.fakeRxMethod;
+ });
+
+ it('updates the Sinon fake on imperative calls', () => {
+ fakeRxMethod(11);
+ expect(getRxMethodFake(fakeRxMethod).callCount).toBe(1);
+ expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([11]);
+ });
+
+ it('updates the Sinon fake when an observable input emits', () => {
+ const o = new Subject();
+ fakeRxMethod(o);
+ o.next(11);
+ expect(getRxMethodFake(fakeRxMethod).callCount).toBe(1);
+ expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([11]);
+ o.next(22);
+ expect(getRxMethodFake(fakeRxMethod).callCount).toBe(2);
+ expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([22]);
+ });
+
+ it('updates the Sinon fake when a signal input emits (1)', () => {
+ const s = signal(72);
+ fakeRxMethod(s);
+ fixture.detectChanges();
+ expect(getRxMethodFake(fakeRxMethod).callCount).toBe(1);
+ expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([72]);
+ s.set(23);
+ fixture.detectChanges();
+ expect(getRxMethodFake(fakeRxMethod).callCount).toBe(2);
+ expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([23]);
+ });
+
+ it('updates the Sinon fake when a signal input emits (2)', () => {
+ const s = signal(72);
+ fakeRxMethod(s);
+ s.set(23);
+ fixture.detectChanges();
+ expect(getRxMethodFake(fakeRxMethod).callCount).toBe(1);
+ expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([23]);
+ });
+ });
+
+ describe('asFakeRxMethod', () => {
+ it('should return the input wihtout change', () => {
+ TestBed.runInInjectionContext(() => {
+ const rxMethod = newFakeRxMethod() as RxMethod;
+ expect(asFakeRxMethod(rxMethod)).toEqual(rxMethod);
+ });
+ });
+ });
+});
diff --git a/modules/signals/testing/spec/mock-signal-store.spec.ts b/modules/signals/testing/spec/mock-signal-store.spec.ts
new file mode 100644
index 0000000000..01d52c7c4a
--- /dev/null
+++ b/modules/signals/testing/spec/mock-signal-store.spec.ts
@@ -0,0 +1,276 @@
+import {
+ signalStore,
+ withState,
+ withComputed,
+ withMethods,
+ patchState,
+ getState,
+} from '@ngrx/signals';
+import { rxMethod } from '@ngrx/signals/rxjs-interop';
+import { Injectable, computed, inject } from '@angular/core';
+import { pipe, switchMap, tap, Observable, of, Subject } from 'rxjs';
+import { tapResponse } from '@ngrx/component-store';
+import { HttpErrorResponse } from '@angular/common/http';
+import { TestBed } from '@angular/core/testing';
+import {
+ MockSignalStore,
+ UnwrapProvider,
+ asMockSignalStore,
+ asSinonSpy,
+ provideMockSignalStore,
+} from '../src/mock-signal-store';
+import { getRxMethodFake } from '../src/fake-rx-method';
+import { fake, replace } from 'sinon';
+
+@Injectable()
+class SampleService {
+ getTripleValue(n: number): Observable {
+ return of(n * 3);
+ }
+}
+
+const initialState = {
+ value: 1,
+ object: {
+ objectValue: 2,
+ nestedObject: {
+ nestedObjectValue: 3,
+ },
+ },
+};
+
+const SampleSignalStore = signalStore(
+ withState(initialState),
+ withComputed(({ value }) => ({
+ doubleNumericValue: computed(() => value() * 2),
+ tripleNumericValue: computed(() => value() * 3),
+ })),
+ withMethods((store) => ({
+ setValue(value: number): void {
+ patchState(store, () => ({
+ value,
+ }));
+ },
+ setNestedObjectValue(nestedObjectValue: number): void {
+ patchState(store, () => ({
+ object: {
+ ...store.object(),
+ nestedObject: {
+ ...store.object.nestedObject(),
+ nestedObjectValue,
+ },
+ },
+ }));
+ },
+ })),
+ withMethods((store, service = inject(SampleService)) => ({
+ rxMethod: rxMethod(
+ pipe(
+ tap(() => store.setValue(10)),
+ switchMap((n) => service.getTripleValue(n)),
+ tapResponse(
+ (response) => {
+ store.setNestedObjectValue(response);
+ },
+ (errorResponse: HttpErrorResponse) => {
+ store.setNestedObjectValue(0);
+ }
+ )
+ )
+ ),
+ }))
+);
+
+describe('mockSignalStore', () => {
+ describe('with default parameters', () => {
+ let store: UnwrapProvider;
+ let mockStore: MockSignalStore;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ SampleService,
+ provideMockSignalStore(SampleSignalStore, {
+ initialComputedValues: {
+ doubleNumericValue: 20,
+ tripleNumericValue: 30,
+ },
+ }),
+ ],
+ });
+ store = TestBed.inject(SampleSignalStore);
+ mockStore = asMockSignalStore(store);
+ });
+
+ it('should set the original initial values', () => {
+ expect(store.value()).toBe(initialState.value);
+ expect(store.object()).toEqual(initialState.object);
+ });
+
+ it("should set the computed signal's initial value", () => {
+ expect(store.doubleNumericValue()).toBe(20);
+ });
+
+ it('should mock the computed signal with a writable signal', () => {
+ expect(store.doubleNumericValue()).toBe(20);
+ mockStore.doubleNumericValue.set(33);
+ expect(store.doubleNumericValue()).toBe(33);
+ });
+
+ it('should mock the updater with a Sinon fake', () => {
+ expect(mockStore.setValue.callCount).toBe(0);
+ store.setValue(11);
+ expect(mockStore.setValue.callCount).toBe(1);
+ expect(mockStore.setValue.lastCall.args).toEqual([11]);
+ });
+
+ it('should mock the rxMethod with a FakeRxMethod (imperative)', () => {
+ expect(getRxMethodFake(store.rxMethod).callCount).toBe(0);
+ store.rxMethod(22);
+ expect(getRxMethodFake(store.rxMethod).callCount).toBe(1);
+ expect(getRxMethodFake(store.rxMethod).lastCall.args).toEqual([22]);
+ });
+
+ it('should mock the rxMethod with a FakeRxMethod (declarative)', () => {
+ const o = new Subject();
+ store.rxMethod(o);
+ expect(getRxMethodFake(store.rxMethod).callCount).toBe(0);
+ o.next(22);
+ expect(getRxMethodFake(store.rxMethod).callCount).toBe(1);
+ expect(getRxMethodFake(store.rxMethod).lastCall.args).toEqual([22]);
+ });
+
+ it('can alter the DeepSignal with patchState', () => {
+ patchState(store, {
+ value: 20,
+ });
+ expect(store.value()).toBe(20);
+ expect(store.object()).toEqual(initialState.object);
+
+ patchState(store, {
+ object: {
+ ...initialState.object,
+ nestedObject: {
+ ...initialState.object.nestedObject,
+ nestedObjectValue: 40,
+ },
+ },
+ });
+ expect(store.object()).toEqual({
+ ...initialState.object,
+ nestedObject: {
+ ...initialState.object.nestedObject,
+ nestedObjectValue: 40,
+ },
+ });
+ });
+ });
+ describe('parameters', () => {
+ it('should throw an expection, if an inital value is missing for a computed Signal', () => {
+ expect(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ SampleService,
+ provideMockSignalStore(SampleSignalStore, {
+ initialComputedValues: {
+ doubleNumericValue: 20,
+ },
+ }),
+ ],
+ });
+ const store = TestBed.inject(SampleSignalStore);
+ const mockStore = asMockSignalStore(store);
+ }).toThrow(Error('tripleNumericValue should have an initial value'));
+ });
+
+ it('should throw an expection, if an inital value is missing for a computed Signal (2)', () => {
+ expect(() => {
+ TestBed.configureTestingModule({
+ providers: [SampleService, provideMockSignalStore(SampleSignalStore)],
+ });
+ const store = TestBed.inject(SampleSignalStore);
+ const mockStore = asMockSignalStore(store);
+ }).toThrowError();
+ });
+
+ it('can keep the original computed signals', () => {
+ TestBed.configureTestingModule({
+ providers: [
+ SampleService,
+ provideMockSignalStore(SampleSignalStore, {
+ mockComputedSignals: false,
+ }),
+ ],
+ });
+ const store = TestBed.inject(SampleSignalStore);
+ const mockStore = asMockSignalStore(store);
+
+ expect(store.doubleNumericValue()).toBe(2);
+ expect(store.tripleNumericValue()).toBe(3);
+ });
+
+ it('can update the initial value of the store by the initialStatePatch parameter', () => {
+ TestBed.configureTestingModule({
+ providers: [
+ SampleService,
+ provideMockSignalStore(SampleSignalStore, {
+ initialComputedValues: {
+ doubleNumericValue: 20,
+ tripleNumericValue: 30,
+ },
+ initialStatePatch: {
+ value: 22,
+ },
+ }),
+ ],
+ });
+ const store = TestBed.inject(SampleSignalStore);
+
+ expect(store.value()).toBe(22);
+ });
+
+ it('can update the initial value of the store by the initialStatePatch parameter (nested objects)', () => {
+ TestBed.configureTestingModule({
+ providers: [
+ SampleService,
+ provideMockSignalStore(SampleSignalStore, {
+ initialComputedValues: {
+ doubleNumericValue: 20,
+ tripleNumericValue: 30,
+ },
+ initialStatePatch: {
+ object: {
+ ...initialState.object,
+ nestedObject: {
+ ...initialState.object.nestedObject,
+ nestedObjectValue: 40,
+ },
+ },
+ },
+ }),
+ ],
+ });
+ const store = TestBed.inject(SampleSignalStore);
+
+ expect(getState(store)).toEqual({
+ ...initialState,
+ object: {
+ ...initialState.object,
+ nestedObject: {
+ ...initialState.object.nestedObject,
+ nestedObjectValue: 40,
+ },
+ },
+ });
+ });
+ });
+ describe('asSinonSpy', () => {
+ it('should return the input wihtout change', () => {
+ TestBed.runInInjectionContext(() => {
+ const o = { fnc: () => 1 };
+ replace(o, 'fnc', fake());
+ expect(asSinonSpy(o.fnc)).toEqual(o.fnc);
+ });
+ });
+ });
+});
diff --git a/modules/signals/testing/src/fake-rx-method.ts b/modules/signals/testing/src/fake-rx-method.ts
new file mode 100644
index 0000000000..368dbe396e
--- /dev/null
+++ b/modules/signals/testing/src/fake-rx-method.ts
@@ -0,0 +1,53 @@
+import { rxMethod } from '@ngrx/signals/rxjs-interop';
+import { tap } from 'rxjs';
+import { SinonSpy, fake } from 'sinon';
+import { RxMethod } from 'modules/signals/rxjs-interop/src/rx-method';
+
+/**
+ * FakeRxMethod mock type, it's an extended version of RxMethod, with an additional
+ * [FAKE_RX_METHOD] property containing a Sinon fake (SinonSpy<[T]>).
+ */
+export const FAKE_RX_METHOD = Symbol('FAKE_RX_METHOD');
+export type FakeRxMethod = RxMethod & { [FAKE_RX_METHOD]: SinonSpy<[T]> };
+
+/**
+ * Creates a new rxMethod mock.
+ * The returned function accepts a static value, signal, or observable as an input argument.
+ *
+ * The Sinon fake stores the call information, when:
+ * - the generated function was called with a static value.
+ * - the generated function was called with a signal argument, and the signal's value changes.
+ * - the generated function was called with an observable argument, and the observable emits.
+ *
+ * @returns {FakeRxMethod} A new rxMethod mock.
+ */
+export function newFakeRxMethod(): FakeRxMethod {
+ const f = fake<[T]>();
+ const r = rxMethod(tap((x) => f(x))) as FakeRxMethod;
+ r[FAKE_RX_METHOD] = f;
+ return r;
+}
+
+/**
+ * Converts the type of a (mocked) RxMethod into a FakeRxMethod.
+ *
+ * @template T - The argument type of the RxMethod.
+ * @param {RxMethod} rxMethod - The (mocked) RxMethod to be converted.
+ * @returns {FakeRxMethod} The converted FakeRxMethod.
+ */
+export function asFakeRxMethod(rxMethod: RxMethod): FakeRxMethod {
+ return rxMethod as unknown as FakeRxMethod;
+}
+
+/**
+ * Gets the Sinon fake from a mocked RxMethod.
+ *
+ * @template T - The argument type of the RxMethod.
+ * @param {RxMethod} rxMethod - The (mocked) RxMethod for which to retrieve the Sinon fake.
+ * @returns {sinon.SinonSpy<[T], unknown>} The Sinon fake capturing calls to the RxMethod.
+ */
+export function getRxMethodFake(
+ rxMethod: RxMethod
+): SinonSpy<[T], unknown> {
+ return asFakeRxMethod(rxMethod)[FAKE_RX_METHOD];
+}
diff --git a/modules/signals/testing/src/mock-signal-store.ts b/modules/signals/testing/src/mock-signal-store.ts
new file mode 100644
index 0000000000..e788ddb30d
--- /dev/null
+++ b/modules/signals/testing/src/mock-signal-store.ts
@@ -0,0 +1,270 @@
+import {
+ Provider,
+ ProviderToken,
+ Signal,
+ WritableSignal,
+ isSignal,
+ untracked,
+ signal,
+} from '@angular/core';
+import { SinonSpy, fake } from 'sinon';
+import { FakeRxMethod, newFakeRxMethod } from './fake-rx-method';
+import { getState, patchState } from '../../src';
+import { StateSignal } from '../../src/state-signal';
+import { RxMethod } from 'modules/signals/rxjs-interop/src/rx-method';
+
+/**
+ * Constructor type.
+ */
+interface Constructor {
+ new (...args: never[]): ClassType;
+}
+
+/**
+ * Function type.
+ */
+type Method = (...args: T) => unknown;
+
+/**
+ * Type for a mocked singalStore:
+ * - Signals are replaced by WritableSignals.
+ * - RxMethods are replaced by FakeRxMethods.
+ * - Functions are replaced by Sinon fakes.
+ */
+export type MockSignalStore = {
+ [K in keyof T]: T[K] extends Signal
+ ? WritableSignal
+ : T[K] extends RxMethod
+ ? FakeRxMethod
+ : T[K] extends Method
+ ? SinonSpy
+ : T[K];
+};
+
+/**
+ * Type for the state of the singlaStore.
+ */
+type InitialState = T extends StateSignal ? U : never;
+
+/**
+ * Given a type T, determines the keys of the signal properties.
+ */
+type SignalKeys = {
+ // -? makes the key required, opposite of ?
+ [K in keyof T]-?: T[K] extends Signal ? K : never;
+}[keyof T];
+
+/**
+ * Type to extract the wrapped type from a Signal type.
+ *
+ * @template T - The original Signal type.
+ * @returns The unwrapped type if T is a Signal, otherwise, 'never'.
+ */
+type UnwrapSignal = T extends Signal ? U : never;
+
+/**
+ * Parameters for providing a mock signal store.
+ *
+ * @template T The type of the original signal store.
+ * @param initialStatePatch A partial initial state to override the original initial state.
+ * @param initialComputedValues Initial values for computed signals.
+ * @param mockComputedSignals Flag to mock computed signals (default is true).
+ * @param mockMethods Flag to mock methods (default is true).
+ * @param mockRxMethods Flag to mock RxMethods (default is true).
+ * @param debug Flag to enable debug mode (default is false).
+ */
+export type ProvideMockSignalStoreParams = {
+ initialStatePatch?: Partial>;
+ initialComputedValues?: Omit<
+ {
+ [K in SignalKeys]?: UnwrapSignal;
+ },
+ keyof InitialState
+ >;
+ mockComputedSignals?: boolean;
+ mockMethods?: boolean;
+ mockRxMethods?: boolean;
+ debug?: boolean;
+};
+
+/**
+ * Provides a mock version of signal store.
+ *
+ * @template ClassType The class type that extends StateSignal