Repository: streamich/react-use
Pull request #554 (2019-08-22 21:28:22)

Animation block tests
Add tests for animation hooks block #101 
This one was hard to test, specially useRaf and useSpring but I learnt a lot testing them!

This include tests for:

- useRaf
- useSpring
- useTimeout
- useUpdate
- useTimeoutFn (already done by someone else! I just adapt it to have same structure as others)

The PR also includes some bug fixes!




Patch:
diff --git a/src/__stories__/useSpring.story.tsx b/src/__stories__/useSpring.story.tsx
index cc7a2d9a..0727e5d7 100644
--- a/src/__stories__/useSpring.story.tsx
+++ b/src/__stories__/useSpring.story.tsx
@@ -1,6 +1,6 @@
 import { storiesOf } from '@storybook/react';
 import * as React from 'react';
-import { useSpring } from '..';
+import useSpring from '../useSpring';
 import ShowDocs from './util/ShowDocs';
 
 const Demo = () => {
diff --git a/src/useSpring.ts b/src/useSpring.ts
index 3567f2ee..7f4dee2a 100644
--- a/src/useSpring.ts
+++ b/src/useSpring.ts
@@ -1,31 +1,37 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useMemo } from 'react';
 import { Spring, SpringSystem } from 'rebound';
 
 const useSpring = (targetValue: number = 0, tension: number = 50, friction: number = 3) => {
   const [spring, setSpring] = useState<Spring | null>(null);
   const [value, setValue] = useState<number>(targetValue);
 
-  useEffect(() => {
-    const listener = {
+  // memoize listener to being able to unsubscribe later properly, otherwise
+  // listener fn will be different on each re-render and wouldn't unsubscribe properly.
+  const listener = useMemo(
+    () => ({
       onSpringUpdate: currentSpring => {
         const newValue = currentSpring.getCurrentValue();
         setValue(newValue);
       },
-    };
+    }),
+    []
+  );
 
+  useEffect(() => {
     if (!spring) {
       const newSpring = new SpringSystem().createSpring(tension, friction);
       newSpring.setCurrentValue(targetValue);
       setSpring(newSpring);
       newSpring.addListener(listener);
-      return;
     }
 
     return () => {
-      spring.removeListener(listener);
-      setSpring(null);
+      if (spring) {
+        spring.removeListener(listener);
+        setSpring(null);
+      }
     };
-  }, [tension, friction]);
+  }, [tension, friction, spring]);
 
   useEffect(() => {
     if (spring) {


Test patch:
diff --git a/src/__tests__/useGetSetState.test.ts b/src/__tests__/useGetSetState.test.ts
index e9eb6aef..46a27fbf 100644
--- a/src/__tests__/useGetSetState.test.ts
+++ b/src/__tests__/useGetSetState.test.ts
@@ -100,8 +100,7 @@ it('should log an error if set with a patch different than an object', () => {
   const [, set] = result.current;
   expect(mockConsoleError).not.toHaveBeenCalled();
 
-  // @ts-ignore
-  act(() => set('not an object'));
+  act(() => set('not an object' as any));
 
   expect(mockConsoleError).toHaveBeenCalledTimes(1);
   expect(mockConsoleError).toHaveBeenCalledWith('useGetSetState setter patch must be an object.');
diff --git a/src/__tests__/useMap.test.ts b/src/__tests__/useMap.test.ts
index 242df55f..cc9d57a8 100644
--- a/src/__tests__/useMap.test.ts
+++ b/src/__tests__/useMap.test.ts
@@ -1,7 +1,7 @@
 import { act, renderHook } from '@testing-library/react-hooks';
 import useMap from '../useMap';
 
-const setUp = (initialMap?: object) => renderHook(() => useMap(initialMap));
+const setUp = <T extends object>(initialMap?: T) => renderHook(() => useMap(initialMap));
 
 it('should init map and utils', () => {
   const { result } = setUp({ foo: 'bar', a: 1 });
@@ -28,7 +28,6 @@ it('should get corresponding value for existing provided key', () => {
 
   let value;
   act(() => {
-    // @ts-ignore
     value = utils.get('a');
   });
 
@@ -36,12 +35,11 @@ it('should get corresponding value for existing provided key', () => {
 });
 
 it('should get undefined for non-existing provided key', () => {
-  const { result } = setUp({ foo: 'bar', a: 1 });
+  const { result } = setUp<{ foo: string; a: number; nonExisting?: any }>({ foo: 'bar', a: 1 });
   const [, utils] = result.current;
 
   let value;
   act(() => {
-    // @ts-ignore
     value = utils.get('nonExisting');
   });
 
@@ -49,11 +47,10 @@ it('should get undefined for non-existing provided key', () => {
 });
 
 it('should set new key-value pair', () => {
-  const { result } = setUp({ foo: 'bar', a: 1 });
+  const { result } = setUp<{ foo: string; a: number; newKey?: number }>({ foo: 'bar', a: 1 });
   const [, utils] = result.current;
 
   act(() => {
-    // @ts-ignore
     utils.set('newKey', 99);
   });
 
@@ -65,11 +62,10 @@ it('should override current value if setting existing key', () => {
   const [, utils] = result.current;
 
   act(() => {
-    // @ts-ignore
-    utils.set('foo', 99);
+    utils.set('foo', 'qux');
   });
 
-  expect(result.current[0]).toEqual({ foo: 99, a: 1 });
+  expect(result.current[0]).toEqual({ foo: 'qux', a: 1 });
 });
 
 it('should remove corresponding key-value pair for existing provided key', () => {
@@ -77,7 +73,6 @@ it('should remove corresponding key-value pair for existing provided key', () =>
   const [, utils] = result.current;
 
   act(() => {
-    // @ts-ignore
     utils.remove('foo');
   });
 
@@ -85,11 +80,10 @@ it('should remove corresponding key-value pair for existing provided key', () =>
 });
 
 it('should do nothing if removing non-existing provided key', () => {
-  const { result } = setUp({ foo: 'bar', a: 1 });
+  const { result } = setUp<{ foo: string; a: number; nonExisting?: any }>({ foo: 'bar', a: 1 });
   const [, utils] = result.current;
 
   act(() => {
-    // @ts-ignore
     utils.remove('nonExisting');
   });
 
@@ -97,11 +91,10 @@ it('should do nothing if removing non-existing provided key', () => {
 });
 
 it('should reset map to initial object provided', () => {
-  const { result } = setUp({ foo: 'bar', a: 1 });
+  const { result } = setUp<{ foo: string; a: number; z?: number }>({ foo: 'bar', a: 1 });
   const [, utils] = result.current;
 
   act(() => {
-    // @ts-ignore
     utils.set('z', 99);
   });
 
diff --git a/src/__tests__/useRaf.test.ts b/src/__tests__/useRaf.test.ts
new file mode 100644
index 00000000..3d1936e3
--- /dev/null
+++ b/src/__tests__/useRaf.test.ts
@@ -0,0 +1,153 @@
+import { act, renderHook } from '@testing-library/react-hooks';
+import { replaceRaf } from 'raf-stub';
+import useRaf from '../useRaf';
+
+/**
+ * New requestAnimationFrame after being replaced with raf-stub for testing purposes.
+ */
+interface RequestAnimationFrame {
+  reset(): void;
+  step(): void;
+}
+declare var requestAnimationFrame: RequestAnimationFrame;
+
+replaceRaf();
+const fixedStart = 1564949709496;
+const spyDateNow = jest.spyOn(Date, 'now').mockImplementation(() => fixedStart);
+
+beforeEach(() => {
+  jest.useFakeTimers();
+  requestAnimationFrame.reset();
+});
+
+afterEach(() => {
+  jest.clearAllTimers();
+  requestAnimationFrame.reset();
+});
+
+it('should init percentage of time elapsed', () => {
+  const { result } = renderHook(() => useRaf());
+  const timeElapsed = result.current;
+
+  expect(timeElapsed).toBe(0);
+});
+
+it('should return corresponding percentage of time elapsed for default ms', () => {
+  const { result } = renderHook(() => useRaf());
+  expect(result.current).toBe(0);
+
+  act(() => {
+    jest.runOnlyPendingTimers(); // start after delay
+    spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 0.25); // 25%
+    requestAnimationFrame.step();
+  });
+  expect(result.current).toBe(0.25);
+
+  act(() => {
+    spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 0.5); // 50%
+    requestAnimationFrame.step();
+  });
+  expect(result.current).toBe(0.5);
+
+  act(() => {
+    spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 0.75); // 75%
+    requestAnimationFrame.step();
+  });
+  expect(result.current).toBe(0.75);
+
+  act(() => {
+    spyDateNow.mockImplementationOnce(() => fixedStart + 1e12); // 100%
+    requestAnimationFrame.step();
+  });
+  expect(result.current).toBe(1);
+});
+
+it('should return corresponding percentage of time elapsed for custom ms', () => {
+  const customMs = 2000;
+
+  const { result } = renderHook(() => useRaf(customMs));
+  expect(result.current).toBe(0);
+
+  act(() => {
+    jest.runOnlyPendingTimers(); // start after delay
+    spyDateNow.mockImplementationOnce(() => fixedStart + customMs * 0.25); // 25%
+    requestAnimationFrame.step();
+  });
+  expect(result.current).toBe(0.25);
+
+  act(() => {
+    spyDateNow.mockImplementationOnce(() => fixedStart + customMs * 0.5); // 50%
+    requestAnimationFrame.step();
+  });
+  expect(result.current).toBe(0.5);
+
+  act(() => {
+    spyDateNow.mockImplementationOnce(() => fixedStart + customMs * 0.75); // 75%
+    requestAnimationFrame.step();
+  });
+  expect(result.current).toBe(0.75);
+
+  act(() => {
+    spyDateNow.mockImplementationOnce(() => fixedStart + customMs); // 100%
+    requestAnimationFrame.step();
+  });
+  expect(result.current).toBe(1);
+});
+
+it('should return always 1 after corresponding ms reached', () => {
+  const { result } = renderHook(() => useRaf());
+  expect(result.current).toBe(0);
+
+  act(() => {
+    jest.runOnlyPendingTimers(); // start after delay
+    spyDateNow.mockImplementationOnce(() => fixedStart + 1e12); // 100%
+    requestAnimationFrame.step();
+  });
+  expect(result.current).toBe(1);
+
+  act(() => {
+    spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 1.1); // 110%
+    requestAnimationFrame.step();
+  });
+  expect(result.current).toBe(1);
+
+  act(() => {
+    spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 3); // 300%
+    requestAnimationFrame.step();
+  });
+  expect(result.current).toBe(1);
+});
+
+it('should wait until delay reached to start calculating elapsed percentage', () => {
+  const { result } = renderHook(() => useRaf(undefined, 500));
+
+  expect(result.current).toBe(0);
+
+  act(() => {
+    jest.advanceTimersByTime(250); // fast-forward only half of custom delay
+  });
+  expect(result.current).toBe(0);
+
+  act(() => {
+    jest.advanceTimersByTime(249); // fast-forward 1ms less than custom delay
+  });
+  expect(result.current).toBe(0);
+
+  act(() => {
+    jest.advanceTimersByTime(1); // fast-forward exactly to custom delay
+  });
+  expect(result.current).not.toBe(0);
+});
+
+it('should clear pending timers on unmount', () => {
+  const spyRafStop = jest.spyOn(global, 'cancelAnimationFrame' as any);
+  const { unmount } = renderHook(() => useRaf());
+
+  expect(clearTimeout).not.toHaveBeenCalled();
+  expect(spyRafStop).not.toHaveBeenCalled();
+
+  unmount();
+
+  expect(clearTimeout).toHaveBeenCalledTimes(2);
+  expect(spyRafStop).toHaveBeenCalledTimes(1);
+});
diff --git a/src/__tests__/useSpring.test.ts b/src/__tests__/useSpring.test.ts
new file mode 100644
index 00000000..a6a1c926
--- /dev/null
+++ b/src/__tests__/useSpring.test.ts
@@ -0,0 +1,119 @@
+import { act, renderHook } from '@testing-library/react-hooks';
+import useSpring from '../useSpring';
+import { Spring } from 'rebound';
+
+// simulate Spring for testing
+const mockSetCurrentValue = jest.fn();
+const mockAddListener = jest.fn();
+const mockSetEndValue = jest.fn();
+const mockRemoveListener = jest.fn();
+let triggerSpringUpdate = () => {};
+let springListener: Listener = { onSpringUpdate: () => {} };
+
+interface Listener {
+  onSpringUpdate: (currentSpring: Spring) => void;
+}
+
+const mockCreateSpring: Spring = jest.fn().mockImplementation(() => {
+  let currentValue = 0;
+  let endValue = 0;
+
+  const getCloserValue = (a, b) => Math.round((a + b) / 2);
+
+  const getCurrentValue = () => {
+    currentValue = getCloserValue(currentValue, endValue);
+    return currentValue;
+  };
+
+  triggerSpringUpdate = () => {
+    if (currentValue !== endValue) {
+      springListener.onSpringUpdate({ getCurrentValue } as any);
+    }
+  };
+
+  return {
+    setCurrentValue: val => {
+      currentValue = val;
+      mockSetCurrentValue(val);
+    },
+    addListener: newListener => {
+      springListener = newListener;
+      mockAddListener(newListener);
+    },
+    setEndValue: val => {
+      endValue = val;
+      mockSetEndValue(val);
+    },
+    removeListener: mockRemoveListener,
+  };
+}) as any;
+
+jest.mock('rebound', () => {
+  return {
+    Sprint: {},
+    SpringSystem: jest.fn().mockImplementation(() => {
+      return { createSpring: mockCreateSpring };
+    }),
+  };
+});
+
+it('should init value to provided target', () => {
+  const { result } = renderHook(() => useSpring(70));
+
+  expect(result.current).toBe(70);
+  expect(mockSetCurrentValue).toHaveBeenCalledTimes(1);
+  expect(mockSetCurrentValue).toHaveBeenCalledWith(70);
+  expect(mockCreateSpring).toHaveBeenCalledTimes(1);
+  expect(mockCreateSpring).toHaveBeenCalledWith(50, 3);
+});
+
+it('should create spring with custom tension and friction args provided', () => {
+  renderHook(() => useSpring(500, 20, 7));
+
+  expect(mockCreateSpring).toHaveBeenCalledTimes(1);
+  expect(mockCreateSpring).toHaveBeenCalledWith(20, 7);
+});
+
+it('should subscribe only once', () => {
+  const { rerender } = renderHook(() => useSpring());
+
+  expect(mockAddListener).toHaveBeenCalledTimes(1);
+  expect(mockAddListener).toHaveBeenCalledWith(springListener);
+
+  rerender();
+
+  expect(mockAddListener).toHaveBeenCalledTimes(1);
+});
+
+it('should handle spring update', () => {
+  let targetValue = 70;
+  let lastSpringValue = targetValue;
+  const { result, rerender } = renderHook(() => useSpring(targetValue));
+
+  targetValue = 100;
+  rerender();
+  expect(result.current).toBe(lastSpringValue);
+
+  act(() => {
+    triggerSpringUpdate(); // simulate new spring value
+  });
+  expect(result.current).toBeGreaterThan(lastSpringValue);
+  expect(result.current).toBeLessThanOrEqual(targetValue);
+
+  lastSpringValue = result.current;
+  act(() => {
+    triggerSpringUpdate(); // simulate another new spring value
+  });
+  expect(result.current).toBeGreaterThan(lastSpringValue);
+  expect(result.current).toBeLessThanOrEqual(targetValue);
+});
+
+it('should remove listener on unmount', () => {
+  const { unmount } = renderHook(() => useSpring());
+  expect(mockRemoveListener).not.toHaveBeenCalled();
+
+  unmount();
+
+  expect(mockRemoveListener).toHaveBeenCalledTimes(1);
+  expect(mockRemoveListener).toHaveBeenCalledWith(springListener);
+});
diff --git a/src/__tests__/useTimeout.test.ts b/src/__tests__/useTimeout.test.ts
index 905b4022..5a852f81 100644
--- a/src/__tests__/useTimeout.test.ts
+++ b/src/__tests__/useTimeout.test.ts
@@ -2,137 +2,135 @@ import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks'
 import { useTimeout } from '../index';
 import { UseTimeoutReturn } from '../useTimeout';
 
-describe('useTimeout', () => {
-  beforeAll(() => {
-    jest.useFakeTimers();
-  });
+beforeAll(() => {
+  jest.useFakeTimers();
+});
 
-  afterEach(() => {
-    jest.clearAllTimers();
-  });
+afterEach(() => {
+  jest.clearAllTimers();
+});
 
-  afterAll(() => {
-    jest.useRealTimers();
-  });
+afterAll(() => {
+  jest.useRealTimers();
+});
 
-  it('should be defined', () => {
-    expect(useTimeout).toBeDefined();
-  });
+it('should be defined', () => {
+  expect(useTimeout).toBeDefined();
+});
 
-  it('should return three functions', () => {
-    const hook = renderHook(() => useTimeout(5));
+it('should return three functions', () => {
+  const hook = renderHook(() => useTimeout(5));
 
-    expect(hook.result.current.length).toBe(3);
-    expect(typeof hook.result.current[0]).toBe('function');
-    expect(typeof hook.result.current[1]).toBe('function');
-    expect(typeof hook.result.current[2]).toBe('function');
-  });
+  expect(hook.result.current.length).toBe(3);
+  expect(typeof hook.result.current[0]).toBe('function');
+  expect(typeof hook.result.current[1]).toBe('function');
+  expect(typeof hook.result.current[2]).toBe('function');
+});
 
-  function getHook(ms: number = 5): [jest.Mock, RenderHookResult<{ delay: number }, UseTimeoutReturn>] {
-    const spy = jest.fn();
-    return [
-      spy,
-      renderHook(
-        ({ delay = 5 }) => {
-          spy();
-          return useTimeout(delay);
-        },
-        { initialProps: { delay: ms } }
-      ),
-    ];
-  }
-
-  it('should re-render component after given amount of time', done => {
-    const [spy, hook] = getHook();
-    expect(spy).toHaveBeenCalledTimes(1);
-    hook.waitForNextUpdate().then(() => {
-      expect(spy).toHaveBeenCalledTimes(2);
-      done();
-    });
-    jest.advanceTimersByTime(5);
+function getHook(ms: number = 5): [jest.Mock, RenderHookResult<{ delay: number }, UseTimeoutReturn>] {
+  const spy = jest.fn();
+  return [
+    spy,
+    renderHook(
+      ({ delay = 5 }) => {
+        spy();
+        return useTimeout(delay);
+      },
+      { initialProps: { delay: ms } }
+    ),
+  ];
+}
+
+it('should re-render component after given amount of time', done => {
+  const [spy, hook] = getHook();
+  expect(spy).toHaveBeenCalledTimes(1);
+  hook.waitForNextUpdate().then(() => {
+    expect(spy).toHaveBeenCalledTimes(2);
+    done();
   });
+  jest.advanceTimersByTime(5);
+});
 
-  it('should cancel timeout on unmount', () => {
-    const [spy, hook] = getHook();
+it('should cancel timeout on unmount', () => {
+  const [spy, hook] = getHook();
 
-    expect(spy).toHaveBeenCalledTimes(1);
-    hook.unmount();
-    jest.advanceTimersByTime(5);
-    expect(spy).toHaveBeenCalledTimes(1);
-  });
+  expect(spy).toHaveBeenCalledTimes(1);
+  hook.unmount();
+  jest.advanceTimersByTime(5);
+  expect(spy).toHaveBeenCalledTimes(1);
+});
 
-  it('first function should return actual state of timeout', done => {
-    let [, hook] = getHook();
-    let [isReady] = hook.result.current;
+it('first function should return actual state of timeout', done => {
+  let [, hook] = getHook();
+  let [isReady] = hook.result.current;
 
-    expect(isReady()).toBe(false);
-    hook.unmount();
-    expect(isReady()).toBe(null);
+  expect(isReady()).toBe(false);
+  hook.unmount();
+  expect(isReady()).toBe(null);
 
-    [, hook] = getHook();
-    [isReady] = hook.result.current;
-    hook.waitForNextUpdate().then(() => {
-      expect(isReady()).toBe(true);
+  [, hook] = getHook();
+  [isReady] = hook.result.current;
+  hook.waitForNextUpdate().then(() => {
+    expect(isReady()).toBe(true);
 
-      done();
-    });
-    jest.advanceTimersByTime(5);
+    done();
   });
+  jest.advanceTimersByTime(5);
+});
 
-  it('second function should cancel timeout', () => {
-    const [spy, hook] = getHook();
-    const [isReady, cancel] = hook.result.current;
-
-    expect(spy).toHaveBeenCalledTimes(1);
-    expect(isReady()).toBe(false);
+it('second function should cancel timeout', () => {
+  const [spy, hook] = getHook();
+  const [isReady, cancel] = hook.result.current;
 
-    act(() => {
-      cancel();
-    });
-    jest.advanceTimersByTime(5);
+  expect(spy).toHaveBeenCalledTimes(1);
+  expect(isReady()).toBe(false);
 
-    expect(spy).toHaveBeenCalledTimes(1);
-    expect(isReady()).toBe(null);
+  act(() => {
+    cancel();
   });
+  jest.advanceTimersByTime(5);
 
-  it('third function should reset timeout', done => {
-    const [spy, hook] = getHook();
-    const [isReady, cancel, reset] = hook.result.current;
+  expect(spy).toHaveBeenCalledTimes(1);
+  expect(isReady()).toBe(null);
+});
 
-    expect(isReady()).toBe(false);
+it('third function should reset timeout', done => {
+  const [spy, hook] = getHook();
+  const [isReady, cancel, reset] = hook.result.current;
 
-    act(() => {
-      cancel();
-    });
-    jest.advanceTimersByTime(5);
+  expect(isReady()).toBe(false);
 
-    expect(isReady()).toBe(null);
+  act(() => {
+    cancel();
+  });
+  jest.advanceTimersByTime(5);
 
-    act(() => {
-      reset();
-    });
-    expect(isReady()).toBe(false);
+  expect(isReady()).toBe(null);
 
-    hook.waitForNextUpdate().then(() => {
-      expect(spy).toHaveBeenCalledTimes(2);
-      expect(isReady()).toBe(true);
+  act(() => {
+    reset();
+  });
+  expect(isReady()).toBe(false);
 
-      done();
-    });
-    jest.advanceTimersByTime(5);
+  hook.waitForNextUpdate().then(() => {
+    expect(spy).toHaveBeenCalledTimes(2);
+    expect(isReady()).toBe(true);
+
+    done();
   });
+  jest.advanceTimersByTime(5);
+});
 
-  it('should reset timeout on delay change', done => {
-    const [spy, hook] = getHook(15);
+it('should reset timeout on delay change', done => {
+  const [spy, hook] = getHook(15);
 
-    expect(spy).toHaveBeenCalledTimes(1);
-    hook.rerender({ delay: 5 });
+  expect(spy).toHaveBeenCalledTimes(1);
+  hook.rerender({ delay: 5 });
 
-    hook.waitForNextUpdate().then(() => {
-      expect(spy).toHaveBeenCalledTimes(3);
+  hook.waitForNextUpdate().then(() => {
+    expect(spy).toHaveBeenCalledTimes(3);
 
-      done();
-    });
-    jest.advanceTimersByTime(15);
+    done();
   });
+  jest.advanceTimersByTime(15);
 });
diff --git a/src/__tests__/useTween.test.ts b/src/__tests__/useTween.test.ts
new file mode 100644
index 00000000..18a49833
--- /dev/null
+++ b/src/__tests__/useTween.test.ts
@@ -0,0 +1,56 @@
+import { renderHook } from '@testing-library/react-hooks';
+import useTween from '../useTween';
+import * as useRaf from '../useRaf';
+import { easing } from 'ts-easing';
+
+let spyUseRaf;
+let spyEasingInCirc;
+let spyEasingOutCirc;
+
+beforeEach(() => {
+  spyUseRaf = jest.spyOn(useRaf, 'default').mockReturnValue(17);
+  spyEasingInCirc = jest.spyOn(easing, 'inCirc').mockReturnValue(999999);
+  spyEasingOutCirc = jest.spyOn(easing, 'outCirc').mockReturnValue(101010);
+});
+
+afterEach(() => {
+  jest.restoreAllMocks();
+});
+
+it('should init corresponding utils with default values', () => {
+  const { result } = renderHook(() => useTween());
+
+  expect(result.current).toBe(999999);
+  expect(spyEasingInCirc).toHaveBeenCalledTimes(1);
+  expect(spyEasingInCirc).toHaveBeenCalledWith(17);
+  expect(spyUseRaf).toHaveBeenCalledTimes(1);
+  expect(spyUseRaf).toHaveBeenCalledWith(200, 0);
+});
+
+it('should init corresponding utils with custom values', () => {
+  const { result } = renderHook(() => useTween('outCirc', 500, 15));
+
+  expect(result.current).toBe(101010);
+  expect(spyEasingOutCirc).toHaveBeenCalledTimes(1);
+  expect(spyEasingOutCirc).toHaveBeenCalledWith(17);
+  expect(spyUseRaf).toHaveBeenCalledTimes(1);
+  expect(spyUseRaf).toHaveBeenCalledWith(500, 15);
+});
+
+describe('when invalid easing name is provided', () => {
+  beforeEach(() => {
+    jest.spyOn(console, 'error').mockImplementation(() => {});
+    jest.spyOn(console, 'trace').mockImplementation(() => {});
+  });
+
+  it('should log an error', () => {
+    const { result } = renderHook(() => useTween('grijanderl'));
+
+    expect(result.current).toBe(0);
+    expect(console.error).toHaveBeenCalledTimes(1);
+    expect(console.error).toHaveBeenCalledWith(
+      expect.stringContaining('useTween() expected "easingName" property to be a valid easing function name')
+    );
+    expect(console.trace).toHaveBeenCalledTimes(1);
+  });
+});
diff --git a/src/__tests__/useUpdate.test.ts b/src/__tests__/useUpdate.test.ts
new file mode 100644
index 00000000..e7d35191
--- /dev/null
+++ b/src/__tests__/useUpdate.test.ts
@@ -0,0 +1,26 @@
+import { act, renderHook } from '@testing-library/react-hooks';
+import useUpdate from '../useUpdate';
+
+it('should init update function', () => {
+  const { result } = renderHook(() => useUpdate());
+  const update = result.current;
+
+  expect(update).toBeInstanceOf(Function);
+});
+
+it('should forces a re-render every time update function is called', () => {
+  let renderCount = 0;
+  const { result } = renderHook(() => {
+    renderCount++;
+    return useUpdate();
+  });
+  const update = result.current;
+
+  expect(renderCount).toBe(1);
+
+  act(() => update());
+  expect(renderCount).toBe(2);
+
+  act(() => update());
+  expect(renderCount).toBe(3);
+});