Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFR] Permission unit testing (doc + example) #2728

Merged
merged 2 commits into from
Jan 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 98 additions & 26 deletions docs/UnitTesting.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ title: "Unit Testing"

# Unit Testing

## Testing custom create and edit views
By default, react-admin acts as a declarative admin configuration: list some resources, define their controllers and, plug some built-in components or your own to define their fields or inputs.

When creating your customised create and edit views you may want to unit test those views.
Thus, unit testing isn't really needed nor recommended at first, because the internal API of the framework is already tested by its maintainers and each custom component can be tested by its own by mocking react-admin. ([see how to do so with Jest](https://jestjs.io/docs/en/manual-mocks#mocking-node-modules))

One issue you may run into when attempting to render your component via enzyme, is that you need to provide the component with the expected props contained within the react-admin redux store.
On the contrary, it is recommended to write end-to-end tests to secure your most common scenario at least.

That being said, there are still some cases, listed below, where a unit test can be useful.

## Testing Custom Views

One issue you may run into when attempting to render custom `Create` or `Edit` views is that you need to provide the component with the expected props contained within the react-admin redux store.

Luckily, react-admin provides access to a `TestContext` wrapper component that can be used to initialise your component with many of the expected react-admin props:

Expand Down Expand Up @@ -46,39 +52,105 @@ You can then provide additional props, as needed, to your component (such as the

At this point, your component should `mount` without errors and you can unit test your component.


## Enabling reducers to ensure actions are dispatched

If you component relies on a a reducer, e.g. redux-form submission, you can enable reducers using the `enableReducers` prop:

```jsx harmony
myCustomEditView = mount(
<TestContext enableReducers>
<MyCustomEditView />
</TestContext>
);
```jsx
myCustomEditView = mount(
<TestContext enableReducers>
<MyCustomEditView />
</TestContext>
);
```

This means that reducers will work as they will within the app. For example, you can now submit a form and redux-form will cause a re-render of your component.


## Spying on the store 'dispatch'

If you are using `mapDispatch` within connected components, it is likely you will want to test that actions have been dispatched with the correct arguments. You can return the `store` being used within the tests using a `renderProp`.

```jsx harmony
let dispatchSpy;
myCustomEditView = mount(
<TestContext>
{({ store }) => {
dispatchSpy = jest.spyOn(store, 'dispatch');
return <MyCustomEditView />
}}
</TestContext>,
);

it('should send the user to another url', () => {
myCustomEditView.find('.next-button').simulate('click');
expect(dispatchSpy).toHaveBeenCalledWith(`/next-url`);
If you are using `mapDispatch` within connected components, it is likely you will want to test that actions have been dispatched with the correct arguments. You can return the `store` being used within the tests using a `renderProp`.

```jsx
let dispatchSpy;
myCustomEditView = mount(
<TestContext>
{({ store }) => {
dispatchSpy = jest.spyOn(store, 'dispatch');
return <MyCustomEditView />
}}
</TestContext>,
);

it('should send the user to another url', () => {
myCustomEditView.find('.next-button').simulate('click');
expect(dispatchSpy).toHaveBeenCalledWith(`/next-url`);
});
```


## Testing Permissions

As explained on the [Authorization page](./Authorization.md), it's possible to manage permissions via the authentication provider in order to filter page and fields the users can see.

In order to avoid regressions and make the design explicit to your co-workers, it's better to unit test which fields is supposed to be displayed or hidden for each permission.

Here is an example with Jest and Enzyme, which is testing the [User `show` page of the simple example](https://github.com/marmelab/react-admin/blob/master/examples/simple/src/users/UserShow.js).

```jsx
// UserShow.spec.js
import React from 'react';
import { shallow } from 'enzyme';
import { Tab, TextField } from 'react-admin';

import UserShow from './UserShow';

describe('UserShow', () => {
describe('As User', () => {
it('should display one tab', () => {
const wrapper = shallow(<UserShow permissions="user" />);

const tab = wrapper.find(Tab);
expect(tab.length).toBe(1);
});

it('should show the user identity in the first tab', () => {
const wrapper = shallow(<UserShow permissions="user" />);

const tab = wrapper.find(Tab);
const fields = tab.find(TextField);

expect(fields.at(0).prop('source')).toBe('id');
expect(fields.at(1).prop('source')).toBe('name');
});
});

describe('As Admin', () => {
it('should display two tabs', () => {
const wrapper = shallow(<UserShow permissions="admin" />);

const tabs = wrapper.find(Tab);
expect(tabs.length).toBe(2);
});

it('should show the user identity in the first tab', () => {
const wrapper = shallow(<UserShow permissions="admin" />);

const tabs = wrapper.find(Tab);
const fields = tabs.at(0).find(TextField);

expect(fields.at(0).prop('source')).toBe('id');
expect(fields.at(1).prop('source')).toBe('name');
});

it('should show the user role in the second tab', () => {
const wrapper = shallow(<UserShow permissions="admin" />);

const tabs = wrapper.find(Tab);
const fields = tabs.at(1).find(TextField);

expect(fields.at(0).prop('source')).toBe('role');
});
});
});
```
77 changes: 77 additions & 0 deletions examples/simple/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const path = require('path');

module.exports = {
setupFiles: ['<rootDir>/src/test/configureEnzyme.js'],
moduleNameMapper: {
'ra-core': path.join(
__dirname,
'..',
'..',
'packages',
'ra-core',
'src'
),
'ra-ui-materialui': path.join(
__dirname,
'..',
'..',
'packages',
'ra-ui-materialui',
'src'
),
'react-admin': path.join(
__dirname,
'..',
'..',
'packages',
'react-admin',
'src'
),
'ra-data-fakerest': path.join(
__dirname,
'..',
'..',
'packages',
'ra-data-fakerest',
'src'
),
'ra-input-rich-text': path.join(
__dirname,
'..',
'..',
'packages',
'ra-input-rich-text',
'src'
),
'ra-tree-core': path.join(
__dirname,
'..',
'..',
'packages',
'ra-tree-core',
'src'
),
'ra-tree-ui-materialui': path.join(
__dirname,
'..',
'..',
'packages',
'ra-tree-ui-materialui',
'src'
),
'ra-tree-language-english': path.join(
__dirname,
'..',
'..',
'packages',
'ra-tree-language-english'
),
'ra-tree-language-french': path.join(
__dirname,
'..',
'..',
'packages',
'ra-tree-language-french'
),
}
};
5 changes: 4 additions & 1 deletion examples/simple/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"start": "./node_modules/.bin/webpack-dev-server --progress --color --hot --watch --mode development",
"serve": "./node_modules/.bin/serve --listen 8080 ./dist",
"build": "./node_modules/.bin/webpack-cli --color --mode development --hide-modules true",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "jest"
},
"author": "",
"license": "MIT",
Expand All @@ -24,9 +24,12 @@
"@material-ui/core": "~1.4.0",
"@material-ui/icons": "~1.1.0",
"babel-loader": "^8.0.4",
"enzyme": "^3.8.0",
"enzyme-adapter-react-16": "^1.7.1",
"hard-source-webpack-plugin": "^0.11.2",
"html-loader": "~0.5.5",
"html-webpack-plugin": "~3.2.0",
"jest": "^23.6.0",
"ra-data-fakerest": "^2.0.0",
"ra-input-rich-text": "^2.0.0",
"ra-language-english": "^2.0.0",
Expand Down
4 changes: 4 additions & 0 deletions examples/simple/src/test/configureEnzyme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });
71 changes: 71 additions & 0 deletions examples/simple/src/users/UserShow.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Tab, TextField } from 'react-admin';

