зеркало из https://github.com/Azure/Sia-EventUI.git
Added more tests, attempted to add coverage yaml config for categoriz… (#93)
* 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
This commit is contained in:
Родитель
8504e3e396
Коммит
86437c9f65
|
@ -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/
|
|
@ -1,27 +1,11 @@
|
||||||
import { authenticatedFetch, authenticatedPost, authenticatedPut } from 'services/authenticatedFetch'
|
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) => {
|
const promiseGenerator = getPromiseGenerator(localAuthenticatedFetch, localAuthenticatedPost, localAuthenticatedPut)(operation)
|
||||||
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.')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(actionSet.try())
|
dispatch(actionSet.try())
|
||||||
|
|
||||||
|
@ -30,6 +14,44 @@ export const reduxBackedPromise = (promiseArgs, actionSet, operation = 'GET') =>
|
||||||
error => dispatch(actionSet.fail(error)))
|
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 goToPageActionType = (BASE_NAME) => 'GOTO_' + BASE_NAME + '_PAGE'
|
||||||
const nextPageActionType = (BASE_NAME) => 'NEXT_' + BASE_NAME + '_PAGE'
|
const nextPageActionType = (BASE_NAME) => 'NEXT_' + BASE_NAME + '_PAGE'
|
||||||
const prevPageActionType = (BASE_NAME) => 'PREV_' + BASE_NAME + '_PAGE'
|
const prevPageActionType = (BASE_NAME) => 'PREV_' + BASE_NAME + '_PAGE'
|
||||||
|
|
|
@ -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.
|
without changing state directly. They're intended to be used with Redux dev tools.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const RAW_HTTP_RESPONSE = 'DEBUG_RAW_HTTP_RESPONSE'
|
export const RAW_HTTP_RESPONSE = 'DEBUG_RAW_HTTP_RESPONSE'
|
||||||
const JSON_RESULT = 'DEBUG_JSON_RESULT'
|
export const JSON_RESULT = 'DEBUG_JSON_RESULT'
|
||||||
|
|
||||||
// response is not serializable
|
// response is not serializable
|
||||||
export const rawHttpResponse = (response) => ({
|
export const rawHttpResponse = (response) => ({
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
import { expect } from 'chai'
|
import { expect, assert } from 'chai'
|
||||||
|
|
||||||
import * as actionHelpers from 'actions/actionHelpers'
|
import * as actionHelpers from 'actions/actionHelpers'
|
||||||
|
import { GetMockDispatch, GetDispatchRecorder } from 'test/helpers/mockDispatch'
|
||||||
|
|
||||||
describe('ActionHelpers', function () {
|
describe('ActionHelpers', function () {
|
||||||
describe('paginationActions', function () {
|
describe('paginationActions', function () {
|
||||||
|
@ -16,4 +18,46 @@ describe('ActionHelpers', function () {
|
||||||
expect(result.types.FILTER).to.equal('FILTER_TEST')
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -2,7 +2,6 @@
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import queryString from 'query-string'
|
import queryString from 'query-string'
|
||||||
import * as filterActions from 'actions/filterActions'
|
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
|
// Istanbul for test coverage, provides us with a new command nyc-mocha, which runs the tests and gives a reporter
|
||||||
describe('FilterActions', function () {
|
describe('FilterActions', function () {
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -2,7 +2,7 @@
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import createComponent from 'test/helpers/shallowRenderHelper'
|
import createComponent from 'test/helpers/shallowRenderHelper'
|
||||||
import { IncidentRedirect, mapStateToProps, IncidentRedirectComponentDidMount } from 'components/Incident/IncidentRedirect'
|
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'
|
import { Redirect } from 'react-router'
|
||||||
|
|
||||||
const setup = (props, children) => createComponent(IncidentRedirect, props, children)
|
const setup = (props, children) => createComponent(IncidentRedirect, props, children)
|
||||||
|
@ -11,16 +11,15 @@ describe('Incident Redirect', function () {
|
||||||
describe('Component', function () {
|
describe('Component', function () {
|
||||||
describe('ComponentDidMount', function () {
|
describe('ComponentDidMount', function () {
|
||||||
it('Should attempt to fetch incident by incident id if ticketid is not known', function () {
|
it('Should attempt to fetch incident by incident id if ticketid is not known', function () {
|
||||||
let mockDispatchRecorder = {
|
let mockDispatchRecorder = GetDispatchRecorder()
|
||||||
action: null
|
|
||||||
}
|
|
||||||
|
|
||||||
const testProps = AddMockDispatch({
|
const testProps = {
|
||||||
ticketId: null,
|
ticketId: null,
|
||||||
incidentId: 2
|
incidentId: 2
|
||||||
})(mockDispatchRecorder)
|
}
|
||||||
|
const mockDispatch = GetMockDispatch(mockDispatchRecorder)
|
||||||
|
|
||||||
IncidentRedirectComponentDidMount(testProps)
|
IncidentRedirectComponentDidMount({...testProps, dispatch: mockDispatch})
|
||||||
|
|
||||||
expect(mockDispatchRecorder.action.type).to.equal('REQUEST_INCIDENT')
|
expect(mockDispatchRecorder.action.type).to.equal('REQUEST_INCIDENT')
|
||||||
})
|
})
|
||||||
|
@ -30,12 +29,14 @@ describe('Incident Redirect', function () {
|
||||||
action: null
|
action: null
|
||||||
}
|
}
|
||||||
|
|
||||||
const testProps = AddMockDispatch({
|
const testProps = {
|
||||||
ticketId: 1,
|
ticketId: 1,
|
||||||
incidentId: 2
|
incidentId: 2
|
||||||
})(mockDispatchRecorder)
|
}
|
||||||
|
|
||||||
IncidentRedirectComponentDidMount(testProps)
|
const mockDispatch = GetMockDispatch(mockDispatchRecorder)
|
||||||
|
|
||||||
|
IncidentRedirectComponentDidMount({...testProps, dispatch: mockDispatch})
|
||||||
|
|
||||||
expect(mockDispatchRecorder.action).to.be.null
|
expect(mockDispatchRecorder.action).to.be.null
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,7 +9,6 @@ import AutoComplete from 'material-ui/AutoComplete'
|
||||||
|
|
||||||
import createComponent from 'test/helpers/shallowRenderHelper'
|
import createComponent from 'test/helpers/shallowRenderHelper'
|
||||||
import AutoCompleteMenu, { onNewRequest } from 'components/elements/AutoCompleteMenu'
|
import AutoCompleteMenu, { onNewRequest } from 'components/elements/AutoCompleteMenu'
|
||||||
import AddMockDispatch from 'test/helpers/mockDispatch'
|
|
||||||
|
|
||||||
|
|
||||||
const mockProps = {
|
const mockProps = {
|
||||||
|
|
|
@ -4,22 +4,20 @@ import React from 'react'
|
||||||
import createComponent from 'test/helpers/shallowRenderHelper'
|
import createComponent from 'test/helpers/shallowRenderHelper'
|
||||||
import { DisplayRetryButton } from 'components/elements/Buttons'
|
import { DisplayRetryButton } from 'components/elements/Buttons'
|
||||||
import FlatButtonStyled from 'components/elements/FlatButtonStyled'
|
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 setup = (props, children) => createComponent(DisplayRetryButton, props, children)
|
||||||
|
|
||||||
const dummyState = AddMockDispatch({
|
const dummyState = {
|
||||||
actionForRetry: {
|
actionForRetry: {
|
||||||
type: 'ActionForRetry'
|
type: 'ActionForRetry'
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
describe('DisplayRetryButton', function () {
|
describe('DisplayRetryButton', function () {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
this.mockDispatchRecorder = {
|
this.mockDispatchRecorder = GetDispatchRecorder()
|
||||||
action: null
|
this.singleState = setup({ ...dummyState, dispatch: GetMockDispatch(this.mockDispatchRecorder)}, null)
|
||||||
}
|
|
||||||
this.singleState = setup(dummyState(this.mockDispatchRecorder), null)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should render a FlatButtonStyled with a Retry label', () => {
|
it('Should render a FlatButtonStyled with a Retry label', () => {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import React from 'react'
|
||||||
import Chip from 'material-ui/Chip'
|
import Chip from 'material-ui/Chip'
|
||||||
import createComponent from 'test/helpers/shallowRenderHelper'
|
import createComponent from 'test/helpers/shallowRenderHelper'
|
||||||
import { FilterChips, mapStateToProps, renderChip, hydrateChip } from 'components/elements/FilterChips'
|
import { FilterChips, mapStateToProps, renderChip, hydrateChip } from 'components/elements/FilterChips'
|
||||||
import AddMockDispatch from 'test/helpers/mockDispatch'
|
|
||||||
|
|
||||||
describe('FilterChips', function () {
|
describe('FilterChips', function () {
|
||||||
describe('mapStateToProps', function () {
|
describe('mapStateToProps', function () {
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { isObject } from 'util'
|
import { isObject } from 'util'
|
||||||
|
|
||||||
export const AddMockDispatch = (state) => (mockDispatchRecorder) => {
|
/*
|
||||||
/*
|
Action could be:
|
||||||
Action could be:
|
an object
|
||||||
an object
|
a function that takes dispatch as an argument (thunk middleware action)
|
||||||
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 a function, call it passing in dispatch.
|
If the action is an object record it; a single action sets the .action property,
|
||||||
If the action is an object record it; a single action sets the .action property,
|
a second action turns .action into an array.
|
||||||
a second action turns .action into an array.
|
Subsequent actions are added to the array.
|
||||||
Subsequent actions are added to the array.
|
*/
|
||||||
*/
|
export const GetMockDispatch = (mockDispatchRecorder) => {
|
||||||
const dispatch = (action) => isObject(action)
|
const dispatch = (action) => isObject(action)
|
||||||
? mockDispatchRecorder.action
|
? mockDispatchRecorder.action
|
||||||
? Array.isArray(mockDispatchRecorder.action)
|
? Array.isArray(mockDispatchRecorder.action)
|
||||||
|
@ -17,10 +17,9 @@ export const AddMockDispatch = (state) => (mockDispatchRecorder) => {
|
||||||
: mockDispatchRecorder.action = [mockDispatchRecorder.action, action]
|
: mockDispatchRecorder.action = [mockDispatchRecorder.action, action]
|
||||||
: mockDispatchRecorder.action = action
|
: mockDispatchRecorder.action = action
|
||||||
: action(dispatch)
|
: action(dispatch)
|
||||||
return {
|
return dispatch
|
||||||
...state,
|
|
||||||
dispatch
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddMockDispatch
|
export const GetDispatchRecorder = () => ({ action: null })
|
||||||
|
|
||||||
|
export default GetMockDispatch
|
||||||
|
|
Загрузка…
Ссылка в новой задаче