Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Advanced testing tools: improvements #1816

Merged
merged 7 commits into from
Apr 16, 2021
Merged
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import partial from 'lodash.partial'
import { OmitFirstArgument, SagaContext, SagaTestResult } from '../../types'
import { mockCommandImplementation } from './mock-command-implementation'
import { mockQueryImplementation } from './mock-query-implementation'
import { shouldExecuteCommand } from './should-execute-command'
import { shouldExecuteQuery } from './should-execute-query'
import { shouldExecuteSideEffect } from './should-execute-side-effect'
@@ -8,11 +10,15 @@ export type SagaAssertionsNode = {
shouldExecuteCommand: OmitFirstArgument<typeof shouldExecuteCommand>
shouldExecuteQuery: OmitFirstArgument<typeof shouldExecuteQuery>
shouldExecuteSideEffect: OmitFirstArgument<typeof shouldExecuteSideEffect>
mockCommandImplementation: OmitFirstArgument<typeof mockCommandImplementation>
mockQueryImplementation: OmitFirstArgument<typeof mockQueryImplementation>
} & Promise<SagaTestResult>

export const makeAssertions = (context: SagaContext): SagaAssertionsNode =>
Object.assign(context.environment.promise, {
shouldExecuteCommand: partial(shouldExecuteCommand, context),
shouldExecuteQuery: partial(shouldExecuteQuery, context),
shouldExecuteSideEffect: partial(shouldExecuteSideEffect, context),
mockCommandImplementation: partial(mockCommandImplementation, context),
mockQueryImplementation: partial(mockQueryImplementation, context),
})
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ import {
SagaTestResult,
TestEvent,
TestSagaAssertion,
MockedCommandImplementation,
MockedQueryImplementation,
} from '../../types'
import { getSecretsManager } from '../../runtime/get-secrets-manager'
import { getEventStore } from '../../runtime/get-event-store'
@@ -19,6 +21,10 @@ import { mockSideEffects } from '../../runtime/mock-side-effects'
import { getReadModelAdapter } from '../../runtime/get-read-model-adapter'
import partial from 'lodash.partial'
import { defaultAssertion } from '../../utils/assertions'
import {
getCommandImplementationKey,
getQueryImplementationKey,
} from '../../runtime/utils'