import UserShow from './UserShow';

// Mock React Admin so we don't have to resolve it
// Do not take this into account for the example
// The tests are valid if react-admin package is installed
jest.mock('react-admin', () => ({
Show: ({ children }) => <div />,
Tab: ({ children }) => <div />,
TabbedShowLayout: ({ children }) => <div />,
TextField: ({ children }) => <div />,
translate: x => x,
}));

// Supress PropTypes warning by default
const defaultProps = {
location: {},
match: {},
};

describe('UserShow', () => {
describe('As User', () => {
it('should display one tab', () => {
const wrapper = shallow(<UserShow {...defaultProps} permissions="user" />);

const tab = wrapper.find(Tab);
expect(tab.length).toBe(1);
});

it('should show the user identity in the first tab', () => {
const wrapper = shallow(<UserShow {...defaultProps} permissions="user" />);

const tab = wrapper.find(Tab);
const fields = tab.find(TextField);

expect(fields.at(0).prop('source')).toBe('id');
expect(fields.at(1).prop('source')).toBe('name');
});
});

describe('As Admin', () => {
it('should display two tabs', () => {
const wrapper = shallow(<UserShow {...defaultProps} permissions="admin" />);

const tabs = wrapper.find(Tab);
expect(tabs.length).toBe(2);
});

it('should show the user identity in the first tab', () => {
const wrapper = shallow(<UserShow {...defaultProps} permissions="admin" />);

const tabs = wrapper.find(Tab);
const fields = tabs.at(0).find(TextField);

expect(fields.at(0).prop('source')).toBe('id');
expect(fields.at(1).prop('source')).toBe('name');
});

it('should show the user role in the second tab', () => {
const wrapper = shallow(<UserShow {...defaultProps} permissions="admin" />);

const tabs = wrapper.find(Tab);
const fields = tabs.at(1).find(TextField);

expect(fields.at(0).prop('source')).toBe('role');
});
});
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"testPathIgnorePatterns": [
"/node_modules/",
"/lib/",
"/esm/"
"/esm/",
"/examples/simple/"
],
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$"
Expand Down
Loading