Avi Sharvit
QE CAMP
WE CAN REPLACE SMALL UI PARTS AND GORW OVER TIME
/* login.js */
import authenticateUserPassword from './authenticateUserPassword';
import getUser from './getUser';
const login = (username, password) => {
const user = getUser(username);
if (!authenticateUserPassword(user, password)) {
throw new Error('Wrong password');
}
return user;
};
export default login;
/* login.test.js */
import login from './login';
import authenticateUserPassword from './authenticateUserPassword';
import getUser from './getUser';
jest.mock('./authenticateUserPassword');
jest.mock('./getUser');
In unit-testing, we mock every single imported module
describe('Auth - login', () => {
it('should login with username and password', () => {
});
it('should throw error when login with wrong password', () => {
});
});
describe('Auth - login', () => {
const username = 'some-username';
const user = 'some-user';
const password = 'some-password';
getUser.mockImplementation(() => user);
it('should login with username and password', () => {
});
it('should throw error when login with wrong password', () => {
});
});
it('should login with username and password', () => {
authenticateUserPassword.mockImplementation(() => true);
const results = login(username, password);
expect(results).toEqual(user);
});
it('should throw error when login with wrong password', () => {
authenticateUserPassword.mockImplementation(() => false);
const doLogin = () => login(username, password);
expect(doLogin).toThrow(new Error('Wrong password'));
});
/* login.test.js */
import login from './login';
import authenticateUserPassword from './authenticateUserPassword';
import getUser from './getUser';
jest.mock('./authenticateUserPassword');
jest.mock('./getUser');
describe('Auth - login', () => {
const username = 'some-username';
const user = 'some-user';
const password = 'some-password';
getUser.mockImplementation(() => user);
it('should login with username and password', () => {
authenticateUserPassword.mockImplementation(() => true);
const results = login(username, password);
expect(results).toEqual(user);
});
it('should throw error when login with wrong password', () => {
authenticateUserPassword.mockImplementation(() => false);
const doLogin = () => login(username, password);
expect(doLogin).toThrow(new Error('Wrong password'));
});
});
/* UserProfile.js */
import React from 'react';
import UserAvatar from './components/UserAvatar';
import UserDetailsBox from './components/UserDetailsBox';
import UserPhotos from './components/UserPhotos';
import UserPosts from './components/UserPosts';
const UserProfile = ({ user, showAvatar, showPosts, showPhotos }) => (
<div className="user-profile">
<UserDetailsBox user={user} />
{showAvatar && <UserAvatar user={user} size="sm" />}
{showPosts && <UserPhotos user={user} count={5} sort="DESC" />}
{showPhotos && <UserPosts user={user} count={5} sort="DESC" />}
</div>
);
export default UserProfile;
/* UserProfile.test.js */
import React from 'react';
import toJson from 'enzyme-to-json';
import { mount } from 'enzyme';
import UserProfile from './UserProfile';
describe('UserProfile - component', () => {
it('should render UserProfile', () => {
const component = mount(<UserProfile user="some-user" />);
expect(toJson(component)).toMatchSnapshot();
});
});
<UserProfile
user="some-user"
>
<div
className="user-profile"
>
<UserDetailsBox
user="some-user"
>
<div
className="user-details-box"
>
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
</div>
</UserDetailsBox>
</div>
</UserProfile>
/* UserProfile.test.js */
import React from 'react';
import toJson from 'enzyme-to-json';
import { shallow } from 'enzyme';
import UserProfile from './UserProfile';
describe('UserProfile - component', () => {
it('should render UserProfile', () => {
const component = shallow(<UserProfile user="some-user" />);
expect(toJson(component)).toMatchSnapshot();
});
});
<div
className="user-profile"
>
<UserDetailsBox
user="some-user"
/>
</div>
/* UserProfile.test.js */
import React from 'react';
import toJson from 'enzyme-to-json';
import { shallow } from 'enzyme';
import UserProfile from './UserProfile';
describe('UserProfile - component', () => {
it('should render UserProfile', () => {
const component = shallow(<UserProfile user="some-user" />);
expect(toJson(component)).toMatchSnapshot();
});
it('should render UserProfile with avatar', () => {
const component = shallow(<UserProfile user="some-user" showAvatar />);
expect(toJson(component)).toMatchSnapshot();
});
it('should render UserProfile with posts and photos', () => {
const component = shallow(
<UserProfile user="some-user" showPosts showPhotos />
);
expect(toJson(component)).toMatchSnapshot();
});
});
Too much of the same code
/* UserProfile.test.js */
import { testComponentSnapshotsWithFixtures } from 'react-redux-test-utils';
import UserProfile from './UserProfile';
const fixtures = {
'should render UserProfile': {
user: 'some-user',
},
'should render UserProfile with avatar': {
user: 'some-user',
showAvatar: true,
},
'should render UserProfile with posts and photos': {
user: 'some-user',
showPosts: true,
showPhotos: true,
},
};
describe('UserProfile - component', () =>
testComponentSnapshotsWithFixtures(UserProfile, fixtures));
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UserProfile - component should render UserProfile 1`] = `
<div
className="user-profile"
>
<UserDetailsBox
user="some-user"
/>
</div>
`;
exports[`UserProfile - component should render UserProfile with avatar 1`] = `
<div
className="user-profile"
>
<UserDetailsBox
user="some-user"
/>
<UserAvatar
size="sm"
user="some-user"
/>
</div>
`;
exports[`UserProfile - component should render UserProfile with posts and photos 1`] = `
<div
className="user-profile"
>
<UserDetailsBox
user="some-user"
/>
<UserPhotos
count={5}
sort="DESC"
user="some-user"
/>
<UserPosts
count={5}
sort="DESC"
user="some-user"
/>
</div>
`;
const UserProfile = ({
user,
showAvatar,
showPosts,
showPhotos,
updateShowAvatar,
updateShowPhotos,
updateShowPosts,
}) => (
<div className="user-profile">
<UserDetailsBox user={user} />
{showAvatar && <UserAvatar user={user} size="sm" />}
{showPosts && <UserPhotos user={user} count={5} sort="DESC" />}
{showPhotos && <UserPosts user={user} count={5} sort="DESC" />}
<input
id="show-avatar-toggler"
type="checkbox"
checked={showAvatar}
onChange={e => updateShowAvatar(e.target.checked)}
/>
<input
id="show-photos-toggler"
type="checkbox"
checked={showPhotos}
onChange={e => updateShowPhotos(e.target.checked)}
/>
<input
id="show-posts-toggler"
type="checkbox"
checked={showPosts}
onChange={e => updateShowPosts(e.target.checked)}
/>
</div>
);
const UserProfile = (props) => { ... };
// map state to props
const mapStateToProps = state => ({
showAvatar: selectShowAvatar(state),
showPosts: selectShowPosts(state),
showPhotos: selectShowPhotos(state),
});
// export default UserProfile;
export default connect(mapStateToProps)(UserProfile);
components/UserProfile
├── index.js
├── UserProfileActions.js
├── UserProfileConstants.js
├── UserProfile.js
├── UserProfileReducer.js
├── UserProfileSelectors.js
└── __tests__
├── integration.test.js
├── UserProfileActions.test.js
├── UserProfileReducer.test.js
├── UserProfileSelectors.test.js
├── UserProfile.test.js
└── __snapshots__
├── integration.test.js.snap
├── UserProfileActions.test.js.snap
├── UserProfileReducer.test.js.snap
├── UserProfileSelectors.test.js.snap
└── UserProfile.test.js.snap
/* UserProfileActions.js */
import {
USER_PROFILE_UPDATE_AVATAR,
USER_PROFILE_UPDATE_POSTS,
USER_PROFILE_UPDATE_PHOTOS,
} from './UserProfileConstants';
export const updateShowAvatar = showAvatar => ({
type: USER_PROFILE_UPDATE_AVATAR,
payload: showAvatar,
});
export const updateShowPosts = showPosts => ({
type: USER_PROFILE_UPDATE_POSTS,
payload: showPosts,
});
export const updateShowPhotos = showPhotos => ({
type: USER_PROFILE_UPDATE_PHOTOS,
payload: showPhotos,
});
/* UserProfileActions.test.js */
import { testActionSnapshotWithFixtures } from 'react-redux-test-utils';
import {
updateShowAvatar,
updateShowPosts,
updateShowPhotos,
} from '../UserProfileActions';
const fixtures = {
'should update-show-avatar': () => updateShowAvatar(true),
'should update-show-posts': () => updateShowPosts(true),
'should update-show-photos': () => updateShowPhotos(true),
};
describe('UserProfile - Actions', () =>
testActionSnapshotWithFixtures(fixtures));
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UserProfile - Actions should update-show-avatar 1`] = `
Object {
"payload": true,
"type": "USER_PROFILE_UPDATE_AVATAR",
}
`;
exports[`UserProfile - Actions should update-show-photos 1`] = `
Object {
"payload": true,
"type": "USER_PROFILE_UPDATE_PHOTOS",
}
`;
exports[`UserProfile - Actions should update-show-posts 1`] = `
Object {
"payload": true,
"type": "USER_PROFILE_UPDATE_POSTS",
}
`;
const initialState = Immutable({
showAvatar: false,
showPosts: false,
showPhotos: false,
});
export default (state = initialState, action) => {
const { payload } = action;
switch (action.type) {
case USER_PROFILE_UPDATE_AVATAR:
return state.set('showAvatar', payload);
case USER_PROFILE_UPDATE_POSTS:
return state.set('showPosts', payload);
case USER_PROFILE_UPDATE_PHOTOS:
return state.set('showPhotos', payload);
default:
return state;
}
};
const fixtures = {
'should return the initial state': {},
'should handle USER_PROFILE_UPDATE_AVATAR': {
action: {
type: USER_PROFILE_UPDATE_AVATAR,
payload: true,
},
},
'should handle USER_PROFILE_UPDATE_POSTS': {
action: {
type: USER_PROFILE_UPDATE_POSTS,
payload: true,
},
},
'should handle USER_PROFILE_UPDATE_PHOTOS': {
action: {
type: USER_PROFILE_UPDATE_PHOTOS,
payload: true,
},
},
};
describe('UserProfile - Reducer', () =>
testReducerSnapshotWithFixtures(reducer, fixtures));
exports[`UserProfile - Reducer should return the initial state 1`] = `
Object {
"showAvatar": false,
"showPhotos": false,
"showPosts": false,
}
`;
exports[`UserProfile - Reducer should handle USER_PROFILE_UPDATE_AVATAR 1`] = `
Object {
"showAvatar": true,
"showPhotos": false,
"showPosts": false,
}
`;
exports[`UserProfile - Reducer should handle USER_PROFILE_UPDATE_PHOTOS 1`] = `
Object {
"showAvatar": false,
"showPhotos": true,
"showPosts": false,
}
`;
exports[`UserProfile - Reducer should handle USER_PROFILE_UPDATE_POSTS 1`] = `
Object {
"showAvatar": false,
"showPhotos": false,
"showPosts": true,
}
`;
const selectState = state => state.userProfile;
export const selectShowAvatar = state => selectState(state).showAvatar;
export const selectShowPosts = state => selectState(state).showPosts;
export const selectShowPhotos = state => selectState(state).showPhotos;
const state = {
userProfile: {
showAvatar: 'showAvatar',
showPosts: 'showPosts',
showPhotos: 'showPhotos',
},
};
const fixtures = {
'should select show-avatar': () => selectShowAvatar(state),
'should select show-posts': () => selectShowPosts(state),
'should select show-photos': () => selectShowPhotos(state),
};
describe('UserProfile - Selectors', () =>
testSelectorsSnapshotWithFixtures(fixtures));
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UserProfile - Selectors should select show-avatar 1`] = `"showAvatar"`;
exports[`UserProfile - Selectors should select show-photos 1`] = `"showPhotos"`;
exports[`UserProfile - Selectors should select show-posts 1`] = `"showPosts"`;
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from './UserProfileActions';
import reducer from './UserProfileReducer';
import {
selectShowAvatar,
selectShowPosts,
selectShowPhotos,
} from './UserProfileSelectors';
import UserProfile from './UserProfile';
// map state to props
const mapStateToProps = state => ({
showAvatar: selectShowAvatar(state),
showPosts: selectShowPosts(state),
showPhotos: selectShowPhotos(state),
});
// map action dispatchers to props
const mapDispatchToProps = dispatch =>
bindActionCreators(actions, dispatch);
// export reducers
export const reducers = { userProfile: reducer };
// export connected component
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserProfile);
import { combineReducers } from 'redux';
import { reducers as userProfileReducers } from './UserProfile';
import { reducers as passwordStrengthReducers } from './PasswordStrength';
import { reducers as breadcrumbBarReducers } from './BreadcrumbBar';
import { reducers as searchBarReducers } from './SearchBar';
import { reducers as layoutReducers } from './Layout';
const reducers = combineReducers({
...userProfileReducers,
...passwordStrengthReducers,
...breadcrumbBarReducers,
...searchBarReducers,
...layoutReducers,
});
export default reducers;
import React from 'react';
import Page from '../Page';
import UserProfile from '../UserProfile';
const UserPage = ({ user }) => (
<Page title={user.name}>
<UserProfile user={user} />
</Page>
);
export default UserPage;
const integrationTestHelper = new IntegrationTestHelper(reducers);
const component = integrationTestHelper.mount(
<UserProfile user="some-user" />
);
// The user-avatar should not be shown
expect(component.exists('UserAvatar')).toEqual(false);
integrationTestHelper.takeStoreSnapshot('initial state');
exports[`UserProfile - Integration Test should flow: initial state 1`] = `
Object {
"userProfile": Object {
"showAvatar": false,
"showPhotos": false,
"showPosts": false,
},
}
`;
// trigger checkbox change
component
.find('input#show-avatar-toggler')
.simulate('change', { target: { checked: true } });
// The user-avatar should be shown now
expect(component.exists('UserAvatar')).toEqual(true);
integrationTestHelper.takeStoreAndLastActionSnapshot(
'Update to show the user-avatar'
);
exports[`UserProfile - Integration Test should flow: Update to show the user-avatar 1`] = `
Object {
"action": Array [
Array [
Object {
"payload": true,
"type": "USER_PROFILE_UPDATE_AVATAR",
},
],
],
"state": Object {
"userProfile": Object {
"showAvatar": true,
"showPhotos": false,
"showPosts": false,
},
},
}
`;
import React from 'react';
import { IntegrationTestHelper } from 'react-redux-test-utils';
import UserProfile, { reducers } from '../index';
describe('UserProfile - Integration Test', () => {
it('should flow', () => {
const integrationTestHelper = new IntegrationTestHelper(reducers);
const component = integrationTestHelper.mount(
<UserProfile user="some-user" />
);
// The user-avatar should not be shown
expect(component.exists('UserAvatar')).toEqual(false);
integrationTestHelper.takeStoreSnapshot('initial state');
// trigger checkbox change
component
.find('input#show-avatar-toggler')
.simulate('change', { target: { checked: true } });
// The user-avatar should be shown now
expect(component.exists('UserAvatar')).toEqual(true);
integrationTestHelper.takeStoreAndLastActionSnapshot(
'Update to show the user-avatar'
);
});
});
Fast, easy and reliable testing for anything that runs in a browser.
A node.js tool to automate
end-to-end web testing.
Write tests in JS or TypeScript, run them and view results
What have we found
npm install --save-dev cypress
"scripts": {
...
"deploy-storybook": "storybook-to-ghpages",
"postinstall": "node ./script/npm_install_plugins.js",
"analyze": "./script/webpack-analyze",
+ "cypress:open": "cypress open"
},
language: node_js
node_js:
- 9.11.2 # choose your version
addons:
postgresql: "9.4" # choose your version
services: # redis is not necessary, https://redis.io/
- postgresql
cache:
bundler: true # cache for bundler
npm: true # cache for npm
directories:
- /home/travis/.cache/Cypress # cache for bundler in node environement # cache travis
- node_modules
before_install:
- sudo apt-get install libsystemd-journal-dev
- sudo apt-get build-dep vagrant ruby-libvirt
- sudo apt-get install qemu libvirt-bin ebtables dnsmasq
- sudo apt-get install libxslt-dev libxml2-dev libvirt-dev zlib1g-dev ruby-dev
- gem install bundler # install bundler
- bundle install # install dependancies in your gemfile
install:
- npm install # install npm
before_script:
- psql -c 'create database travis_ci_test;' -U postgres # create database test
- cp config/database.yml.travis config/database.yml
- RAILS_ENV=test bundle exec rake db:migrate # create database with table
- SEED_ADMIN_PASSWORD=1234 RAILS_ENV=test bundle exec rake db:seed # launch your seed
- RAILS_ENV=test bundle exec rake assets:precompile # compile assets js
- RAILS_ENV=test bundle exec rake webpack:compile
- RAILS_ENV=test bundle exec foreman start rails & # launch server rails
- sleep 12
script:
- cypress run --record --key 9de1d57d-763b-4a9e-a5a6-c7eee4983ec7 # launch your frontend test with Cypress
describe('Layout', () => {
beforeEach(() => {
cy.visit('http://localhost:5000');
});
it('should hover all menu items', () => { });
it('should contain taxonomies dropdown', () => { });
it('should contain notfication drawer', () => { });
it('should contain user dropdwon', () => { });
it('should switch to mobile-view', () => { });
});
it('should hover all menu items', () => {
cy.get('.fa-tachometer').trigger('mouseover');
cy.contains('Dashboard').should('be.visible');
cy.get('.fa-server').trigger('mouseover');
cy.contains('All Hosts').should('be.visible');
cy.get('.fa-wrench').trigger('mouseover');
cy.contains('Host Group').should('be.visible');
cy.get('.pficon-network').trigger('mouseover');
cy.contains('Smart Proxies').should('be.visible');
cy.get('.fa-cog').trigger('mouseover');
cy.contains('Users').should('be.visible');
});
it('should contain user dropdwon', () => {
cy.get('#account_menu').click();
cy.contains('My Account').should('be.visible');
});
it('should switch to mobile-view', () => {
cy.viewport(550, 750);
cy.contains('Toggle navigation').click();
cy.get('.visible-xs-block .fa-user').click();
cy.contains('My Account').should('be.visible');
});