type SagaTestContext = {
saga: TestSaga
@@ -36,6 +42,16 @@ export type SagaTestEnvironment = {
setSideEffectsStartTimestamp: (value: number) => void
addAssertion: (assertion: TestSagaAssertion) => void
isExecuted: () => boolean
mockCommandImplementation: (
aggregateName: string,
type: string,
implementation: MockedCommandImplementation
) => void
mockQueryImplementation: (
modelName: string,
resolverName: string,
implementation: MockedQueryImplementation
) => void
}

type PromisedAssertion = (
@@ -56,6 +72,15 @@ const promisedAssertion = (
export const makeTestEnvironment = (
context: SagaTestContext
): SagaTestEnvironment => {
const mockedCommandImplementations = new Map<
string,
MockedCommandImplementation
>()
const mockedQueryImplementations = new Map<
string,
MockedQueryImplementation
>()

let executed = false
let useRealSideEffects = false
let sideEffectsStartTimestamp = 0
@@ -77,6 +102,27 @@ export const makeTestEnvironment = (
assertions.push(partial(promisedAssertion, value))
}
const isExecuted = () => executed
const mockCommandImplementation = (
aggregateName: string,
type: string,
implementation: MockedCommandImplementation
) => {
mockedCommandImplementations.set(
getCommandImplementationKey({ type, aggregateName }),
implementation
)
}
const mockQueryImplementation = (
modelName: string,
resolverName: string,
implementation: MockedQueryImplementation
) => {
mockedQueryImplementations.set(
getQueryImplementationKey({ modelName, resolverName }),
implementation
)
}

const promise = new Promise<SagaTestResult>((resolve, reject) => {
completeTest = resolve
failTest = reject
@@ -145,6 +191,10 @@ export const makeTestEnvironment = (
try {
const runtime = getSagaRuntime(
result,
{
commands: mockedCommandImplementations,
queries: mockedQueryImplementations,
},
domain.sagaDomain.schedulerName,
secretsManager,
monitoring,
@@ -293,5 +343,7 @@ export const makeTestEnvironment = (
addAssertion,
isExecuted,
promise,
mockCommandImplementation,
mockQueryImplementation,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MockedCommandImplementation, SagaContext } from '../../types'
import { makeAssertions, SagaAssertionsNode } from './make-assertions'

export const mockCommandImplementation = (
context: SagaContext,
aggregateName: string,
type: string,
implementation: MockedCommandImplementation
): SagaAssertionsNode => {
const { environment } = context

if (environment.isExecuted()) {
throw Error(
`Command implementation cannot be mocked if the test was executed.`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`Command implementation cannot be mocked if the test was executed.`
`Command implementation cannot be mocked if the test is finished.`

?
/cc @EugeniyBurmistrov

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original phrase and the edit mean different things. The edit is indeed correct if the message's indended meaning was "is already finished" and not "was invoked and possibly still runs".

)
}

environment.mockCommandImplementation(aggregateName, type, implementation)

return makeAssertions(context)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MockedQueryImplementation, SagaContext } from '../../types'
import { makeAssertions, SagaAssertionsNode } from './make-assertions'

export const mockQueryImplementation = (
context: SagaContext,
modelName: string,
resolverName: string,
implementation: MockedQueryImplementation
): SagaAssertionsNode => {
const { environment } = context

if (environment.isExecuted()) {
throw Error(
`Query implementation cannot be mocked if the test was executed.`
)
}

environment.mockQueryImplementation(modelName, resolverName, implementation)

return makeAssertions(context)
}
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import isEqual from 'lodash.isequal'
import { Command } from '@resolve-js/core'
import { SagaContext } from '../../types'
import { makeAssertions, SagaAssertionsNode } from './make-assertions'
import { stringifyCommand } from '../../utils/format'
import { stringifyShouldExecuteCommandFailure } from '../../utils/format'

export const shouldExecuteCommand = (
context: SagaContext,
@@ -29,9 +29,7 @@ export const shouldExecuteCommand = (
return resolve(result)
}
return reject(
new Error(
`shouldExecuteCommand assertion failed:\n${stringifyCommand(command)}`
)
new Error(stringifyShouldExecuteCommandFailure(command, result.commands))
)
})

Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import isEqual from 'lodash.isequal'
import { ReadModelQuery } from '@resolve-js/core'
import { SagaContext } from '../../types'
import { makeAssertions, SagaAssertionsNode } from './make-assertions'
import { stringifyQuery } from '../../utils/format'
import { stringifyShouldExecuteQueryFailure } from '../../utils/format'

export const shouldExecuteQuery = (
context: SagaContext,
@@ -29,9 +29,7 @@ export const shouldExecuteQuery = (
return resolve(result)
}
return reject(
new Error(
`shouldExecuteQuery assertion failed:\n${stringifyQuery(query)}`
)
new Error(stringifyShouldExecuteQueryFailure(query, result.queries))
)
})

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import isEqual from 'lodash.isequal'
import { SagaContext } from '../../types'
import { ExecutedSideEffect, SagaContext } from '../../types'
import { makeAssertions, SagaAssertionsNode } from './make-assertions'
import { stringifySideEffectInvocation } from '../../utils/format'
import { stringifyShouldExecuteSideEffectFailure } from '../../utils/format'

export const shouldExecuteSideEffect = (
context: SagaContext,
@@ -21,7 +21,7 @@ export const shouldExecuteSideEffect = (
)
}

const expected = [name, ...args]
const expected: ExecutedSideEffect = [name, ...args]

const index = result
? result.sideEffects.findIndex((executed) => isEqual(executed, expected))
@@ -32,9 +32,7 @@ export const shouldExecuteSideEffect = (
}
return reject(
new Error(
`shouldExecuteSideEffect assertion failed:\n${stringifySideEffectInvocation(
expected
)}`
stringifyShouldExecuteSideEffectFailure(expected, result.sideEffects)
)
)
})
50 changes: 44 additions & 6 deletions packages/tools/testing-tools/src/runtime/get-saga-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import partial from 'lodash.partial'
import { Command, Monitoring, SecretsManager } from '@resolve-js/core'
import { SagaTestResult } from '../types'
import {
Command,
Monitoring,
ReadModelQuery,
SecretsManager,
} from '@resolve-js/core'
import {
MockedCommandImplementation,
MockedQueryImplementation,
SagaTestResult,
} from '../types'
import { getCommandImplementationKey, getQueryImplementationKey } from './utils'

const executeCommand = (
type MockedImplementations = {
commands: Map<string, MockedCommandImplementation>
queries: Map<string, MockedQueryImplementation>
}

const executeCommand = async (
buffer: SagaTestResult,
schedulerName: string,
mockedImplementations: Map<string, MockedCommandImplementation>,
command: Command
) => {
if (command.aggregateName === schedulerName) {
@@ -15,10 +31,26 @@ const executeCommand = (
})
} else {
buffer.commands.push(command)
const implementation = mockedImplementations.get(
getCommandImplementationKey(command)
)
if (typeof implementation === 'function') {
await implementation(command)
}
}
}
const executeQuery = (buffer: SagaTestResult, query: any) => {
const executeQuery = async (
buffer: SagaTestResult,
mockedImplementations: Map<string, MockedQueryImplementation>,
query: ReadModelQuery
) => {
buffer.queries.push(query)
const implementation = mockedImplementations.get(
getQueryImplementationKey(query)
)
if (typeof implementation === 'function') {
await implementation(query)
}
}

const makeScheduler = () => ({
@@ -29,6 +61,7 @@ const makeScheduler = () => ({

export const getSagaRuntime = (
buffer: SagaTestResult,
mockedImplementations: MockedImplementations,
schedulerName: string,
secretsManager: SecretsManager,
monitoring: Monitoring,
@@ -40,8 +73,13 @@ export const getSagaRuntime = (
return {
secretsManager,
monitoring,
executeCommand: partial(executeCommand, buffer, schedulerName),
executeQuery: partial(executeQuery, buffer),
executeCommand: partial(
executeCommand,
buffer,
schedulerName,
mockedImplementations.commands
),
executeQuery: partial(executeQuery, buffer, mockedImplementations.queries),
scheduler: makeScheduler(),
uploader,
getSideEffectsTimestamp: async () => sideEffectsTimestamp,
15 changes: 15 additions & 0 deletions packages/tools/testing-tools/src/runtime/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const getCommandImplementationKey = ({
type,
aggregateName,
}: {
type: string
aggregateName: string
}) => `${aggregateName}:${type}`

export const getQueryImplementationKey = ({
modelName,
resolverName,
}: {
modelName: string
resolverName: string
}) => `${modelName}:${resolverName}`
13 changes: 10 additions & 3 deletions packages/tools/testing-tools/src/types.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import {
ReadModelQueryResult,
Command,
ReadModelQuery,
ViewModelQuery,
CommandResult,
} from '@resolve-js/core'
import { AggregateTestEnvironment } from './flow/aggregate/make-test-environment'
import { ReadModelTestEnvironment } from './flow/read-model/make-test-environment'
@@ -121,11 +121,13 @@ export type ScheduledCommand = {
command: Command
}

export type ExecutedSideEffect = [string, ...any[]]

export type SagaTestResult = {
commands: Array<Command>
scheduledCommands: Array<ScheduledCommand>
sideEffects: Array<[string, ...any[]]>
queries: Array<ReadModelQuery | ViewModelQuery>
sideEffects: Array<ExecutedSideEffect>
queries: Array<ReadModelQuery>
// FIXME: deprecated
scheduleCommands: any[]
}
@@ -136,3 +138,8 @@ export type SagaContext = {
encryption?: EventHandlerEncryptionFactory
environment: SagaTestEnvironment
} & GivenEventsContext

export type MockedCommandImplementation = (command: Command) => CommandResult
export type MockedQueryImplementation = (
query: ReadModelQuery
) => ReadModelQueryResult
Loading