Service - Integration Testing: Difference between revisions
No edit summary |
No edit summary |
||
Line 18: | Line 18: | ||
; testCompleteTimestamp | ; testCompleteTimestamp | ||
: time the integration test completed. | : time the integration test completed. | ||
; testSettings | |||
: straight copy of test configuration testSettings property | |||
; stages | ; stages | ||
: an object that holds the details and status of each stage [[#testRecord.stages structure]] | : an object that holds the details and status of each stage [[#testRecord.stages structure]] | ||
Line 60: | Line 62: | ||
### Invoke the initialStage's Lambda function | ### Invoke the initialStage's Lambda function | ||
== | == receiveMsgInput == | ||
<syntaxhighlight lang="JavaScript"> | <syntaxhighlight lang="JavaScript"> | ||
Line 79: | Line 81: | ||
# Use intTest-tag and intTest-time correlation ids in received message to query TestRecord table for matching record, none found can return, but should send a system log because should not happen | # Use intTest-tag and intTest-time correlation ids in received message to query TestRecord table for matching record, none found can return, but should send a system log because should not happen | ||
# Find matching stage using [[#findMatchingStage]] | # Find matching stage using [[#findMatchingStage]] | ||
# If | # If findMatchingStage returns false, return | ||
# For each testRecord.stages.{stageKey}.inputEventTag.properties where testValueMatches != false test if value matches in requestParams | # For each testRecord.stages.{stageKey}.inputEventTag.properties where testValueMatches != false test if value matches in requestParams | ||
# ... (to update) or straight | # ... (to update:) or straight eventValue check? .... | ||
# Update testRecord.stages.{stageKey}.stageResults.inputResult values using a DynamoDB Conditional Expression to make sure testRecord.stages.{stageKey}.stageResults.inputResult does not already exist, if it exists add an element to testRecord.stages.{stageKey}.stageErrors array because should only update once (message triggering receiveMsgResourceInput might get delivered multiple times, if experience that maybe adjust logic to not record error?) | # Update testRecord.stages.{stageKey}.stageResults.inputResult values using a DynamoDB Conditional Expression to make sure testRecord.stages.{stageKey}.stageResults.inputResult does not already exist, if it exists add an element to testRecord.stages.{stageKey}.stageErrors array because should only update once (message triggering receiveMsgResourceInput might get delivered multiple times, if experience that maybe adjust logic to not record error?) | ||
# Check if any tests for this stage remain using [[#checkStageTestsComplete]] | # Check if any tests for this stage remain using [[#checkStageTestsComplete]] | ||
== | == receiveMsgOutput == | ||
<syntaxhighlight lang="JavaScript"> | <syntaxhighlight lang="JavaScript"> | ||
Line 107: | Line 107: | ||
# Use intTest-tag and intTest-time correlation ids in received message to query TestRecord table for matching record, none found can return, but should send a system log because should not happen | # Use intTest-tag and intTest-time correlation ids in received message to query TestRecord table for matching record, none found can return, but should send a system log because should not happen | ||
# Find matching stage using [[#findMatchingStage]] | # Find matching stage using [[#findMatchingStage]] | ||
# If | # If findMatchingStage returns false, return | ||
## If | # For each testRecord.stages.{stageKey}.outputEventTag.properties where testValueMatches != false test if value matches in returnValue | ||
## Return | # ... (to update:) or straight eventValue check? .... | ||
# For each testRecord.stages.{stageKey}.inputEventTag.properties where testValueMatches != false test if value matches in | # ... (to update) or errors? .... | ||
# Update testRecord.stages.{stageKey}.stageResults.outputResult values using a DynamoDB Conditional Expression to make sure testRecord.stages.{stageKey}.stageResults.outputResult does not already exist, if it exists add an element to testRecord.stages.{stageKey}.stageErrors array because should only update once (message triggering receiveMsgResourceOutput might get delivered multiple times, if experience that maybe adjust logic to not record error?) | |||
# Check if any tests for this stage remain using [[#checkStageTestsComplete]] | |||
== receiveMsgInvoke == | |||
<syntaxhighlight lang="JavaScript"> | |||
/** | |||
* Triggered when a resource invoked by an integration test invokes another resource | |||
* Triggered by it's own SQS queue, which subscribes to the tested services msgOut queue | |||
* Checks if any stages in integration test config match resource invocation, and has an invoke setting that matches message, if yes perform tests on invoke's values and record results | |||
* @param {object} requestParams - The original event body received by the resource | |||
* @param {string} serviceName | |||
* @param {string} resourceType | |||
* @param {string} resourceName | |||
* @param {object} invokeRequestParams | |||
* @param {string} invokeServiceName | |||
* @param {string} invokeResourceType | |||
* @param {string} invokeResourceName | |||
*/ | |||
module.exports.handler = middleware.wrap(async (event, context, callback) => { | |||
</syntaxhighlight> | |||
=== logic === | |||
# Use intTest-tag and intTest-time correlation ids in received message to query TestRecord table for matching record, none found can return, but should send a system log because should not happen | |||
# Find matching stage using [[#findMatchingStage]] | |||
# If findMatchingStage returns false, return | |||
# Find matching invoke for stage using [[#findMatchingStageShared]], inputting testRecord.stages.{stageKey}.stageConfig.invokes and '''invoke..''' params | |||
# If no matching invoke found and testRecord.testSettings.errorIfInvokeUndefined set to true: | |||
## Add error to testRecord.stages.{stageKey}.stageErrors | |||
## Save testRecord.stages.{stageKey}.stageResults.invokes.{invokeTag}, resultStatus set to '''failed''' | |||
## Check if any stageResults for this stage remain using [[#checkStageTestsComplete]] | |||
## Return false | |||
# If more than one matching invoke found: | |||
## Add error to testRecord.stages.{stageKey}.stageErrors | |||
## For each invoke found: | |||
### Save testRecord.stages.{stageKey}.stageResults.invokes.{invokeTag}, resultStatus set to '''failed''' | |||
## Check if any stageResults for this stage remain using [[#checkStageTestsComplete]] | |||
## Return false | |||
# For each testRecord.stages.{stageKey}.stageConfig.invokes.{invokeKey}.inputEventTag.properties where testValueMatches != false test if value matches in invokeRequestParams | |||
# ... (to update) or straight value check .... | # ... (to update) or straight value check .... | ||
# ... (to update) or errors? .... | # ... (to update) or errors? .... | ||
# Update testRecord.stages.{stageKey}.stageResults. | # Update testRecord.stages.{stageKey}.stageResults.invokes.{invokeTag} values using a DynamoDB Conditional Expression to make sure testRecord.stages.{stageKey}.stageResults.invokes.{invokeTag} does not already exist, if it exists add an element to testRecord.stages.{stageKey}.stageErrors array because should only update once (message triggering receiveMsgResourceOutput might get delivered multiple times, if experience that maybe adjust logic to not record error?) | ||
# Check if any tests for this stage remain using [[#checkStageTestsComplete]] | # Check if any tests for this stage remain using [[#checkStageTestsComplete]] | ||
Line 124: | Line 164: | ||
* Checks to see if received message has a matching stage | * Checks to see if received message has a matching stage | ||
* @param {object} requestParams - The event body received by the resource | * @param {object} requestParams - The event body received by the resource | ||
* @param {object} | * @param {string} serviceName | ||
* @param {string} resourceType | |||
* @param {string} resourceName | |||
* @param {object} testRecord | |||
* @param {string} resultTag | |||
* | * | ||
* @returns {number} key of found stage or null if none found | * @returns {number} key of found stage or null if none found | ||
*/ | */ | ||
module.exports.findMatchingStage = (stage, invokes) => { | module.exports.findMatchingStage = (stage, invokes) => { | ||
</syntaxhighlight> | |||
=== logic === | |||
# find matching stage/s using [[#findMatchingStageShared]] | |||
# If no matching stage found and testRecord.testSettings.errorIfStageUndefined set to true: | |||
## add error to testRecord.testErrors | |||
## Return false | |||
# If more than one matching stage found, for each stage found: | |||
## add error to testRecord.stages.{stageKey}.stageErrors | |||
## save testRecord.stages.{stageKey}.stageResults.[resultTag], resultStatus set to '''failed''' (probably skip saving additional fields like requestParams and returnValue, is an edge case) | |||
## Check if any stageResults for this stage remain using [[#checkStageTestsComplete]] | |||
## Return false | |||
== findMatchingStageShared == | |||
<syntaxhighlight lang="JavaScript"> | |||
/** | |||
* Checks to see if received message has a matching stage | |||
* @param {object} requestParams - The event body received by the resource | |||
* @param {string} serviceName | |||
* @param {string} resourceType | |||
* @param {string} resourceName | |||
* @param {object} stages - testRecord.stages array or testRecord.stages.{stageKey}.stageConfig.invokes array | |||
* | |||
* @returns {} key of found stage, null if none found, or array of keys if many match | |||
*/ | |||
module.exports.findMatchingStageShared = (stage, invokes) => { | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 134: | Line 206: | ||
# Iterate stages to find matching stage using: | # Iterate stages to find matching stage using: | ||
## serviceName / resourceType / resourceName | ## match: serviceName / resourceType / resourceName | ||
## For each | ## For each stages.inputEventTag.properties | ||
# if matching stage found return its array element key | ### If forStageMatching != false then values must match for this stage to be a match | ||
# if find more than one matching stage | # if one matching stage found return its array element key | ||
# if find more than one matching stage return array of element keys | |||
# if no matching stage found return null | # if no matching stage found return null | ||
Line 164: | Line 235: | ||
## If the testRecord.stages.{stageKey}.stageConfig.inputEventTag set, has testRecord.stages.{stageKey}.stageResults.inputResult been set? | ## If the testRecord.stages.{stageKey}.stageConfig.inputEventTag set, has testRecord.stages.{stageKey}.stageResults.inputResult been set? | ||
## If the testRecord.stages.{stageKey}.stageConfig.outputEventTag set, has testRecord.stages.{stageKey}.stageResults.outputResult been set? | ## If the testRecord.stages.{stageKey}.stageConfig.outputEventTag set, has testRecord.stages.{stageKey}.stageResults.outputResult been set? | ||
## For each testRecord.stages.{stageKey}.stageConfig.invokes check has result set in testRecord.stages.{stageKey}.stageResults.invokes.{ | ## For each testRecord.stages.{stageKey}.stageConfig.invokes check has result set in testRecord.stages.{stageKey}.stageResults.invokes.{invokeTag}? | ||
# If all stage tests have results: | # If all stage tests have results: | ||
## Update testRecord.stages.{stageKey}.stageStatus to either passed or failed depending on whether any of the above result's resultStatus set to '''failed''', use a DynamoDB Conditional Expression to make sure testRecord.stages.{stageKey}.stageStatus set to '''waiting''', if conditional expression does not pass means another process already updated it, should not happen, add an element to testRecord.stages.{stageKey}.stageErrors array (message triggering this processing might get delivered multiple times, if experience this maybe adjust logic to not record error?) | ## Update testRecord.stages.{stageKey}.stageStatus to either passed or failed depending on whether any of the above result's resultStatus set to '''failed''', use a DynamoDB Conditional Expression to make sure testRecord.stages.{stageKey}.stageStatus set to '''waiting''', if conditional expression does not pass means another process already updated it, should not happen, add an element to testRecord.stages.{stageKey}.stageErrors array (message triggering this processing might get delivered multiple times, if experience this maybe adjust logic to not record error?) |
Revision as of 14:07, 6 September 2020
Overview
Service that pulls integration tests from the Integration Test Config service, invokes each test and monitors the steps execute as expected.
This service is intended to be run in a deployed AWS environment, all services and resources required for the integration tests need to be operational.
DynamoDB tables
TestRecord
Fields
- integrationTestTag
- (partition key)
- testStartedTimestamp
- (sort key)
- time the integration test was started/created.
- testCompleteTimestamp
- time the integration test completed.
- testSettings
- straight copy of test configuration testSettings property
- stages
- an object that holds the details and status of each stage #testRecord.stages structure
- testStatus
- current status of the integration test, either processing, passed, or failed
- testErrors
- an array of any misc errors found
Lambda Functions
initiateIntegrationTest
/**
* Starts integration test/s
* @param {string} [integrationTestTag] - Only initiate test with matching integrationTestTag
* @param {string} [serviceName] - Only initiate tests where initialStage serviceName matches
* @param {string} [resourceType] - Only initiate tests where initialStage resourceType matches
* @param {string} [resourceName] - Only initiate tests where initialStage resourceName matches
*
* @returns {boolean} true if the test was initiated successfully, false if the test could not be initiated (? or an error thrown ?)
*/
module.exports.handler = middleware.wrap(async (event, context, callback) => {
logic
- Invoke Service - Integration Test Config:getIntegrationTests to get configurations of all matching tests
- For each integration test:
- For each integration test stage:
- Invoke Service - Integration Test Config:getEventConfig for inputEventTag, outputEventTag, record these in the stages config
- For each invokes element:
- Invoke Service - Integration Test Config:getEventConfig for inputEventTag, outputEventTag, record these in the stages invoke config
- (we could cache getEventConfig results, as many will be duplicated)
- Invoke Service - Integration Test Config:getEventConfig for inputEventTag, outputEventTag, record these in the stages invoke config
- Store in a variable config for the initialStage
- (we do not want to invoke the initial request until all stages are saved into TestRecord table, to avoid race conditions)
- Save the resulting test's config into TestRecord table, testStatus set to processing
- If no initialStage found add an error to testRecord.testErrors
- For the initialStage:
- Build initial event to start the test using the initialStage's input event
- Add intTest-xx correlation ids
- Invoke the initialStage's Lambda function
- For each integration test stage:
receiveMsgInput
/**
* Triggered when a resource is invoked from an integration test request
* Triggered by it's own SQS queue, which subscribes to the tested services msgOut queue
* Checks if any stages in integration test config match resource invocation, if yes perform tests and record results
* @param {object} requestParams - The event body received by the resource
* @param {string} serviceName
* @param {string} resourceType
* @param {string} resourceName
*/
module.exports.handler = middleware.wrap(async (event, context, callback) => {
logic
- Use intTest-tag and intTest-time correlation ids in received message to query TestRecord table for matching record, none found can return, but should send a system log because should not happen
- Find matching stage using #findMatchingStage
- If findMatchingStage returns false, return
- For each testRecord.stages.{stageKey}.inputEventTag.properties where testValueMatches != false test if value matches in requestParams
- ... (to update:) or straight eventValue check? ....
- Update testRecord.stages.{stageKey}.stageResults.inputResult values using a DynamoDB Conditional Expression to make sure testRecord.stages.{stageKey}.stageResults.inputResult does not already exist, if it exists add an element to testRecord.stages.{stageKey}.stageErrors array because should only update once (message triggering receiveMsgResourceInput might get delivered multiple times, if experience that maybe adjust logic to not record error?)
- Check if any tests for this stage remain using #checkStageTestsComplete
receiveMsgOutput
/**
* Triggered when a resource invoked by an integration test returns
* Triggered by it's own SQS queue, which subscribes to the tested services msgOut queue
* Checks if any stages in integration test config match resource invocation, if yes perform tests on return value and record results
* @param {object} requestParams - The event body received by the resource
* @param {string} serviceName
* @param {string} resourceType
* @param {string} resourceName
* @param {object} returnValue //how to handle errors? Maybe place the actual value into a property, and have other properties for error/other settings
*/
module.exports.handler = middleware.wrap(async (event, context, callback) => {
logic
- Use intTest-tag and intTest-time correlation ids in received message to query TestRecord table for matching record, none found can return, but should send a system log because should not happen
- Find matching stage using #findMatchingStage
- If findMatchingStage returns false, return
- For each testRecord.stages.{stageKey}.outputEventTag.properties where testValueMatches != false test if value matches in returnValue
- ... (to update:) or straight eventValue check? ....
- ... (to update) or errors? ....
- Update testRecord.stages.{stageKey}.stageResults.outputResult values using a DynamoDB Conditional Expression to make sure testRecord.stages.{stageKey}.stageResults.outputResult does not already exist, if it exists add an element to testRecord.stages.{stageKey}.stageErrors array because should only update once (message triggering receiveMsgResourceOutput might get delivered multiple times, if experience that maybe adjust logic to not record error?)
- Check if any tests for this stage remain using #checkStageTestsComplete
receiveMsgInvoke
/**
* Triggered when a resource invoked by an integration test invokes another resource
* Triggered by it's own SQS queue, which subscribes to the tested services msgOut queue
* Checks if any stages in integration test config match resource invocation, and has an invoke setting that matches message, if yes perform tests on invoke's values and record results
* @param {object} requestParams - The original event body received by the resource
* @param {string} serviceName
* @param {string} resourceType
* @param {string} resourceName
* @param {object} invokeRequestParams
* @param {string} invokeServiceName
* @param {string} invokeResourceType
* @param {string} invokeResourceName
*/
module.exports.handler = middleware.wrap(async (event, context, callback) => {
logic
- Use intTest-tag and intTest-time correlation ids in received message to query TestRecord table for matching record, none found can return, but should send a system log because should not happen
- Find matching stage using #findMatchingStage
- If findMatchingStage returns false, return
- Find matching invoke for stage using #findMatchingStageShared, inputting testRecord.stages.{stageKey}.stageConfig.invokes and invoke.. params
- If no matching invoke found and testRecord.testSettings.errorIfInvokeUndefined set to true:
- Add error to testRecord.stages.{stageKey}.stageErrors
- Save testRecord.stages.{stageKey}.stageResults.invokes.{invokeTag}, resultStatus set to failed
- Check if any stageResults for this stage remain using #checkStageTestsComplete
- Return false
- If more than one matching invoke found:
- Add error to testRecord.stages.{stageKey}.stageErrors
- For each invoke found:
- Save testRecord.stages.{stageKey}.stageResults.invokes.{invokeTag}, resultStatus set to failed
- Check if any stageResults for this stage remain using #checkStageTestsComplete
- Return false
- For each testRecord.stages.{stageKey}.stageConfig.invokes.{invokeKey}.inputEventTag.properties where testValueMatches != false test if value matches in invokeRequestParams
- ... (to update) or straight value check ....
- ... (to update) or errors? ....
- Update testRecord.stages.{stageKey}.stageResults.invokes.{invokeTag} values using a DynamoDB Conditional Expression to make sure testRecord.stages.{stageKey}.stageResults.invokes.{invokeTag} does not already exist, if it exists add an element to testRecord.stages.{stageKey}.stageErrors array because should only update once (message triggering receiveMsgResourceOutput might get delivered multiple times, if experience that maybe adjust logic to not record error?)
- Check if any tests for this stage remain using #checkStageTestsComplete
Functions
findMatchingStage
/**
* Checks to see if received message has a matching stage
* @param {object} requestParams - The event body received by the resource
* @param {string} serviceName
* @param {string} resourceType
* @param {string} resourceName
* @param {object} testRecord
* @param {string} resultTag
*
* @returns {number} key of found stage or null if none found
*/
module.exports.findMatchingStage = (stage, invokes) => {
logic
- find matching stage/s using #findMatchingStageShared
- If no matching stage found and testRecord.testSettings.errorIfStageUndefined set to true:
- add error to testRecord.testErrors
- Return false
- If more than one matching stage found, for each stage found:
- add error to testRecord.stages.{stageKey}.stageErrors
- save testRecord.stages.{stageKey}.stageResults.[resultTag], resultStatus set to failed (probably skip saving additional fields like requestParams and returnValue, is an edge case)
- Check if any stageResults for this stage remain using #checkStageTestsComplete
- Return false
/**
* Checks to see if received message has a matching stage
* @param {object} requestParams - The event body received by the resource
* @param {string} serviceName
* @param {string} resourceType
* @param {string} resourceName
* @param {object} stages - testRecord.stages array or testRecord.stages.{stageKey}.stageConfig.invokes array
*
* @returns {} key of found stage, null if none found, or array of keys if many match
*/
module.exports.findMatchingStageShared = (stage, invokes) => {
logic
- Iterate stages to find matching stage using:
- match: serviceName / resourceType / resourceName
- For each stages.inputEventTag.properties
- If forStageMatching != false then values must match for this stage to be a match
- if one matching stage found return its array element key
- if find more than one matching stage return array of element keys
- if no matching stage found return null
checkStageTestsComplete
/**
* Tests whether any tests for a single stage are waiting results
* @param {string} intTestTag
* @param {number} intTestTime
* @param {number} stageKey - array key for the stage that we just saved results for. We could skip this and check all stages, more thorough but more processing time
*
* @returns {boolean} true if all stage tests have results saved
*/
module.exports.checkStageTestsComplete = (stage, invokes) => {
logic
- Use intTestTag and intTestTime to query TestRecord table for matching record, none found can return, but should send a system log because should not happen.
- (we need to query Dynamo again to prevent race conditions)
- If no stage exists in testRecord.stages.{stageKey} add an element to testRecord.testErrors array, invoke #checkTestComplete, and return
- Check if any tests for this stageKey remain:
- If the testRecord.stages.{stageKey}.stageConfig.inputEventTag set, has testRecord.stages.{stageKey}.stageResults.inputResult been set?
- If the testRecord.stages.{stageKey}.stageConfig.outputEventTag set, has testRecord.stages.{stageKey}.stageResults.outputResult been set?
- For each testRecord.stages.{stageKey}.stageConfig.invokes check has result set in testRecord.stages.{stageKey}.stageResults.invokes.{invokeTag}?
- If all stage tests have results:
- Update testRecord.stages.{stageKey}.stageStatus to either passed or failed depending on whether any of the above result's resultStatus set to failed, use a DynamoDB Conditional Expression to make sure testRecord.stages.{stageKey}.stageStatus set to waiting, if conditional expression does not pass means another process already updated it, should not happen, add an element to testRecord.stages.{stageKey}.stageErrors array (message triggering this processing might get delivered multiple times, if experience this maybe adjust logic to not record error?)
- check if test complete using #checkTestComplete
- if all stage tests have results return true, if not return false
checkTestComplete
/**
* Tests whether any tests for a single stage are waiting results
* @param {string} intTestTag
* @param {number} intTestTime
*
* @returns {boolean} true if all stage tests have results saved
*/
module.exports.checkTestComplete = (stage, invokes) => {
logic
- Use intTestTag and intTestTime to query TestRecord table for matching record, none found can return, but should send a system log because should not happen.
- (we need to query Dynamo again to prevent race conditions)
- For each testRecord.stages:
- If any testRecord.stages.{stageKey}.stageStatus set to waiting return
- If no stages waiting:
- Update testRecord.testStatus passed or failed, use a DynamoDB Conditional Expression to make sure testRecord.testStatus set to processing and testCompleteTimestamp to current timestamp, if conditional expression does not pass means another process already updated it, should not happen, add an element to testRecord.testErrors array (message triggering this processing might get delivered multiple times, if experience this maybe adjust logic to not record error?)
testRecord.stages structure
[
{
stageConfig: {
//straight copy of this stage from integration test config
},
stageStatus: waiting|passed|failed,
stageFinishedTimestamp: {time that all tests finished and testRecord.stages.{stageKey}.stageStatus updated}
stageResults: {
//results at the point of entering the resource (eg a Lambda function is invoked)
inputResult: {
resultTimestamp: {time result saved},
resultStatus: passed|failed,
requestParams: {a copy of the requestParams}
},
//results at the point of returning from the resource (eg a Lambda function returns)
outputResult: {
resultTimestamp: {time result saved},
resultStatus: passed|failed,
returnValue: {a copy of the value returned by the resource}
//.. maybe other settings for errors etc..
},
//results at the point of invoking an external resource (eg the tested Lambda function is invoking another Lambda function)
invokes: {
{serviceName_resourceType_resourceName_inputEventTag_outputEventTag}: {
resultTimestamp: {time result saved},
resultStatus: passed|failed,
requestParams: {a copy of the requestParams}
},
..
},
},
stageErrors: [
//misc errors encountered
]
},
...
]
Initiating tests
When the initial request is sent to begin an integration test the middleware LOG_LEVEL is set to DEBUG, this causes all Lambda functions and requests to external services to also add a message to the services MsgOut queue.
The Integration Testing service subscribes to these queues and uses these messages to monitor each stage of the workflow to ensure input and output is as expected, and that the integration test completes fully.
Add the following Correlation Ids to the initial request so we can track the integration test, filter messages, etc.:
- intTest-tag
- matches the test's integrationTestTag
- intTest-time
- matches the test's testStartedTimestamp
These can also be used in production environment to exclude requests that middleware randomly set to debug from requests initiated by Integration Test service.