From 86437c9f650c0b3963e6c3c86c3f9d2cc07b5098 Mon Sep 17 00:00:00 2001
From: pdimitratos
Date: Fri, 2 Mar 2018 15:33:05 -0800
Subject: [PATCH] =?UTF-8?q?Added=20more=20tests,=20attempted=20to=20add=20?=
=?UTF-8?q?coverage=20yaml=20config=20for=20categoriz=E2=80=A6=20(#93)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Added more tests, attempted to add coverage yaml config for categorization of coverage
* formatting improvements, cyclomatic complexity reduction on testableReduxBackedPromise
* throw Error rather than string, some adjustment to validateActionSet
---
.codecov.yml | 34 +++++++++
src/actions/actionHelpers.js | 64 +++++++++++------
src/actions/debugActions.js | 4 +-
test/actions/ActionHelpersTest.js | 46 ++++++++++++-
test/actions/ExpandSectionActionsTest.js | 20 ++++++
test/actions/FilterActionsTest.js | 1 -
test/actions/debugActionsTest.js | 69 +++++++++++++++++++
.../Incident/IncidentRedirectTest.js | 21 +++---
.../elements/AutoCompleteMenuTest.js | 1 -
test/components/elements/ButtonsTest.js | 12 ++--
test/components/elements/FilterChipsTest.js | 1 -
test/helpers/mockDispatch.js | 29 ++++----
12 files changed, 243 insertions(+), 59 deletions(-)
create mode 100644 .codecov.yml
create mode 100644 test/actions/ExpandSectionActionsTest.js
create mode 100644 test/actions/debugActionsTest.js
diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000..f2c2805
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,34 @@
+coverage:
+ precision: 2
+ round: down
+ range: "70...100"
+
+ status:
+ project:
+ default: on
+ patch:
+ default: on
+ changes:
+ default: off
+
+comment:
+ layout: "header, reach, diff, flags, files, footer"
+ behavior: default
+ require_changes: no
+ require_base: no
+ require_head: yes
+
+flags:
+ actions:
+ paths:
+ - src/actions/
+ reducers:
+ paths:
+ - src/actions/
+ services:
+ paths:
+ - src/services/
+ components:
+ paths:
+ - src/components/
+ - src/containers/
diff --git a/src/actions/actionHelpers.js b/src/actions/actionHelpers.js
index 4509448..550aa47 100644
--- a/src/actions/actionHelpers.js
+++ b/src/actions/actionHelpers.js
@@ -1,27 +1,11 @@
import { authenticatedFetch, authenticatedPost, authenticatedPut } from 'services/authenticatedFetch'
-const needOnActionSet = (prop) => `Need "${prop}" function on actionSet!`
+export const testableReduxBackedPromise = (localAuthenticatedFetch, localAuthenticatedPost, localAuthenticatedPut) =>
+(promiseArgs, actionSet, operation = 'GET') =>
+(dispatch) => {
+ validateActionSet(actionSet)
-export const reduxBackedPromise = (promiseArgs, actionSet, operation = 'GET') => (dispatch) => {
- if (!actionSet.try) { throw needOnActionSet('try') }
- if (!actionSet.succeed) { throw needOnActionSet('succeed') }
- if (!actionSet.fail) { throw needOnActionSet('fail') }
-
- let promiseGenerator
- switch (operation.toUpperCase()) {
- case 'PUT':
- promiseGenerator = authenticatedPut
- break
- case 'POST':
- promiseGenerator = authenticatedPost
- break
- default:
- promiseGenerator = authenticatedFetch
- break
- }
- if (!promiseGenerator) {
- throw new Error('promiseGenerator not initialized. This should not be possible. Consider rolling back.')
- }
+ const promiseGenerator = getPromiseGenerator(localAuthenticatedFetch, localAuthenticatedPost, localAuthenticatedPut)(operation)
dispatch(actionSet.try())
@@ -30,6 +14,44 @@ export const reduxBackedPromise = (promiseArgs, actionSet, operation = 'GET') =>
error => dispatch(actionSet.fail(error)))
}
+export const reduxBackedPromise = testableReduxBackedPromise(authenticatedFetch, authenticatedPost, authenticatedPut)
+
+const needOnActionSet = (prop) => `Need "${prop}" function on actionSet!`
+
+/*
+ Expect actionSet to have shape:
+ {
+ try: () => tryAction,
+ succeed: (returnedObject) => succeedAction,
+ fail: (failureReason) => failureAction
+ }
+ actions can be Thunk actions ((dispatch) => action) or plain action objects
+*/
+export const validateActionSet = (actionSet) => {
+ ['try', 'succeed', 'fail'].map(
+ a => {
+ if (!actionSet[a] || typeof actionSet[a] !== 'function') {
+ throw new Error(needOnActionSet(a))
+ }
+ }
+ )
+}
+
+export const getPromiseGenerator = (localAuthenticatedFetch, localAuthenticatedPost, localAuthenticatedPut) =>
+(operation) => {
+ switch (operation.toUpperCase()) {
+ case 'TESTERROR':
+ break // Never intended to happen in a deployed instance, just here for testing
+ case 'PUT':
+ return localAuthenticatedPut
+ case 'POST':
+ return localAuthenticatedPost
+ default:
+ return localAuthenticatedFetch
+ }
+ throw new Error('promiseGenerator not initialized. This should not be possible. Consider rolling back.')
+}
+
const goToPageActionType = (BASE_NAME) => 'GOTO_' + BASE_NAME + '_PAGE'
const nextPageActionType = (BASE_NAME) => 'NEXT_' + BASE_NAME + '_PAGE'
const prevPageActionType = (BASE_NAME) => 'PREV_' + BASE_NAME + '_PAGE'
diff --git a/src/actions/debugActions.js b/src/actions/debugActions.js
index 6382e82..ba8eb1e 100644
--- a/src/actions/debugActions.js
+++ b/src/actions/debugActions.js
@@ -3,8 +3,8 @@ These actions have no associated reducer, but they provide additional context fo
without changing state directly. They're intended to be used with Redux dev tools.
*/
-const RAW_HTTP_RESPONSE = 'DEBUG_RAW_HTTP_RESPONSE'
-const JSON_RESULT = 'DEBUG_JSON_RESULT'
+export const RAW_HTTP_RESPONSE = 'DEBUG_RAW_HTTP_RESPONSE'
+export const JSON_RESULT = 'DEBUG_JSON_RESULT'
// response is not serializable
export const rawHttpResponse = (response) => ({
diff --git a/test/actions/ActionHelpersTest.js b/test/actions/ActionHelpersTest.js
index df85eb9..1181caf 100644
--- a/test/actions/ActionHelpersTest.js
+++ b/test/actions/ActionHelpersTest.js
@@ -1,6 +1,8 @@
'use strict'
-import { expect } from 'chai'
+import { expect, assert } from 'chai'
+
import * as actionHelpers from 'actions/actionHelpers'
+import { GetMockDispatch, GetDispatchRecorder } from 'test/helpers/mockDispatch'
describe('ActionHelpers', function () {
describe('paginationActions', function () {
@@ -16,4 +18,46 @@ describe('ActionHelpers', function () {
expect(result.types.FILTER).to.equal('FILTER_TEST')
})
})
+
+ describe('validateActionSet', function () {
+ it('Should throw "Need "try" function on actionSet!" when no try or try is not a function', function () {
+ assert.throws(
+ () => actionHelpers.validateActionSet({}),
+ 'Need "try" function on actionSet'
+ )
+ assert.throws(
+ () => actionHelpers.validateActionSet({ try: true }),
+ 'Need "try" function on actionSet'
+ )
+ })
+ it('Should throw "Need "succeed" function on actionSet!" when no succeed or succeed is not a function', function () {
+ assert.throws(
+ () => actionHelpers.validateActionSet({ try: () => null }),
+ 'Need "succeed" function on actionSet'
+ )
+ assert.throws(
+ () => actionHelpers.validateActionSet({ try: () => null, succeed: true }),
+ 'Need "succeed" function on actionSet'
+ )
+ })
+ it('Should throw "Need "fail" function on actionSet!" when no fail or fail is not a function', function () {
+ assert.throws(
+ () => actionHelpers.validateActionSet({ try: () => null, succeed: () => null }),
+ 'Need "fail" function on actionSet'
+ )
+ assert.throws(
+ () => actionHelpers.validateActionSet({ try: () => null, succeed: () => null, fail: true }),
+ 'Need "fail" function on actionSet'
+ )
+ })
+ })
+
+ describe('testableReduxBackedPromise', function () {
+ const mockSuccess = {
+ json: 'successJson',
+ response: 'successResponse'
+ }
+ const mockFailure = 'failureError'
+ const successDummy = Promise.resolve(mockSuccess)
+ })
})
diff --git a/test/actions/ExpandSectionActionsTest.js b/test/actions/ExpandSectionActionsTest.js
new file mode 100644
index 0000000..492cf5f
--- /dev/null
+++ b/test/actions/ExpandSectionActionsTest.js
@@ -0,0 +1,20 @@
+'use strict'
+import { expect } from 'chai'
+import * as expandSectionActions from 'actions/expandSectionActions'
+
+describe('Expand Section Actions', function () {
+ describe('toggleCollapse', function () {
+ context('when passed an elementName, the returned object', function () {
+ const expectedElementName = 'testElement'
+ const result = expandSectionActions.toggleCollapse(expectedElementName)
+
+ it('Should have type TOGGLE_COLLAPSE', function (){
+ expect(result.type).to.equal(expandSectionActions.TOGGLE_COLLAPSE)
+ })
+
+ it('Should have property elementName with value equal to the passed in elementName', function (){
+ expect(result.elementName == expectedElementName).to.be.true
+ })
+ })
+ })
+})
diff --git a/test/actions/FilterActionsTest.js b/test/actions/FilterActionsTest.js
index fdd1373..c5394a8 100644
--- a/test/actions/FilterActionsTest.js
+++ b/test/actions/FilterActionsTest.js
@@ -2,7 +2,6 @@
import { expect } from 'chai'
import queryString from 'query-string'
import * as filterActions from 'actions/filterActions'
-import AddMockDispatch from 'test/helpers/mockDispatch'
// Istanbul for test coverage, provides us with a new command nyc-mocha, which runs the tests and gives a reporter
describe('FilterActions', function () {
diff --git a/test/actions/debugActionsTest.js b/test/actions/debugActionsTest.js
new file mode 100644
index 0000000..bdb76e1
--- /dev/null
+++ b/test/actions/debugActionsTest.js
@@ -0,0 +1,69 @@
+'use strict'
+import { expect } from 'chai'
+import * as debugActions from 'actions/debugActions'
+
+describe('Debug Actions', function () {
+ describe('rawHttpResponse', function() {
+ const mockResponse = {
+ bodyUsed: 'expectedBodyUsed',
+ ok: 'expectedOk',
+ redirected: 'expectedRedirected',
+ status: 'expectedStatus',
+ statusText: 'expectedStatusText',
+ type: 'expectedResponseType',
+ url: 'expectedUrl',
+ mockUnusedField: '!!!expect undefined, not this!!!'
+ }
+
+ const mockResult = debugActions.rawHttpResponse(mockResponse)
+ const nullResult = debugActions.rawHttpResponse(null)
+ const undefinedResult = debugActions.rawHttpResponse(undefined)
+
+ context('When passed a null or undefined response object', function () {
+ it('Should return an object with a null response', function () {
+ expect(nullResult.response).to.be.null
+ expect(undefinedResult.response).to.be.null
+ })
+ })
+
+ context('When passed a valid response object', function () {
+ it('Should copy a specific subset of the object property values to the result.response object', function () {
+ expect(mockResult.response.bodyUsed).to.equal(mockResponse.bodyUsed)
+ expect(mockResult.response.ok).to.equal(mockResponse.ok)
+ expect(mockResult.response.redirected).to.equal(mockResponse.redirected)
+ expect(mockResult.response.status).to.equal(mockResponse.status)
+ expect(mockResult.response.statusText).to.equal(mockResponse.statusText)
+ expect(mockResult.response.type).to.equal(mockResponse.type)
+ expect(mockResult.response.url).to.equal(mockResponse.url)
+
+ expect(mockResult.response.mockUnusedField).to.be.undefined
+ })
+
+ it('Should not return the passed in object as its response property', function () {
+ expect(mockResult.response == mockResponse).to.be.false
+ })
+ })
+
+ context('Whenever the function is called', function () {
+ it('Should return an object with a type property equal to RAW_HTTP_RESPONSE', function () {
+ expect(nullResult.type).to.equal(debugActions.RAW_HTTP_RESPONSE)
+ expect(undefinedResult.type).to.equal(debugActions.RAW_HTTP_RESPONSE)
+ expect(mockResult.type).to.equal(debugActions.RAW_HTTP_RESPONSE)
+ })
+ })
+ })
+
+ describe('jsonResult', function () {
+ const mock = {}
+ const result = debugActions.jsonResult(mock)
+ context('The returned object', function () {
+ it('Should have type debugActions.JSON_RESULT', function () {
+ expect(result.type).to.equal(debugActions.JSON_RESULT)
+ })
+
+ it('Should have json with value the same as the passed in object', function () {
+ expect(result.json == mock).to.be.true
+ })
+ })
+ })
+})
diff --git a/test/components/Incident/IncidentRedirectTest.js b/test/components/Incident/IncidentRedirectTest.js
index 25ac1d6..5ceff6c 100644
--- a/test/components/Incident/IncidentRedirectTest.js
+++ b/test/components/Incident/IncidentRedirectTest.js
@@ -2,7 +2,7 @@
import { expect } from 'chai'
import createComponent from 'test/helpers/shallowRenderHelper'
import { IncidentRedirect, mapStateToProps, IncidentRedirectComponentDidMount } from 'components/Incident/IncidentRedirect'
-import AddMockDispatch from 'test/helpers/mockDispatch'
+import { GetMockDispatch, GetDispatchRecorder } from 'test/helpers/mockDispatch'
import { Redirect } from 'react-router'
const setup = (props, children) => createComponent(IncidentRedirect, props, children)
@@ -11,16 +11,15 @@ describe('Incident Redirect', function () {
describe('Component', function () {
describe('ComponentDidMount', function () {
it('Should attempt to fetch incident by incident id if ticketid is not known', function () {
- let mockDispatchRecorder = {
- action: null
- }
+ let mockDispatchRecorder = GetDispatchRecorder()
- const testProps = AddMockDispatch({
+ const testProps = {
ticketId: null,
incidentId: 2
- })(mockDispatchRecorder)
+ }
+ const mockDispatch = GetMockDispatch(mockDispatchRecorder)
- IncidentRedirectComponentDidMount(testProps)
+ IncidentRedirectComponentDidMount({...testProps, dispatch: mockDispatch})
expect(mockDispatchRecorder.action.type).to.equal('REQUEST_INCIDENT')
})
@@ -30,12 +29,14 @@ describe('Incident Redirect', function () {
action: null
}
- const testProps = AddMockDispatch({
+ const testProps = {
ticketId: 1,
incidentId: 2
- })(mockDispatchRecorder)
+ }
- IncidentRedirectComponentDidMount(testProps)
+ const mockDispatch = GetMockDispatch(mockDispatchRecorder)
+
+ IncidentRedirectComponentDidMount({...testProps, dispatch: mockDispatch})
expect(mockDispatchRecorder.action).to.be.null
})
diff --git a/test/components/elements/AutoCompleteMenuTest.js b/test/components/elements/AutoCompleteMenuTest.js
index 61f900a..79b34e6 100644
--- a/test/components/elements/AutoCompleteMenuTest.js
+++ b/test/components/elements/AutoCompleteMenuTest.js
@@ -9,7 +9,6 @@ import AutoComplete from 'material-ui/AutoComplete'
import createComponent from 'test/helpers/shallowRenderHelper'
import AutoCompleteMenu, { onNewRequest } from 'components/elements/AutoCompleteMenu'
-import AddMockDispatch from 'test/helpers/mockDispatch'
const mockProps = {
diff --git a/test/components/elements/ButtonsTest.js b/test/components/elements/ButtonsTest.js
index 12e4d92..06ed66f 100644
--- a/test/components/elements/ButtonsTest.js
+++ b/test/components/elements/ButtonsTest.js
@@ -4,22 +4,20 @@ import React from 'react'
import createComponent from 'test/helpers/shallowRenderHelper'
import { DisplayRetryButton } from 'components/elements/Buttons'
import FlatButtonStyled from 'components/elements/FlatButtonStyled'
-import AddMockDispatch from 'test/helpers/mockDispatch'
+import { GetMockDispatch, GetDispatchRecorder } from 'test/helpers/mockDispatch'
const setup = (props, children) => createComponent(DisplayRetryButton, props, children)
-const dummyState = AddMockDispatch({
+const dummyState = {
actionForRetry: {
type: 'ActionForRetry'
}
-})
+}
describe('DisplayRetryButton', function () {
beforeEach(() => {
- this.mockDispatchRecorder = {
- action: null
- }
- this.singleState = setup(dummyState(this.mockDispatchRecorder), null)
+ this.mockDispatchRecorder = GetDispatchRecorder()
+ this.singleState = setup({ ...dummyState, dispatch: GetMockDispatch(this.mockDispatchRecorder)}, null)
})
it('Should render a FlatButtonStyled with a Retry label', () => {
diff --git a/test/components/elements/FilterChipsTest.js b/test/components/elements/FilterChipsTest.js
index 23cabc9..87c5d56 100644
--- a/test/components/elements/FilterChipsTest.js
+++ b/test/components/elements/FilterChipsTest.js
@@ -4,7 +4,6 @@ import React from 'react'
import Chip from 'material-ui/Chip'
import createComponent from 'test/helpers/shallowRenderHelper'
import { FilterChips, mapStateToProps, renderChip, hydrateChip } from 'components/elements/FilterChips'
-import AddMockDispatch from 'test/helpers/mockDispatch'
describe('FilterChips', function () {
describe('mapStateToProps', function () {
diff --git a/test/helpers/mockDispatch.js b/test/helpers/mockDispatch.js
index 0b54d1d..23473aa 100644
--- a/test/helpers/mockDispatch.js
+++ b/test/helpers/mockDispatch.js
@@ -1,15 +1,15 @@
import { isObject } from 'util'
-export const AddMockDispatch = (state) => (mockDispatchRecorder) => {
- /*
- Action could be:
- an object
- a function that takes dispatch as an argument (thunk middleware action)
- If the action is a function, call it passing in dispatch.
- If the action is an object record it; a single action sets the .action property,
- a second action turns .action into an array.
- Subsequent actions are added to the array.
- */
+/*
+ Action could be:
+ an object
+ a function that takes dispatch as an argument (thunk middleware action)
+ If the action is a function, call it passing in dispatch.
+ If the action is an object record it; a single action sets the .action property,
+ a second action turns .action into an array.
+ Subsequent actions are added to the array.
+*/
+export const GetMockDispatch = (mockDispatchRecorder) => {
const dispatch = (action) => isObject(action)
? mockDispatchRecorder.action
? Array.isArray(mockDispatchRecorder.action)
@@ -17,10 +17,9 @@ export const AddMockDispatch = (state) => (mockDispatchRecorder) => {
: mockDispatchRecorder.action = [mockDispatchRecorder.action, action]
: mockDispatchRecorder.action = action
: action(dispatch)
- return {
- ...state,
- dispatch
- }
+ return dispatch
}
-export default AddMockDispatch
+export const GetDispatchRecorder = () => ({ action: null })
+
+export default GetMockDispatch