When mobx store is used from useContext() hook what will be best approach to write unit test case? Should we use enzyme or react testing library? Here Provider or #inject() is not used
Store design:
//StoreA
export class StoreA {
rootStore: RootStore;
constructor(rootStore: RootStore) {
this.rootStore = rootStore;
}
//StoreA implementations...
}
//StoreB
export class StoreB {
rootStore: RootStore;
constructor(rootStore: RootStore) {
this.rootStore = rootStore;
}
//StoreB implementations...
}
//RootStore
export class RootStore {
StoreA: StoreA;
StoreB: StoreB;
constructor() {
this.storeA = new storeA(this);
this.storeB = new storeB(this);
}
}
export const RootStoreContext = createContext(new RootStore());
In component store is used from useContext hook
const SomeComponent = ({}) => {
const rootStore = useContext(RootStoreContext);
const { storeAdata, storeAmethods } = rootStore.storeA;
//Component code....
}
Stackblitz Link:
https://stackblitz.com/edit/react-ts-qec5vu?file=Hello.tsx
In below scenario,
How to write Unit test case only for individual stores (its having circular dependency)
If react testing library used, how to mock stores and useContext?
Is it possible to use enzyme?
Which lib's are suitable for writing UTC?
If you are just unit testing the stores, don't use components. Instantiate the stores directly and test them.
If you are testing component interaction (integration) then I would suggest you use the react testing library. Create the context provider and wrap you components with the provider for each test.
more info: https://testing-library.com/docs/example-react-context
Related
How to use redux in class component using react with typescript.
1.) useDispatch,useSelector how to use it in class base components react typescript
And here I get the data from store.tsx using useSelector but this is a class component(App.tsx)
then, I dispatched here(App.jsx)
You have to use higher order component connect which is provided by react-redux library.
first import it
import { connect } from "react-redux";
then to access state use the function
const mapStateToProps = (props) => {
return {
api: props.api,
};
};
and for action use
const mapDispatchToProps = (dispatch) => {
return {
action: () => dispatch(action),
};
};
and at the end export it like
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
to use it simply access through props, for example
const data = props.api
and same for dispatch
function addData () {
props.action()
}
I'm working on shared libraries for multiple projects. And I need to have a flexible way of using hooks not depending on implementation. So I'm using React Context API as a dependency injection pattern.
Simplified example:
const DIContext = React.createContext();
// multiple implementations of common hook useConfig
const useReduxConfig = () => useSelector(getConfig);
const useStaticConfig = () => ({foo: 'bar'})
const useFetchConfig = () => ... //ie fetch config for server
// this will be always static value
const DIValue={
useConfig: useReduxConfig // ca be any of implementation
}
const App = () => {
return <DiContext.Provider value={DIValue}><SharedComponent /></DiContext.Provider>
}
const SharedComponent = () => {
// shared component does not care about the implementation of hook
const {useConfig} = useContext(DiContext)
const config = useConfig();
return <div>{JSON.stringify(config)}</div>
}
What do you think, is it a good pattern ?
This question is opinion-based, here is my view.
I think DI overuses react context, see use cases for context.
I would not pass down some kind of common react custom hooks to descendants component using react context. Make things complicated.
Illustration from a testing perspective:
Yeah, the DI pattern is good for testing, you can create mock objects with the same interface to replace the real object injected. But this doesn't apply to react custom hooks, we'd better not mock custom hooks, most custom hooks will rely on React built-in hooks, such as useEffect, useState, and mocking custom hooks that use these built-in hooks will result in the incorrect lifecycle and state update functionality. When testing, you still need to pass the real custom hooks to the context value rather than create mocks for these hooks. DI doesn't make testing easier as many articles said in this case.
E.g.
index.tsx:
import React, { useContext } from 'react';
import { useEffect } from 'react';
export const DIContext = React.createContext<any>({});
const useConfig = () => ({ foo: 'bar' });
// You should NOT mock useLifecycle, we need to use the real useEffect to perform component lifecycle.
const useLifecycle = () => {
useEffect(() => {
console.log('useLifecycle mount');
}, []);
};
export const DIValue = {
useConfig,
useLifecycle,
};
export const App = () => {
return (
<DIContext.Provider value={DIValue}>
<SharedComponent />
</DIContext.Provider>
);
};
export const SharedComponent = () => {
const { useLifecycle, useConfig } = useContext(DIContext);
const config = useConfig();
console.count('SharedComponent render');
useLifecycle();
useEffect(() => {
console.log('SharedComponent mount');
}, []);
return <div>{JSON.stringify(config)}</div>;
};
index.test.tsx:
import { render } from '#testing-library/react';
import React from 'react';
import { DIContext, DIValue, SharedComponent } from '.';
describe('DI via react context', () => {
test('should pass', () => {
render(
<DIContext.Provider value={DIValue}>
<SharedComponent />
</DIContext.Provider>,
);
});
});
For every component that uses context, you need to provide the same context and the real DIValue, NOT mock DIValue. If I use import rather than context, I don't need the context provider.
Although DI reduced the code for importing custom hooks in each component, the code for the context provider was added to the test code. Of course, you can create a test util function named renderWithDIContext to DRY.
There is another scenario:
If the react custom hook is used by only one component, not a common one, the file location of such a custom hook is usually next to the component file, and the component is imported directly through import, it's quick and easy.
E.g.
SharedComponent.tsx:
import { useA } from './hooks';
import { useContext } from 'react';
const SharedComponent = () => {
const a = useA();
const { useConfig } = useContext(DiContext);
const config = useConfig();
//...
}
The useA hook is only used by one component, so there is no need to import the hook into the context provider file and then put it in the context value to pass down. And, other components don't need this hook.
The most you can do are to put some common hooks in the context value.
This scenario mixes the custom hook used by the component itself with the common hook passed down through the context.
Keep it simple. Just import and use it.
import { useA } from './hooks';
import { useConfig } from '#/common/hooks';
const SharedComponent = () => {
const a = useA();
const config = useConfig();
//...
}
Besides, putting every common hook in context value and passing them down violate interface-segregation principles. The context interface is very large, but each component only wants to know about the hooks that are of interest to them, such shrunken interfaces are also called role interfaces. Of course, you can split the large context into smaller contexts. It will increase code and the need to define these role interfaces. We also need to decide how to compose these context providers with our components. Put all context providers in the root App component(application level) or page component(page-level) or module level. This adds complexity.
Am using https://www.npmjs.com/package/#azure/msal-react in my react project. The library provides hooks to perform auth very easliy.
So in functional component, for getting access token,
if (account && inProgress === "none") {
instance.acquireTokenSilent({
...loginRequest,
account: account
}).then((response) => {
callMsGraph(response.accessToken).then(response => setGraphData(response));
});
}
needs to be called where const { instance, accounts, inProgress } = useMsal(); is used.
But I need to call api to fetch data from class components.
So, how to achieve the same functionality in class component
You cannot access the msal-react hooks inside your class components, so to do this you would either need to access the raw context, or wrap with a higher order component.
The following takes the examples from documentation, modified to your question about accessing instance, accounts, inProgress for subsequent API calls.
Consuming Raw Context
import React from "react";
import { MsalContext } from "#azure/msal-react";
class YourClassComponent extends React.Component {
static contextType = MsalContext;
render() {
const msalInstance = this.context.instance;
const msalAccounts = this.context.accounts;
const msalInProgress = this.context.inProgress;
// rest of your render code
}
}
}
Using Higher-Order-Component
import React from "react";
import { withMsal } from "#azure/msal-react";
class YourClassComponent extends React.Component {
render() {
const msalInstance = this.props.msalContext.instance;
const msalAccounts = this.props.msalContext.accounts;
const msalInProgress = this.props.msalContext.inProgress;
// rest of your render code
}
}
export default YourWrappedComponent = withMsal(YourClassComponent);
Either way works so personal preference really. I prefer accessing the raw context over wrapping for readability.
I was trying to upgrade to react 16+ and my existing unit tests are failing. The application runs fine, only unit tests fails.
connected-component:
const componentsWrapper = (Components, selectData) => {
class BaseComponent extends Component {
constructor(props) {
super(props);
this.state = {
saveMode: false,
updateMode: false
};
this.foo = () => { };
}
render() {
return (
<Components
{...this.props}
/>
);
}
}
}
const mapDispatchToProps = dispatch => {
return selectData.getDispatch(dispatch);
};
const mapStateToProps = state => {
return selectData.getStore(state);
};
return connect(mapStateToProps, mapDispatchToProps)(BaseComponent);
};
export default componentsWrapper;
unit test:
class MockListComponent extends Component {
render() {
return (<div>Fake List</div>);
}
}
Components = componentsWrapper(MockListComponent, selectComponents);
wrapper = shallow(<Components store={store} />).dive();
instance = wrapper.instance()
// Here instance is null hence the rest will fail.
instance.foo = jest.fn();
instance is null because "Connect" component is functional or stateless. Source
NOTE: With React 16 and above, instance() returns null for stateless functional components.
I don't know how to get the instance to not be null or maybe refactor the code. I appreciate any help or hint.
These are the library versions I am trying to use:
react#16.2.9
enzyme#3.3.0
redux#4.0.5
I am not sure what is the exact behaviour you are trying to test, but in many cases, there is no need to explicitly 'test' the entire connected component unless you are planning to do integrated testing or functional testing.
To fix the issue you are facing, I would recommend you to export the child component (take note of export on BaseComponent):
const componentsWrapper = (Components, selectData) => {
export class BaseComponent extends Component {
// ... rest of the logic
}
}
Then, on your test file, we use enzyme's .find() to find the node that matches the selector. In this case, our selector is BaseComponent.
const wrapper = shallow(<Components store={store} />)
const wrapperInstance = wrapper.dive().find(BaseComponent).instance();
From there, you will have access to BaseComponent's class instance, and you can use it to call its methods, or spy on its methods.
The reason why you are facing the error stated on the question is explained on the enzyme documentation:
With React 16 and above, instance() returns null for stateless
functional components.
Therefore, you can only call .instance() on the component itself, rather than the entire connected component.
I have a component SampleComponent that mounts another "connected component" (i.e. container). When I try to test SampleComponent by mounting (since I need the componentDidMount), I get the error:
Invariant Violation: Could not find "store" in either the context or
props of "Connect(ContainerComponent)". Either wrap the root component
in a , or explicitly pass "store" as a prop to
"Connect(ContainerComponent)".
What's the best way of testing this?
Enzyme's mount takes optional parameters. The two that are necessary for what you need are
options.context: (Object [optional]): Context to be passed into the component
options.childContextTypes: (Object [optional]): Merged contextTypes for all children of the wrapper
You would mount SampleComponent with an options object like so:
const store = {
subscribe: () => {},
dispatch: () => {},
getState: () => ({ ... whatever state you need to pass in ... })
}
const options = {
context: { store },
childContextTypes: { store: React.PropTypes.object.isRequired }
}
const _wrapper = mount(<SampleComponent {...defaultProps} />, options)
Now your SampleComponent will pass the context you provided down to the connected component.
What I essentially did was bring in my redux store (and Provider) and wrapped it in a utility component as follows:
export const CustomProvider = ({ children }) => {
return (
<Provider store={store}>
{children}
</Provider>
);
};
then, I mount the SampleComponent and run tests against it:
it('contains <ChildComponent/> Component', () => {
const wrapper = mount(
<CustomProvider>
<SampleComponent {...defaultProps} />
</CustomProvider>
);
expect(wrapper.find(ChildComponent)).to.have.length(1);
});
Option 1)
You can wrap the container component with React-Redux's Provider component within your test. So with this approach, you actually reference the store, pass it to the Provider, and compose your component under test inside. The advantage of this approach is you can actually create a custom store for the test. This approach is useful if you want to test the Redux-related portions of your component.
Option 2)
Maybe you don't care about testing the Redux-related pieces. If you're merely interested in testing the component's rendering and local state-related behaviors, you can simply add a named export for the unconnected plain version of your component. And just to clarify when you add the "export" keyword to your class basically you are saying that now the class could be imported in 2 ways either with curly braces {} or not. example:
export class MyComponent extends React.Component{ render(){ ... }}
...
export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
later on your test file:
import MyComponent from 'your-path/MyComponent'; // it needs a store because you use "default export" with connect
import {MyComponent} from 'your-path/MyComponent'; // don't need store because you use "export" on top of your class.
I hope helps anyone out there.
There is also the option to use redux-mock-store.
A mock store for testing Redux async action creators and middleware. The mock store will create an array of dispatched actions which serve as an action log for tests.
The mock store provides the necessary methods on the store object which are required for Redux.
You can specify optional middlewares and your app specific initial state.
import configureStore from 'redux-mock-store'
const middlewares = []
const mockStore = configureStore(middlewares)
const initialState = {}
const store = mockStore(initialState)
const wrapper = mount(<SampleComponent store={store}/>)
You can use name export to solve this problem:
You should have:
class SampleComponent extends React.Component{
...
render(){
<div></div>
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SampleComponent)
You can add a export before class:
export class SampleComponent extends React.Component{
and import this component with no redux store:
import { SampleComponent } from 'your-path/SampleComponent';
With this solution you don't need to import store to your test files.
in an attempt to make the use of decorator syntax more testable I made this:
https://www.npmjs.com/package/babel-plugin-undecorate
input:
#anyOldClassDecorator
export class AnyOldClass {
#anyOldMethodDecorator
method() {
console.log('hello');
}
}
output:
#anyOldClassDecorator
export class AnyOldClass {
#anyOldMethodDecorator
method() {
console.log('hello');
}
}
export class __undecorated__AnyOldClass {
method() {
console.log('hello');
}
}
Hopefully this can provide a solid Option 3!