Skip to content

Commit

Permalink
Improve assertion messages when running expectations (#281)
Browse files Browse the repository at this point in the history
* tweak Arg generated description

* capture stacktraces on each call

* add textBuilder and improve assertion messages

* bump to node18

* refactor utilities

* replace utilities

* add rest of constants and stringifies

* replace stringify & raw values

* support serialization in different contexts

* 2.0.0-beta.4
  • Loading branch information
notanengineercom authored Mar 21, 2024
1 parent cf95e14 commit 30c69d9
Show file tree
Hide file tree
Showing 19 changed files with 494 additions and 182 deletions.
9 changes: 5 additions & 4 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@ jobs:
strategy:
fail-fast: false
matrix:
language: ['javascript']
language: ['javascript-typescript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection

steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
show-progress: false

# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
Expand All @@ -35,9 +36,9 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
8 changes: 5 additions & 3 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12, 14, 16, 18, 20]
node-version: [18, 20, 21]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
Expand Down
24 changes: 15 additions & 9 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
with:
node-version: 14
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- run: npm ci --ignore-scripts
- run: npm test
Expand All @@ -20,10 +22,12 @@ jobs:
needs: build-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: 14
node-version: 18
registry-url: https://registry.npmjs.org/
cache: 'npm'
- run: npm ci --ignore-scripts
Expand All @@ -43,10 +47,12 @@ jobs:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: 14
node-version: 18
registry-url: https://npm.pkg.github.com/
cache: 'npm'
- run: npm ci --ignore-scripts
Expand Down
40 changes: 29 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fluffy-spoon/substitute",
"version": "2.0.0-beta.3",
"version": "2.0.0-beta.4",
"description": "TypeScript port of NSubstitute, which aims to provide a much more fluent mocking opportunity for strong-typed languages",
"license": "MIT",
"funding": {
Expand Down Expand Up @@ -46,9 +46,12 @@
"devDependencies": {
"@ava/typescript": "^3.0.1",
"@sinonjs/fake-timers": "^11.2.2",
"@types/node": "^12.20.55",
"@types/node": "^18.19.22",
"@types/sinonjs__fake-timers": "^8.1.5",
"ava": "^4.3.3",
"typescript": "^4.8.4"
},
"volta": {
"node": "18.19.1"
}
}
28 changes: 18 additions & 10 deletions spec/regression/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,19 @@ test('class method received', t => {
t.notThrows(() => substitute.received(1).c('hi', 'the1re'))
t.notThrows(() => substitute.received().c('hi', 'there'))

const expectedMessage = 'Expected 7 calls to the method c with arguments [\'hi\', \'there\'], but received 4 of such calls.\n' +
'All calls received to method c:\n' +
'-> call with arguments [\'hi\', \'there\']\n' +
'-> call with arguments [\'hi\', \'the1re\']\n' +
'-> call with arguments [\'hi\', \'there\']\n' +
'-> call with arguments [\'hi\', \'there\']\n' +
'-> call with arguments [\'hi\', \'there\']'
const expectedMessage = 'Call count mismatch in @Substitute.c:\n' +
`Expected to receive 7 method calls matching c('hi', 'there'), but received 4.\n` +
'All property or method calls to @Substitute.c received so far:\n' +
`› ✔ @Substitute.c('hi', 'there')\n` +
` called at <anonymous> (${process.cwd()}/spec/regression/index.test.ts:114:18)\n` +
`› ✘ @Substitute.c('hi', 'the1re')\n` +
` called at <anonymous> (${process.cwd()}/spec/regression/index.test.ts:115:18)\n` +
`› ✔ @Substitute.c('hi', 'there')\n` +
` called at <anonymous> (${process.cwd()}/spec/regression/index.test.ts:116:18)\n` +
`› ✔ @Substitute.c('hi', 'there')\n` +
` called at <anonymous> (${process.cwd()}/spec/regression/index.test.ts:117:18)\n` +
`› ✔ @Substitute.c('hi', 'there')\n` +
` called at <anonymous> (${process.cwd()}/spec/regression/index.test.ts:118:18)\n`
const { message } = t.throws(() => { substitute.received(7).c('hi', 'there') })
t.is(message.replace(textModifierRegex, ''), expectedMessage)
})
Expand All @@ -142,9 +148,11 @@ test('received call matches after partial mocks using property instance mimicks'
substitute.received(1).c('lala', 'bar')

t.notThrows(() => substitute.received(1).c('lala', 'bar'))
const expectedMessage = 'Expected 2 calls to the method c with arguments [\'lala\', \'bar\'], but received 1 of such calls.\n' +
'All calls received to method c:\n' +
'-> call with arguments [\'lala\', \'bar\']'
const expectedMessage = 'Call count mismatch in @Substitute.c:\n' +
`Expected to receive 2 method calls matching c('lala', 'bar'), but received 1.\n` +
'All property or method calls to @Substitute.c received so far:\n' +
`› ✔ @Substitute.c('lala', 'bar')\n` +
` called at <anonymous> (${process.cwd()}/spec/regression/index.test.ts:145:13)\n`
const { message } = t.throws(() => substitute.received(2).c('lala', 'bar'))
t.is(message.replace(textModifierRegex, ''), expectedMessage)
t.deepEqual(substitute.d, 1337)
Expand Down
13 changes: 13 additions & 0 deletions spec/regression/issues/138.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import test from 'ava'

import { Substitute } from '../../../src'

interface Library { }

test('issue 138: serializes to JSON compatible data', t => {
const lib = Substitute.for<Library>()
const result = JSON.stringify(lib)

t.true(typeof result === 'string')
t.is(result, '"@Substitute {\\n}"')
})
39 changes: 39 additions & 0 deletions spec/regression/issues/27.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import test from 'ava'
import { types } from 'util'

import { Substitute } from '../../../src'

interface Library {
subSection: () => string
}

// Adapted snipped extracted from https://github.com/angular/angular/blob/main/packages/compiler/src/parse_util.ts#L176
// This is to reproduce the behavior of the Angular compiler. This function tries to extract the id from a reference.
const identifierName = (compileIdentifier: { reference: any } | null | undefined): string | null => {
if (!compileIdentifier || !compileIdentifier.reference) {
return null
}
const ref = compileIdentifier.reference
if (ref['__anonymousType']) {
return ref['__anonymousType']
}
}

test('issue 27: mocks should work with Angular TestBed', t => {
const lib = Substitute.for<Library>()
lib.subSection().returns('This is the mocked value')
const result = identifierName({ reference: lib })

t.not(result, null)
t.true(types.isProxy(result))

const jitId = `jit_${result}`
t.is(jitId, 'jit_property<__anonymousType>: ')
})

test('issue 27: subsitute node can be coerced to a primitive value', t => {
const lib = Substitute.for<Library>()
t.true(typeof `${lib}` === 'string')
t.true(typeof (lib + '') === 'string')
t.is(`${lib}`, '@Substitute {\n}')
})
13 changes: 8 additions & 5 deletions src/Arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ type ArgumentOptions = {
inverseMatch?: boolean
}
class BaseArgument<T> {
private _description: string
constructor(
private _description: string,
description: string,
private _matchingFunction: PredicateFunction<T>,
private _options?: ArgumentOptions
) { }
) {
this._description = `${this._options?.inverseMatch ? 'Not ' : ''}${description}`
}

matches(arg: T) {
const inverseMatch = this._options?.inverseMatch ?? false
Expand All @@ -33,7 +36,7 @@ export class Argument<T> extends BaseArgument<T> {
export class AllArguments<T extends any[]> extends BaseArgument<T> {
private readonly _type = 'AllArguments';
constructor() {
super('{all}', () => true, {})
super('Arg.all{}', () => true, {})
}
get type(): 'AllArguments' {
return this._type // TODO: Needed?
Expand All @@ -60,7 +63,7 @@ export namespace Arg {

type Is = <T>(predicate: PredicateFunction<ExtractFirstArg<T>>) => ReturnArg<ExtractFirstArg<T>>
const isFunction = <T>(predicate: PredicateFunction<ExtractFirstArg<T>>, options?: ArgumentOptions) => new Argument(
`{predicate ${toStringify(predicate)}}`, predicate, options
`Arg.is{${toStringify(predicate)}}`, predicate, options
)
export const is = createInversable(isFunction) as Inversable<Is>

Expand All @@ -79,7 +82,7 @@ export namespace Arg {
type Any = <T extends AnyType = 'any'>(type?: T) => MapAnyReturn<T>

const anyFunction = (type: AnyType = 'any', options?: ArgumentOptions) => {
const description = `{type ${type}}`
const description = `Arg.any{${type}}`
const predicate = (x: any) => {
switch (type) {
case 'any':
Expand Down
Loading

0 comments on commit 30c69d9

Please sign in to comment.