Redux: How to test a connected component? - unit-testing

I am using Enzyme to unit test my React components. I understand that in order to test the raw unconnected component I'd have to just export it and test it (I've done that). I have managed to write a test for the connected component but I am really not sure if this's the right way and also what exactly would I want to test for the connected component.
Container.jsx
import {connect} from 'react-redux';
import Login from './Login.jsx';
import * as loginActions from './login.actions';
const mapStateToProps = state => ({
auth: state.auth
});
const mapDispatchToProps = dispatch => ({
loginUser: credentials => dispatch(loginActions.loginUser(credentials))
});
export default connect(mapStateToProps, mapDispatchToProps)(Login);
Container.test.js
import React from 'react';
import {Provider} from 'react-redux';
import {mount, shallow} from 'enzyme';
import {expect} from 'chai';
import LoginContainer from '../../src/login/login.container';
import Login from '../../src/login/Login';
describe('Container Login', () => {
it('should render the container component', () => {
const storeFake = state => ({
default: () => {
},
subscribe: () => {
},
dispatch: () => {
},
getState: () => ({ ...state })
});
const store = storeFake({
auth: {
sport: 'BASKETBALL'
}
});
const wrapper = mount(
<Provider store={store}>
<LoginContainer />
</Provider>
);
expect(wrapper.find(LoginContainer).length).to.equal(1);
const container = wrapper.find(LoginContainer);
expect(container.find(Login).length).to.equal(1);
expect(container.find(Login).props().auth).to.eql({ sport: 'BASKETBALL' });
});
});

This is an interesting question.
I usually do import both container and component to do the testing. For container testing I use, redux-mock-store. Component testing is for testing async functions. For instance in your case, login process is an async function using sinon stubs. Here is a snippet of the same,
import React from 'react';
import {Provider} from 'react-redux';
import {mount, shallow} from 'enzyme';
import {expect} from 'chai';
import LoginContainer from '../../src/login/login.container';
import Login from '../../src/login/Login';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { stub } from 'sinon';
const mockStore = configureMockStore([thunk]);
describe('Container Login', () => {
let store;
beforeEach(() => {
store = mockStore({
auth: {
sport: 'BASKETBALL',
},
});
});
it('should render the container component', () => {
const wrapper = mount(
<Provider store={store}>
<LoginContainer />
</Provider>
);
expect(wrapper.find(LoginContainer).length).to.equal(1);
const container = wrapper.find(LoginContainer);
expect(container.find(Login).length).to.equal(1);
expect(container.find(Login).props().auth).to.eql({ sport: 'BASKETBALL' });
});
it('should perform login', () => {
const loginStub = stub().withArgs({
username: 'abcd',
password: '1234',
});
const wrapper = mount(<Login
loginUser={loginStub}
/>);
wrapper.find('button').simulate('click');
expect(loginStub.callCount).to.equal(1);
});
});

As you pointed out, the way I usually do this is to export the un-connected component as well, and test that.
i.e.
export {Login};
Here's an example. Source of the component, and source of the tests.
For the wrapped component, I don't author tests for those because my mappings (mapStateToProps and mapDispatchToProps) are generally very simple. If I wanted to test a wrapped component, I'd really just be testing those maps. So those are what I would choose to explicitly test, rather than re-testing the entire component in a wrapped form.
There are two ways to test those functions. One way would be to export the functions within the module itself.
i.e.;
export {mapStateToProps, mapDispatchToProps}
I'm not a huge fan of this, because I wouldn't want other modules in the app to access them. In my tests, I sometimes use babel-plugin-rewire to access "in-scope" variables, so that's what I would do in this situation.
That might look something like:
import {
Login, __Rewire__
}
const mapStateToProps = __Rewire__.__get__('mapStateToProps');
describe('mapStateToProps', () => { ... });

If we have a router issue, we can consider to add the router lib into the test file, eg:
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import { mount } from 'enzyme';
import ReadDots from './ReadDots';
const storeFake = state => ({
default: () => {
},
subscribe: () => {
},
dispatch: () => {
},
getState: () => ({ ...state })
});
const store = storeFake({
dot: {
dots: [
{
id: '1',
dot: 'test data',
cost: '100',
tag: 'pocket money'
}
]
}
});
describe('<ReadDots />', () => {
it('should render ReadDots component', () => {
const component = mount(
<Provider store={store}>
<Router>
<ReadDots />
</Router>
</Provider>
);
expect(component.length).toEqual(1);
});
});

Related

vuejs unit test a component which has a child component

