@nestjs/testing 12.0.4 → 12.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +2 -2
- package/services/capturing-logger.service.d.ts +129 -0
- package/services/capturing-logger.service.js +179 -0
- package/testing-module.js +13 -3
package/index.d.ts
CHANGED
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs/testing",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.1.1",
|
|
4
4
|
"description": "Nest - modern, fast, powerful node.js web framework (@testing)",
|
|
5
5
|
"author": "Kamil Mysliwiec",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"optional": true
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "9feedffde8ef8c86f0cd521153740575dc579d88"
|
|
45
45
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { ConsoleLogger, type ConsoleLoggerOptions, type LogLevel } from '@nestjs/common';
|
|
2
|
+
/**
|
|
3
|
+
* A log call recorded by `CapturingLogger`.
|
|
4
|
+
*
|
|
5
|
+
* @publicApi
|
|
6
|
+
*/
|
|
7
|
+
export interface CapturedLog {
|
|
8
|
+
level: LogLevel;
|
|
9
|
+
/**
|
|
10
|
+
* The message. A lazy message (a function) is evaluated, and when an
|
|
11
|
+
* `Error` is logged as the message, this is the error's message.
|
|
12
|
+
*/
|
|
13
|
+
message: unknown;
|
|
14
|
+
/**
|
|
15
|
+
* The context, e.g. the one passed to `new Logger('Context')`.
|
|
16
|
+
*/
|
|
17
|
+
context?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The plain objects passed after the message, merged.
|
|
20
|
+
*/
|
|
21
|
+
params?: Record<string, any>;
|
|
22
|
+
/**
|
|
23
|
+
* The first `Error` passed to the call, as the message or after it.
|
|
24
|
+
*/
|
|
25
|
+
error?: Error;
|
|
26
|
+
/**
|
|
27
|
+
* The stack trace passed as a string, as in `logger.error(message, stack)`.
|
|
28
|
+
*/
|
|
29
|
+
stack?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The arguments, as passed to the logger (including the context).
|
|
32
|
+
*/
|
|
33
|
+
args: unknown[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Criteria for `CapturingLogger` queries. Every criterion that is set must
|
|
37
|
+
* match.
|
|
38
|
+
*
|
|
39
|
+
* @publicApi
|
|
40
|
+
*/
|
|
41
|
+
export interface CapturedLogFilter {
|
|
42
|
+
level?: LogLevel;
|
|
43
|
+
/**
|
|
44
|
+
* A string matches the context exactly.
|
|
45
|
+
*/
|
|
46
|
+
context?: string | RegExp;
|
|
47
|
+
/**
|
|
48
|
+
* A string matches when the message contains it. Only string messages
|
|
49
|
+
* match (an `Error` logged as the message counts, through its message).
|
|
50
|
+
*/
|
|
51
|
+
message?: string | RegExp;
|
|
52
|
+
/**
|
|
53
|
+
* Matches when each of these params is deeply equal to the logged one.
|
|
54
|
+
*/
|
|
55
|
+
params?: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* @publicApi
|
|
59
|
+
*/
|
|
60
|
+
export type CapturedLogMatcher = CapturedLogFilter | ((entry: CapturedLog) => boolean);
|
|
61
|
+
/**
|
|
62
|
+
* @publicApi
|
|
63
|
+
*/
|
|
64
|
+
export interface CapturingLoggerOptions extends ConsoleLoggerOptions {
|
|
65
|
+
/**
|
|
66
|
+
* If enabled, logs are also printed, as a `ConsoleLogger` with the same
|
|
67
|
+
* options would print them.
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
print?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A logger for tests that records every log call with its structured data,
|
|
74
|
+
* so that tests can assert on it. Pass it to `setLogger()` of the testing
|
|
75
|
+
* module builder, or to `app.useLogger()`, to capture the framework's logs and
|
|
76
|
+
* the logs of `Logger` instances.
|
|
77
|
+
*
|
|
78
|
+
* Every level is captured unless `logLevels` is set; the `NEST_LOG_LEVEL`
|
|
79
|
+
* environment variable is ignored.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const logger = new CapturingLogger();
|
|
83
|
+
* const moduleRef = await Test.createTestingModule({ providers: [PaymentsService] })
|
|
84
|
+
* .setLogger(logger)
|
|
85
|
+
* .compile();
|
|
86
|
+
*
|
|
87
|
+
* await moduleRef.get(PaymentsService).charge(order);
|
|
88
|
+
*
|
|
89
|
+
* logger.assertLogged({ level: 'warn', context: 'PaymentsService', message: /retrying/ });
|
|
90
|
+
*
|
|
91
|
+
* @publicApi
|
|
92
|
+
*/
|
|
93
|
+
export declare class CapturingLogger extends ConsoleLogger {
|
|
94
|
+
private readonly captured;
|
|
95
|
+
private readonly shouldPrint;
|
|
96
|
+
constructor(options?: CapturingLoggerOptions);
|
|
97
|
+
/**
|
|
98
|
+
* The recorded entries, oldest first.
|
|
99
|
+
*/
|
|
100
|
+
get entries(): CapturedLog[];
|
|
101
|
+
log(message: any, ...optionalParams: any[]): void;
|
|
102
|
+
error(message: any, ...optionalParams: any[]): void;
|
|
103
|
+
warn(message: any, ...optionalParams: any[]): void;
|
|
104
|
+
debug(message: any, ...optionalParams: any[]): void;
|
|
105
|
+
verbose(message: any, ...optionalParams: any[]): void;
|
|
106
|
+
fatal(message: any, ...optionalParams: any[]): void;
|
|
107
|
+
/**
|
|
108
|
+
* Returns the entries that match.
|
|
109
|
+
*/
|
|
110
|
+
filter(matcher?: CapturedLogMatcher): CapturedLog[];
|
|
111
|
+
/**
|
|
112
|
+
* Returns the first entry that matches.
|
|
113
|
+
*/
|
|
114
|
+
find(matcher: CapturedLogMatcher): CapturedLog | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* Throws an error listing the captured entries if no entry matches.
|
|
117
|
+
* @returns The first entry that matches.
|
|
118
|
+
*/
|
|
119
|
+
assertLogged(matcher: CapturedLogMatcher): CapturedLog;
|
|
120
|
+
/**
|
|
121
|
+
* Throws an error listing the matching entries if any entry matches.
|
|
122
|
+
*/
|
|
123
|
+
assertNotLogged(matcher: CapturedLogMatcher): void;
|
|
124
|
+
/**
|
|
125
|
+
* Removes the recorded entries.
|
|
126
|
+
*/
|
|
127
|
+
clear(): void;
|
|
128
|
+
private capture;
|
|
129
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { ConsoleLogger, LOG_LEVELS, } from '@nestjs/common';
|
|
2
|
+
import { inspect, isDeepStrictEqual } from 'util';
|
|
3
|
+
/**
|
|
4
|
+
* A logger for tests that records every log call with its structured data,
|
|
5
|
+
* so that tests can assert on it. Pass it to `setLogger()` of the testing
|
|
6
|
+
* module builder, or to `app.useLogger()`, to capture the framework's logs and
|
|
7
|
+
* the logs of `Logger` instances.
|
|
8
|
+
*
|
|
9
|
+
* Every level is captured unless `logLevels` is set; the `NEST_LOG_LEVEL`
|
|
10
|
+
* environment variable is ignored.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const logger = new CapturingLogger();
|
|
14
|
+
* const moduleRef = await Test.createTestingModule({ providers: [PaymentsService] })
|
|
15
|
+
* .setLogger(logger)
|
|
16
|
+
* .compile();
|
|
17
|
+
*
|
|
18
|
+
* await moduleRef.get(PaymentsService).charge(order);
|
|
19
|
+
*
|
|
20
|
+
* logger.assertLogged({ level: 'warn', context: 'PaymentsService', message: /retrying/ });
|
|
21
|
+
*
|
|
22
|
+
* @publicApi
|
|
23
|
+
*/
|
|
24
|
+
export class CapturingLogger extends ConsoleLogger {
|
|
25
|
+
captured = [];
|
|
26
|
+
shouldPrint;
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
const { print = false, ...consoleLoggerOptions } = options;
|
|
29
|
+
super({
|
|
30
|
+
...consoleLoggerOptions,
|
|
31
|
+
logLevels: consoleLoggerOptions.logLevels ?? [...LOG_LEVELS],
|
|
32
|
+
});
|
|
33
|
+
this.shouldPrint = print;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The recorded entries, oldest first.
|
|
37
|
+
*/
|
|
38
|
+
get entries() {
|
|
39
|
+
return [...this.captured];
|
|
40
|
+
}
|
|
41
|
+
log(message, ...optionalParams) {
|
|
42
|
+
this.capture('log', [message, ...optionalParams]);
|
|
43
|
+
}
|
|
44
|
+
error(message, ...optionalParams) {
|
|
45
|
+
this.capture('error', [message, ...optionalParams]);
|
|
46
|
+
}
|
|
47
|
+
warn(message, ...optionalParams) {
|
|
48
|
+
this.capture('warn', [message, ...optionalParams]);
|
|
49
|
+
}
|
|
50
|
+
debug(message, ...optionalParams) {
|
|
51
|
+
this.capture('debug', [message, ...optionalParams]);
|
|
52
|
+
}
|
|
53
|
+
verbose(message, ...optionalParams) {
|
|
54
|
+
this.capture('verbose', [message, ...optionalParams]);
|
|
55
|
+
}
|
|
56
|
+
fatal(message, ...optionalParams) {
|
|
57
|
+
this.capture('fatal', [message, ...optionalParams]);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Returns the entries that match.
|
|
61
|
+
*/
|
|
62
|
+
filter(matcher = {}) {
|
|
63
|
+
const predicate = toPredicate(matcher);
|
|
64
|
+
return this.captured.filter(entry => predicate(entry));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns the first entry that matches.
|
|
68
|
+
*/
|
|
69
|
+
find(matcher) {
|
|
70
|
+
return this.captured.find(toPredicate(matcher));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Throws an error listing the captured entries if no entry matches.
|
|
74
|
+
* @returns The first entry that matches.
|
|
75
|
+
*/
|
|
76
|
+
assertLogged(matcher) {
|
|
77
|
+
const entry = this.find(matcher);
|
|
78
|
+
if (!entry) {
|
|
79
|
+
throw new Error(`Expected a log entry matching ${describeMatcher(matcher)}, but none of the ${this.captured.length} captured entries matched.${formatEntries(this.captured)}`);
|
|
80
|
+
}
|
|
81
|
+
return entry;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Throws an error listing the matching entries if any entry matches.
|
|
85
|
+
*/
|
|
86
|
+
assertNotLogged(matcher) {
|
|
87
|
+
const entries = this.filter(matcher);
|
|
88
|
+
if (entries.length > 0) {
|
|
89
|
+
throw new Error(`Expected no log entry matching ${describeMatcher(matcher)}, but ${entries.length} matched.${formatEntries(entries)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Removes the recorded entries.
|
|
94
|
+
*/
|
|
95
|
+
clear() {
|
|
96
|
+
this.captured.length = 0;
|
|
97
|
+
}
|
|
98
|
+
capture(level, args) {
|
|
99
|
+
if (!this.isLevelEnabled(level)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const parsed = level === 'error' || level === 'fatal'
|
|
103
|
+
? this.getContextAndStackAndMessagesToPrint(args)
|
|
104
|
+
: this.getContextAndMessagesToPrint(args);
|
|
105
|
+
// Evaluated once, for the entry and for printing.
|
|
106
|
+
const resolvedMessages = parsed.messages.map(message => this.resolveMessage(message));
|
|
107
|
+
const { messages, error } = this.extractJsonError(resolvedMessages);
|
|
108
|
+
const params = 'params' in parsed ? parsed.params : undefined;
|
|
109
|
+
const stack = 'stack' in parsed && typeof parsed.stack === 'string'
|
|
110
|
+
? parsed.stack
|
|
111
|
+
: undefined;
|
|
112
|
+
const entry = { level, message: messages[0], args };
|
|
113
|
+
if (parsed.context) {
|
|
114
|
+
entry.context = parsed.context;
|
|
115
|
+
}
|
|
116
|
+
if (params) {
|
|
117
|
+
entry.params = params;
|
|
118
|
+
}
|
|
119
|
+
if (error) {
|
|
120
|
+
entry.error = error;
|
|
121
|
+
}
|
|
122
|
+
if (stack) {
|
|
123
|
+
entry.stack = stack;
|
|
124
|
+
}
|
|
125
|
+
this.captured.push(entry);
|
|
126
|
+
if (this.shouldPrint) {
|
|
127
|
+
// What the ConsoleLogger method for this level does once the arguments
|
|
128
|
+
// are parsed.
|
|
129
|
+
const isError = level === 'error' || level === 'fatal';
|
|
130
|
+
this.printMessages(resolvedMessages, parsed.context, level, isError ? 'stderr' : 'stdout', stack, params);
|
|
131
|
+
if (isError) {
|
|
132
|
+
this.printStackTrace(stack);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function toPredicate(matcher) {
|
|
138
|
+
if (typeof matcher === 'function') {
|
|
139
|
+
return matcher;
|
|
140
|
+
}
|
|
141
|
+
const { level, context, message, params } = matcher;
|
|
142
|
+
return entry => (level === undefined || entry.level === level) &&
|
|
143
|
+
(context === undefined ||
|
|
144
|
+
(typeof context === 'string'
|
|
145
|
+
? entry.context === context
|
|
146
|
+
: entry.context !== undefined &&
|
|
147
|
+
entry.context.search(context) !== -1)) &&
|
|
148
|
+
(message === undefined ||
|
|
149
|
+
(typeof entry.message === 'string' &&
|
|
150
|
+
(typeof message === 'string'
|
|
151
|
+
? entry.message.includes(message)
|
|
152
|
+
: entry.message.search(message) !== -1))) &&
|
|
153
|
+
(params === undefined ||
|
|
154
|
+
Object.entries(params).every(([key, value]) => isDeepStrictEqual(entry.params?.[key], value)));
|
|
155
|
+
}
|
|
156
|
+
function describeMatcher(matcher) {
|
|
157
|
+
return typeof matcher === 'function'
|
|
158
|
+
? 'the predicate'
|
|
159
|
+
: inspect(matcher, { breakLength: Infinity });
|
|
160
|
+
}
|
|
161
|
+
const MAX_LISTED_ENTRIES = 50;
|
|
162
|
+
function formatEntries(entries) {
|
|
163
|
+
if (entries.length === 0) {
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
const lines = entries.slice(-MAX_LISTED_ENTRIES).map(entry => {
|
|
167
|
+
const context = entry.context ? ` [${entry.context}]` : '';
|
|
168
|
+
const message = typeof entry.message === 'string'
|
|
169
|
+
? entry.message
|
|
170
|
+
: inspect(entry.message, { breakLength: Infinity, depth: 2 });
|
|
171
|
+
const params = entry.params
|
|
172
|
+
? ` ${inspect(entry.params, { breakLength: Infinity, depth: 2 })}`
|
|
173
|
+
: '';
|
|
174
|
+
return ` ${entry.level.toUpperCase()}${context} ${message}${params}`;
|
|
175
|
+
});
|
|
176
|
+
const omitted = entries.length - lines.length;
|
|
177
|
+
const header = omitted > 0 ? `\n (${omitted} earlier entries omitted)` : '';
|
|
178
|
+
return `\nCaptured entries:${header}\n${lines.join('\n')}`;
|
|
179
|
+
}
|
package/testing-module.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Logger, } from '@nestjs/common';
|
|
2
2
|
import { NestApplication, NestApplicationContext, } from '@nestjs/core';
|
|
3
|
-
import { tryLoadPackage, loadPackageCached, isUndefined, } from '@nestjs/common/internal';
|
|
3
|
+
import { tryLoadPackage, loadPackageCached, isFunction, isUndefined, } from '@nestjs/common/internal';
|
|
4
4
|
/**
|
|
5
5
|
* @publicApi
|
|
6
6
|
*/
|
|
@@ -50,13 +50,23 @@ export class TestingModule extends NestApplicationContext {
|
|
|
50
50
|
Logger.overrideLogger(options.logger);
|
|
51
51
|
}
|
|
52
52
|
createAdapterProxy(app, adapter) {
|
|
53
|
-
|
|
53
|
+
const proxy = new Proxy(app, {
|
|
54
54
|
get: (receiver, prop) => {
|
|
55
55
|
if (!(prop in receiver) && prop in adapter) {
|
|
56
|
-
|
|
56
|
+
const value = adapter[prop];
|
|
57
|
+
if (!isFunction(value)) {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
// Called on the adapter, as on applications created by NestFactory.
|
|
61
|
+
// Chainable methods keep returning the application.
|
|
62
|
+
return (...args) => {
|
|
63
|
+
const result = value.apply(adapter, args);
|
|
64
|
+
return result === adapter ? proxy : result;
|
|
65
|
+
};
|
|
57
66
|
}
|
|
58
67
|
return receiver[prop];
|
|
59
68
|
},
|
|
60
69
|
});
|
|
70
|
+
return proxy;
|
|
61
71
|
}
|
|
62
72
|
}
|