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