Skip to content

Commit 30c69d9

Browse files
Improve assertion messages when running expectations (#281)
* 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
1 parent cf95e14 commit 30c69d9

19 files changed

+494
-182
lines changed

.github/workflows/codeql-analysis.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,18 @@ jobs:
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
language: ['javascript']
19+
language: ['javascript-typescript']
2020
# Learn more...
2121
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
2222

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

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

3637
# Initializes the CodeQL tools for scanning.
3738
- name: Initialize CodeQL
38-
uses: github/codeql-action/init@v2
39+
uses: github/codeql-action/init@v3
3940
with:
4041
languages: ${{ matrix.language }}
4142

4243
- name: Perform CodeQL Analysis
43-
uses: github/codeql-action/analyze@v2
44+
uses: github/codeql-action/analyze@v3

.github/workflows/nodejs.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ jobs:
66
runs-on: ubuntu-latest
77
strategy:
88
matrix:
9-
node-version: [12, 14, 16, 18, 20]
9+
node-version: [18, 20, 21]
1010

1111
steps:
12-
- uses: actions/checkout@v3
12+
- uses: actions/checkout@v4
13+
with:
14+
show-progress: false
1315
- name: Use Node.js ${{ matrix.node-version }}
14-
uses: actions/setup-node@v3
16+
uses: actions/setup-node@v4
1517
with:
1618
node-version: ${{ matrix.node-version }}
1719
cache: 'npm'

.github/workflows/npm-publish.yml

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ jobs:
88
build-test:
99
runs-on: ubuntu-latest
1010
steps:
11-
- uses: actions/checkout@v3
12-
- uses: actions/setup-node@v3
11+
- uses: actions/checkout@v4
1312
with:
14-
node-version: 14
13+
show-progress: false
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: 18
1517
cache: 'npm'
1618
- run: npm ci --ignore-scripts
1719
- run: npm test
@@ -20,10 +22,12 @@ jobs:
2022
needs: build-test
2123
runs-on: ubuntu-latest
2224
steps:
23-
- uses: actions/checkout@v3
24-
- uses: actions/setup-node@v3
25+
- uses: actions/checkout@v4
26+
with:
27+
show-progress: false
28+
- uses: actions/setup-node@v4
2529
with:
26-
node-version: 14
30+
node-version: 18
2731
registry-url: https://registry.npmjs.org/
2832
cache: 'npm'
2933
- run: npm ci --ignore-scripts
@@ -43,10 +47,12 @@ jobs:
4347
contents: read
4448
packages: write
4549
steps:
46-
- uses: actions/checkout@v3
47-
- uses: actions/setup-node@v3
50+
- uses: actions/checkout@v4
51+
with:
52+
show-progress: false
53+
- uses: actions/setup-node@v4
4854
with:
49-
node-version: 14
55+
node-version: 18
5056
registry-url: https://npm.pkg.github.com/
5157
cache: 'npm'
5258
- run: npm ci --ignore-scripts

package-lock.json

Lines changed: 29 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@fluffy-spoon/substitute",
3-
"version": "2.0.0-beta.3",
3+
"version": "2.0.0-beta.4",
44
"description": "TypeScript port of NSubstitute, which aims to provide a much more fluent mocking opportunity for strong-typed languages",
55
"license": "MIT",
66
"funding": {
@@ -46,9 +46,12 @@
4646
"devDependencies": {
4747
"@ava/typescript": "^3.0.1",
4848
"@sinonjs/fake-timers": "^11.2.2",
49-
"@types/node": "^12.20.55",
49+
"@types/node": "^18.19.22",
5050
"@types/sinonjs__fake-timers": "^8.1.5",
5151
"ava": "^4.3.3",
5252
"typescript": "^4.8.4"
53+
},
54+
"volta": {
55+
"node": "18.19.1"
5356
}
5457
}

spec/regression/index.test.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,19 @@ test('class method received', t => {
121121
t.notThrows(() => substitute.received(1).c('hi', 'the1re'))
122122
t.notThrows(() => substitute.received().c('hi', 'there'))
123123

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

144150
t.notThrows(() => substitute.received(1).c('lala', 'bar'))
145-
const expectedMessage = 'Expected 2 calls to the method c with arguments [\'lala\', \'bar\'], but received 1 of such calls.\n' +
146-
'All calls received to method c:\n' +
147-
'-> call with arguments [\'lala\', \'bar\']'
151+
const expectedMessage = 'Call count mismatch in @Substitute.c:\n' +
152+
`Expected to receive 2 method calls matching c('lala', 'bar'), but received 1.\n` +
153+
'All property or method calls to @Substitute.c received so far:\n' +
154+
`› ✔ @Substitute.c('lala', 'bar')\n` +
155+
` called at <anonymous> (${process.cwd()}/spec/regression/index.test.ts:145:13)\n`
148156
const { message } = t.throws(() => substitute.received(2).c('lala', 'bar'))
149157
t.is(message.replace(textModifierRegex, ''), expectedMessage)
150158
t.deepEqual(substitute.d, 1337)

spec/regression/issues/138.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import test from 'ava'
2+
3+
import { Substitute } from '../../../src'
4+
5+
interface Library { }
6+
7+
test('issue 138: serializes to JSON compatible data', t => {
8+
const lib = Substitute.for<Library>()
9+
const result = JSON.stringify(lib)
10+
11+
t.true(typeof result === 'string')
12+
t.is(result, '"@Substitute {\\n}"')
13+
})

spec/regression/issues/27.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import test from 'ava'
2+
import { types } from 'util'
3+
4+
import { Substitute } from '../../../src'
5+
6+
interface Library {
7+
subSection: () => string
8+
}
9+
10+
// Adapted snipped extracted from https://github.com/angular/angular/blob/main/packages/compiler/src/parse_util.ts#L176
11+
// This is to reproduce the behavior of the Angular compiler. This function tries to extract the id from a reference.
12+
const identifierName = (compileIdentifier: { reference: any } | null | undefined): string | null => {
13+
if (!compileIdentifier || !compileIdentifier.reference) {
14+
return null
15+
}
16+
const ref = compileIdentifier.reference
17+
if (ref['__anonymousType']) {
18+
return ref['__anonymousType']
19+
}
20+
}
21+
22+
test('issue 27: mocks should work with Angular TestBed', t => {
23+
const lib = Substitute.for<Library>()
24+
lib.subSection().returns('This is the mocked value')
25+
const result = identifierName({ reference: lib })
26+
27+
t.not(result, null)
28+
t.true(types.isProxy(result))
29+
30+
const jitId = `jit_${result}`
31+
t.is(jitId, 'jit_property<__anonymousType>: ')
32+
})
33+
34+
test('issue 27: subsitute node can be coerced to a primitive value', t => {
35+
const lib = Substitute.for<Library>()
36+
t.true(typeof `${lib}` === 'string')
37+
t.true(typeof (lib + '') === 'string')
38+
t.is(`${lib}`, '@Substitute {\n}')
39+
})

src/Arguments.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ type ArgumentOptions = {
33
inverseMatch?: boolean
44
}
55
class BaseArgument<T> {
6+
private _description: string
67
constructor(
7-
private _description: string,
8+
description: string,
89
private _matchingFunction: PredicateFunction<T>,
910
private _options?: ArgumentOptions
10-
) { }
11+
) {
12+
this._description = `${this._options?.inverseMatch ? 'Not ' : ''}${description}`
13+
}
1114

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

6164
type Is = <T>(predicate: PredicateFunction<ExtractFirstArg<T>>) => ReturnArg<ExtractFirstArg<T>>
6265
const isFunction = <T>(predicate: PredicateFunction<ExtractFirstArg<T>>, options?: ArgumentOptions) => new Argument(
63-
`{predicate ${toStringify(predicate)}}`, predicate, options
66+
`Arg.is{${toStringify(predicate)}}`, predicate, options
6467
)
6568
export const is = createInversable(isFunction) as Inversable<Is>
6669

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

8184
const anyFunction = (type: AnyType = 'any', options?: ArgumentOptions) => {
82-
const description = `{type ${type}}`
85+
const description = `Arg.any{${type}}`
8386
const predicate = (x: any) => {
8487
switch (type) {
8588
case 'any':

0 commit comments

Comments
 (0)