I want to test a TheLogin.vue component that has a child BaseInput.vue component. I tried the code below and also shallowMount but I keep getting the error below.
TheLogin.vue
<template>
<section>
<legend>
Hello Login
</legend>
<BaseInput id="userName"></BaseInput>
</section>
</template>
export default {
name: 'TheLogin',
data() {
return {
userName: null
}
}
}
TheLogin.spec.js
import TheLogin from '#/pages/login/TheLogin.vue';
import BaseInput from '#/components/ui/BaseInput.vue';
import { createLocalVue, mount } from '#vue/test-utils';
describe('TheLogin.vue', () => {
const localVue = createLocalVue();
localVue.use(BaseInput); // no luck
it('renders the title', () => {
const wrapper = mount(TheLogin, {
localVue,
// stubs: {BaseInput: true // no luck either
// stubs: ['base-input'] // no luck again
});
expect(wrapper.find('legend').text()).toEqual(
'Hello Login'
);
});
I import my base components in a separate file which I import into my main.js
import Vue from 'vue';
const components = {
BaseInput: () => import('#/components/ui/BaseInput.vue'),
BaseButton: () => import('#/components/ui/BaseButton.vue'),
//et cetera
};
Object.entries(components).forEach(([name, component]) =>
Vue.component(name, component)
);
The error I'm getting is:
TypeError: Cannot read property 'userName' of undefined
UPDATE
Turned out it was Vuelidate causing the error (the code above was not complete). I also had in my script:
validations: {
userName: {
required,
minLength: minLength(4)
},
password: {
required,
minLength: minLength(4)
}
}
I solved it by adding in my test:
import Vuelidate from 'vuelidate';
import Vue from 'vue';
Vue.use(Vuelidate);
Have you tried to shallow mount the component without using localVue and setting BaseInput as a stub?
Something like:
import TheLogin from '#/pages/login/TheLogin.vue';
import { shallowMount } from '#vue/test-utils';
describe('TheLogin.vue', () => {
it('renders the title', () => {
const wrapper = shallowMount(TheLogin, {
stubs: { BaseInput: true }
});
expect(wrapper.find('legend').text()).toEqual(
'Hello Login'
);
});
});

vue-test-utils: How to test logic within mounted() lifecycle hook (with vuex)?

I'm trying to write a unit test for the logic within Vue's mounted() lifecycle hook, but not having much luck. The problem seems to be that mounted() never gets called when the component is mounted using vue-test-utils mount. Here's the Vue component I'm trying to test:
<template>
<div></div>
</template>
<script>
export default {
name: 'MyComponent',
mounted () {
this.$store.dispatch('logout')
}
}
</script>
And the test itself:
import { mount, createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
import MyComponent from '#/components/MyComponent'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('MyComponent.vue', () => {
let store
let actions
beforeEach(() => {
actions = {
logout: jest.fn().mockName('logout')
}
store = new Vuex.Store({
state: {},
actions
})
})
it('calls store "logout" action', () => {
mount(MyComponent, { localVue, store })
expect(actions.logout).toHaveBeenCalled()
})
})
However, this fails with expect(logout).toHaveBeenCalled() asserting false.
If I call the mocked store action directly with actions.logout() the test passes, and I have other tests which also call store actions on things like a button press, and those pass as well, so the problem definitely appears to be with the mounted() lifecycle hook.
Any thoughts?
(vue 2.5.4 and vue-test-utils 1.0.0-beta-.15)
Not sure how it's any different, but I abstracted the store mock to another file and everything seems to work now.
mocks.js
export const storeMock = Object.freeze({
state: {},
actions: {
logout: jest.fn().mockName('logout')
},
})
test.spec.js
import { shallowMount, createLocalVue } from '#vue/test-utils'
import Vuex from 'vuex'
import { storeMock } from './mocks.js'
import MyComponent from '#/components/MyComponent'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('MyComponent.vue', () => {
let options
beforeEach(() => {
jest.clearAllMocks()
const store = new Vuex.Store(storeMock)
options = { store, localVue }
})
it('calls store "logout" action', () => {
shallowMount(MyComponent, options)
expect(storeMock.actions.logout).toHaveBeenCalled()
})
})
Without abstracting the store mock to another file, and slightly different approach without beforeEach (ruined my tests for some reason).
import { createLocalVue, shallowMount } from "#vue/test-utils";
import Vuex from "vuex";
import MyComponent from "#/components/MyComponent.vue";
describe("MyComponent", () => {
const localVue = createLocalVue();
localVue.use(Vuex);
const actions = {
logout: jest.fn()
};
const store = new Vuex.Store({ actions });
const wrapper = shallowMount(MyComponent, {
localVue,
store
});
it('calls store "logout" action', () => {
expect(actions.logout).toHaveBeenCalled();
});
});

Unit Test react-router onEnter/onLeave

I've some logic runs at onEnter/onLeave. I've used some setInterval at onEnter and clear them at onLeave using clearInterval.
How can I unit test above case?
<Route
component={App}
onEnter={onEnterApp}
onLeave={onLeaveApp}
path="/app">
Below is my test case but it fails ,
import React from 'react';
import App from '../components/views/app.jsx';
import {shallow, mount, render} from 'enzyme';
import {expect, assert} from 'chai';
import sinon from 'sinon';
import {mountWithIntl} from '../test_utils/intl-enzyme-test-helper.js';
describe(‘App renders correctly', () => {
it('When mounting set intervals', () => {
const wrapper = mountWithIntl(<App/>);
expect(window.x).to.exist;
expect(window.y).to.exist;
});
it('When unmounting clear intervals', () => {
const wrapper = mountWithIntl(<App/>);
wrapper.unmount();
expect(window.x).to.not.exist;
expect(window.y).to.not.exist;
});
});
The onEnter and onLeave props aren't tied to the componentWillMount and componentWillUnmount methods of your <App> component, so just mounting and unmounting the <App> will not call those functions.
Assuming you trust that React Router works, you can just test that your onEnterApp and onLeaveApp functions work properly
describe('onEnterApp', () => {
it('sets x and y', () => {
onEnterApp();
expect(global.x).to.exist;
expect(globa.y).to.exist;
});
});
If you want to verify that they are run when the URL matches the /app path, then you will need to involve a <Router>.
import createMemoryHistory from 'react-router';
describe('App', () => {
it('runs onEnterApp when navigating to /app', (done) => {
const history = createMemoryHistory(['/app']);
const holder = document.createElement('div');
render((
<Router history={history}>
<Route
component={App}
onEnter={onEnterApp}
onLeave={onLeaveApp}
path="/app">
</Router>
), holder, () => {
expect(global.x).to.exist;
expect(globa.y).to.exist;
done();
});
});
});
Testing onLeaveApp would require you to navigate to a new location with your history instance and then test that the desired global state exists.
history.push({ pathname: '/foo' })

Angular 2 unit testing data passed from parent component to child component

I have a question about the way I've seen (the very few) examples of testing of data passed down from a parent component into a child component. Currently, in the Angular2 docs, they're testing to see if data has been passed down from a parent component to a child by inspecting the dom values of the child component. The issue that I have with this approach is that it forces the parent's spec to know the html structure of the child component. The parent component's job is just to pass data into the child. An example...
I have a Story Component as follows:
'use strict';
import {Component, OnInit, Input} from '#angular/core';
import {StoryService} from '../../services/story.service';
import {StoryModel} from '../../models/story-model';
import {AlbumCover} from './album-cover/album-cover';
import {Author} from "./author/author";
import {StoryDuration} from "./story-duration/story-duration";
#Component({
selector: 'story',
templateUrl: 'build/components/story/story.html',
providers: [StoryService],
directives: [AlbumCover, Author, StoryDuration]
})
export class Story implements OnInit {
#Input('id') id:number;
public story:StoryModel;
constructor(private storyService:StoryService) {}
ngOnInit() {
this.getStory();
}
private getStory() {
this.storyService.getStory(this.id).subscribe(story => this.story = story);
}
}
Notice how it has an AlbumCover Component dependency in the directives array in the #Component decorator.
Here is my Story template:
<div *ngIf="story">
<album-cover [image]="story.albumCover" [title]="story.title"></album-cover>
<div class="author-duration-container">
<author [avatar]="story.author.avatar" [name]="story.author.name"></author>
<story-duration [word-count]="story.wordCount"></story-duration>
</div>
</div>
Notice the <album-cover [image]="story.albumCover" [title]="story.title"></album-cover> line where I'm binding the story.albumCover from the Story controller to the image property of the AlbumCover. This is all working perfectly. Now for the test:
import {provide} from '#angular/core';
import {beforeEach, beforeEachProviders, describe, expect, injectAsync, it, setBaseTestProviders, resetBaseTestProviders} from '#angular/core/testing';
import {HTTP_PROVIDERS} from '#angular/http';
import {BROWSER_APP_DYNAMIC_PROVIDERS} from "#angular/platform-browser-dynamic";
import {TEST_BROWSER_STATIC_PLATFORM_PROVIDERS, ADDITIONAL_TEST_BROWSER_PROVIDERS} from '#angular/platform-browser/testing';
import {ComponentFixture, TestComponentBuilder} from '#angular/compiler/testing';
import {Observable} from 'rxjs/Observable';
// TODO: this pattern of importing 'of' can probably go away once rxjs is fixed
// https://github.com/ReactiveX/rxjs/issues/1713
import 'rxjs/add/observable/of';
resetBaseTestProviders();
setBaseTestProviders(
TEST_BROWSER_STATIC_PLATFORM_PROVIDERS,
[BROWSER_APP_DYNAMIC_PROVIDERS, ADDITIONAL_TEST_BROWSER_PROVIDERS]
);
import {Story} from './story';
import {StoryModel} from '../../models/story-model';
import {StoryService} from '../../services/story.service';
var mockStory = {
id: 1,
title: 'Benefit',
albumCover: 'images/placeholders/story-4.jpg',
author: {
id: 2,
name: 'Brett Beach',
avatar: 'images/placeholders/author-1.jpg'
},
wordCount: 4340,
content: '<p>This is going to be a great book! I <strong>swear!</strong></p>'
};
class MockStoryService {
public getStory(id):Observable<StoryModel> {
return Observable.of(mockStory);
}
}
describe('Story', () => {
var storyFixture,
story,
storyEl;
beforeEachProviders(() => [
HTTP_PROVIDERS
]);
beforeEach(injectAsync([TestComponentBuilder], (tcb:TestComponentBuilder) => {
return tcb
.overrideProviders(Story, [
provide(StoryService, {
useClass: MockStoryService
})
])
.createAsync(Story)
.then((componentFixture:ComponentFixture<Story>) => {
storyFixture = componentFixture;
story = componentFixture.componentInstance;
storyEl = componentFixture.nativeElement;
componentFixture.detectChanges();
});
}));
describe(`ngOnInit`, () => {
describe(`storyService.getStory`, () => {
it(`should be called, and on success, set this.story`, () => {
spyOn(story.storyService, 'getStory').and.callThrough();
story.ngOnInit();
expect(story.storyService.getStory).toHaveBeenCalled();
expect(story.story.title).toBe('Benefit');
});
});
});
it('should not show the story component if story does not exist', () => {
story.story = null;
storyFixture.detectChanges();
expect(storyEl.children.length).toBe(0);
});
it('should show the story component if story exists', () => {
story.story = mockStory;
storyFixture.detectChanges();
expect(storyEl.children.length).not.toBe(0);
});
describe('story components', () => {
beforeEach(() => {
story.story = mockStory;
storyFixture.detectChanges();
});
describe('album cover', () => {
var element,
img;
beforeEach(() => {
element = storyEl.querySelector('album-cover');
img = element.querySelector('img');
});
it(`should be passed the story albumCover and title to the album cover component`, () => {
expect(img.attributes.src.value).toBe(mockStory.albumCover);
expect(img.attributes.alt.value).toBe(mockStory.title);
});
});
describe('author', () => {
var element,
img,
nameEl;
beforeEach(() => {
element = storyEl.querySelector('author');
img = element.querySelector('img');
nameEl = element.querySelector('.name');
});
it(`should be passed the author name and avatar`, () => {
expect(img.attributes.src.value).toBe(story.story.author.avatar);
expect(img.attributes.alt.value).toBe(story.story.author.name);
expect(nameEl.innerText).toBe(story.story.author.name);
});
});
describe('story duration', () => {
var element;
beforeEach(() => {
element = storyEl.querySelector('.story-duration');
});
it(`should be passed the word count to generate the total read time`, () => {
story.story.wordCount = 234234;
storyFixture.detectChanges();
expect(element.innerText).toBe(`852 min read`);
});
});
});
});
Look at my describe('album cover'.... The way I'm passing this expectation is that I'm finding the <album-cover> element, then finding the <img> tag inside of it, then checking the <img>'s DOM attributes. To me, this expection should be inside of the album-cover.spec.ts - NOT the story.spec.ts.
My question is: is there a way to test if a parent component passed data into a child component without relying on reading dom values?
You can use overrideTemplate to pass a view just for the test.
return tcb
.overrideTemplate(AlbumCover, '<div>{{valueFromParent}}</div>')
.overrideProviders(Story, [

Unit Test Failed: Cannot read property 'componentInstance' of undefined

Unit test works fine with angular2-seed project. The problem is it does not work with my project which resembles angular2-seed project.
Only difference between my project and angular2-seed is that I use gulpfile.ts without using tools directory. I defined all gulp tasks in a single file.
I need some help to give me some hint on this error.
I have this very simplified component
import {Component} from 'angular2/core';
import {RouterLink, Router} from 'angular2/router';
import {CORE_DIRECTIVES} from 'angular2/common';
#Component({
selector: 'cet-login',
moduleId: module.id,
templateUrl: 'apps/common/features/login/login.tpl.html',
directives: [RouterLink, CORE_DIRECTIVES]
})
export class LoginComponent {
constructor(
private router: Router
) {}
}
and I have very simple unit test
import {
TestComponentBuilder,
describe,
expect,
injectAsync,
it
} from 'angular2/testing';
import {Component} from 'angular2/core';
import {LoginComponent} from './login.cmp';
export function main(): void {
'use strict';
describe('Login component', () => {
it('should work',
injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.createAsync(TestComponent)
.then(rootTC => {
rootTC.detectChanges();
expect(1).toBe(1);
});
}));
});
}
class Mock {}
#Component({
selector: 'test-cmp',
template: '<cet-login></cet-login>',
directives: [LoginComponent]
})
class TestComponent {}
I also have gulpfile.ts test section like the following;
gulp.task('test:buildjs', 'Compile typescript test files', ['test:buildcss'], () => {
var tsProject = tsProjectFn();
var result = gulp.src(PATH.src.ts)
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(inlineNg2Template({base: PATH.src.base}))
.pipe(tsc(tsProject));
return result.js
.pipe(sourcemaps.init())
.pipe(gulp.dest(PATH.dest.test));
});
gulp.task('test:unit', 'Start a karma server and run a unit test', (done: any) => {
return new karma.Server({
configFile: __dirname + '/karma.config.js',
singleRun: true
}).start(done);
});
When I run gulp test, which runs test:buildjs and test:unit, I have the following error
1) should work
Login component
Failed: Cannot read property 'componentInstance' of undefined
TypeError: Cannot read property 'componentInstance' of undefined
at new ComponentFixture_ (C:/repos/FAR/node_modules/angular2/src/testing/test_component_builder.js:38:51)
at C:/repos/FAR/node_modules/angular2/src/testing/test_component_builder.js:204:52
at Zone.run (C:/repos/FAR/node_modules/zone.js/dist/zone-microtask.js:1217:24)
at zoneBoundFn (C:/repos/FAR/node_modules/zone.js/dist/zone-microtask.js:1194:26)
at lib$es6$promise$$internal$$tryCatch (C:/repos/FAR/node_modules/zone.js/dist/zone-microtask.js:442:17)
at lib$es6$promise$$internal$$invokeCallback (C:/repos/FAR/node_modules/zone.js/dist/zone-microtask.js:454:18)
at lib$es6$promise$$internal$$publish (C:/repos/FAR/node_modules/zone.js/dist/zone-microtask.js:425:12)
at C:/repos/FAR/node_modules/zone.js/dist/zone-microtask.js:97:10
at Zone.run (C:/repos/FAR/node_modules/zone.js/dist/zone-microtask.js:1217:24)
at zoneBoundFn (C:/repos/FAR/node_modules/zone.js/dist/zone-microtask.js:1194:26)
at lib$es6$promise$asap$$flush (C:/repos/FAR/node_modules/zone.js/dist/zone-microtask.js:236:10)
karma.config.js is exactly the same as angualr2-seed
test-main.js is the exactly the same as angualr2-seed
Any idea what I miss and what I am doing wrong?
I don't know which version of Angular2 (in my case beta7+) you use but in my unit tests, I need to register TEST_BROWSER_PLATFORM_PROVIDERS and TEST_BROWSER_APPLICATION_PROVIDERS to be able to use the TestComponentBuilder class:
import {
it,
describe,
expect,
beforeEach,
inject,
injectAsync,
TestComponentBuilder,
ComponentFixture,
setBaseTestProviders
} from 'angular2/testing';
import {
TEST_BROWSER_PLATFORM_PROVIDERS,
TEST_BROWSER_APPLICATION_PROVIDERS
} from 'angular2/platform/testing/browser';
import {MyListComponent} from "./my-list";
import {MyService} from "../services/my-service";
describe('MyList Tests', () => {
setBaseTestProviders(TEST_BROWSER_PLATFORM_PROVIDERS,
TEST_BROWSER_APPLICATION_PROVIDERS);
let list:MyListComponent;
beforeEach(() => {
list = new MyListComponent();
});
it('should render list', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.createAsync(MyListComponent).then((componentFixture: ComponentFixture) => {
const element = componentFixture.nativeElement;
componentFixture.detectChanges();
expect(element.querySelectorAll('li').length).toBe(3);
});
}));
});
See this plunkr: https://plnkr.co/edit/oVBPsH?p=preview.
Found the cause of this problem. The following line should be commented out for successful test.
import {enableProdMode} from 'angular2/core';
enableProdMode();