constate
constate copied to clipboard
Added `Provider.FromValue`, to enable mocking in Storybook and Unit Tests
I have started using constate
to separate my data-fetching logic from my UI, and it's perfect for the job! Thank you.
However, in Storybook and in Unit tests, I'd like to be able to manually provide (mock) values to the context provider.
So, in this PR, I've exposed a property on the Provider
called Provider.FromValue
, which is just a Provider that ignores the hook data and uses the provided data instead.
Storybook example:
const [CounterProvider, useCounterContext] = constate(useCounter);
export const withMockedValues = <>
<CounterProvider.FromValue value={{ count: 0, increment: action('increment') }}>
<Increment />
<Count />
</CounterProvider.FromValue>
</>;
React Testing Library example:
test("increment gets called", () => {
const mockCounter = { count: 0, increment: jest.fn() };
const MockProvider: React.FC = ({ children }) => (
<CounterProvider.FromValue value={mockCounter}>
{children}
</CounterProvider.FromValue>
);
const { getByText } = render(
<div>
<Increment />
<Count />
</div>,
{ wrapper: MockProvider }
);
fireEvent.click(getByText("Increment"));
expect(getByText("10")).toBeDefined();
expect(mockCounter.increment).toHaveBeenCalledTimes(1);
});
I'm open to feedback, especially regarding naming things, so please let me know!
TODO:
- [x] Add
FromValue
feature - [x] Add unit tests
- [ ] Add documentation
@diegohaz Thoughts on this PR?
I’m facing exactly this same issue. Seems like a great solution!
@scottrippey hey, how are you solving this problem until this PR is not approved? Can you share a solution? :)
@luiznasciment0 this whole lib is just 1 file, so I just copied my fork into my project.
lol hahaha awesome! actually that's a pretty good solution!
Thanks for working on this.
There are several ways to deal with this problem. For example, a more explicit approach would be accepting the overrides through the hook/Provider props:
function useCounter(props) {
const [_count, _setCount] = useState(props.initialCount);
const count = props.count ?? _count;
const setCount = props.setCount || _setCount;
const _increment = useCallback(() => setCount((prevCount) => prevCount + 1), [setCount]);
const increment = props.increment || _increment;
return { count, increment };
}
const [CounterProvider, useCounterContext] = constate(useCounter);
<CounterProvider count={0} increment={action("increment")} />
I haven't tried it, but I guess you can create a wrapper around constate to abstract this logic.
Anyway, I don't think this library should encourage mocking the state like that, which usually leads to implementation detail tests, which is the case of the increment gets called
test example in the PR.
Codecov Report
Merging #127 (87894df) into master (48ac889) will not change coverage. The diff coverage is
100.00%
.
@@ Coverage Diff @@
## master #127 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 1 1
Lines 31 34 +3
Branches 5 5
=========================================
+ Hits 31 34 +3
Impacted Files | Coverage Δ | |
---|---|---|
src/index.tsx | 100.00% <100.00%> (ø) |
Continue to review full report at Codecov.
Legend - Click here to learn more
Δ = absolute <relative> (impact)
,ø = not affected
,? = missing data
Powered by Codecov. Last update 48ac889...87894df. Read the comment docs.
My intent with this PR is just to expose the Provider
externally, so that it could be used for whatever purposes a user might need. I think there are many practical use-cases; in my case, it's extremely convenient for mocking and testing.
I don't think this makes constate
any more complex, and apart from type improvements, this only required about 3 lines of code and has no runtime overhead. Please reconsider supporting this feature! Thanks :)
FWIW, there is an alternative approach, similar to what you described. I've authored react-overridable-hooks
which wraps hooks, making them overridable via an override Provider.
// Normal custom hook:
const useCounterHook = () => ...;
// Wrap it:
export const [ useCounter, CounterProvider ] = overridableHook(useCounterHook);
// Use it like normal:
const Counter = () => { const { count, increment } = useCounter(); return <span>{count}</span> };
// Mock it in tests/storybook/etc:
const Test = <CounterProvider value={{ count: 5 }}> <Counter /> </CounterProvider>
This plays nicely with constate
, but ultimately, I could just be using constate
if the Provider was exposed!
Just came here looking for the exact solution @scottrippey is proposing in the PR.
As someone who inherited this library as dependency, I can't believe there is no other way to mock or inject the value into provider. The solution proposed by @diegohaz is just ... ugly.